Merge branch 'MDL-80191-main-v06' of https://github.com/ferranrecio/moodle

This commit is contained in:
Sara Arjona 2024-07-01 12:26:46 +02:00
commit 4fcea1f716
No known key found for this signature in database
22 changed files with 946 additions and 184 deletions

View File

@ -38,6 +38,9 @@ abstract class backup_activity_task extends backup_task {
protected $moduleid;
protected $sectionid;
/** @var stdClass the section object */
protected $section;
protected $modulename;
protected $activityid;
protected $contextid;
@ -50,6 +53,7 @@ abstract class backup_activity_task extends backup_task {
* @param backup_plan|null $plan the backup plan instance this task is part of
*/
public function __construct($name, $moduleid, $plan = null) {
global $DB;
// Check moduleid exists
if (!$coursemodule = get_coursemodule_from_id(false, $moduleid)) {
@ -65,6 +69,7 @@ abstract class backup_activity_task extends backup_task {
$this->modulename = $coursemodule->modname;
$this->activityid = $coursemodule->instance;
$this->contextid = context_module::instance($this->moduleid)->id;
$this->section = $DB->get_record('course_sections', ['id' => $this->sectionid]);
parent::__construct($name, $plan);
}
@ -90,6 +95,16 @@ abstract class backup_activity_task extends backup_task {
return $this->modulename;
}
/**
* Return if the activity is inside a subsection.
*
* @return bool
*/
public function is_in_subsection(): bool {
return !empty($this->section->component);
}
/**
* @return int the id of the activity instance (id in the activity's instances table)
*/
@ -123,12 +138,12 @@ abstract class backup_activity_task extends backup_task {
}
// Add some extra settings that related processors are going to need
$this->add_setting(new backup_activity_generic_setting(backup::VAR_MODID, base_setting::IS_INTEGER, $this->moduleid));
$this->add_setting(new backup_activity_generic_setting(backup::VAR_COURSEID, base_setting::IS_INTEGER, $this->get_courseid()));
$this->add_setting(new backup_activity_generic_setting(backup::VAR_SECTIONID, base_setting::IS_INTEGER, $this->sectionid));
$this->add_setting(new backup_activity_generic_setting(backup::VAR_MODNAME, base_setting::IS_FILENAME, $this->modulename));
$this->add_setting(new backup_activity_generic_setting(backup::VAR_ACTIVITYID, base_setting::IS_INTEGER, $this->activityid));
$this->add_setting(new backup_activity_generic_setting(backup::VAR_CONTEXTID, base_setting::IS_INTEGER, $this->contextid));
$this->add_section_setting(backup::VAR_MODID, base_setting::IS_INTEGER, $this->moduleid);
$this->add_section_setting(backup::VAR_COURSEID, base_setting::IS_INTEGER, $this->get_courseid());
$this->add_section_setting(backup::VAR_SECTIONID, base_setting::IS_INTEGER, $this->sectionid);
$this->add_section_setting(backup::VAR_MODNAME, base_setting::IS_FILENAME, $this->modulename);
$this->add_section_setting(backup::VAR_ACTIVITYID, base_setting::IS_INTEGER, $this->activityid);
$this->add_section_setting(backup::VAR_CONTEXTID, base_setting::IS_INTEGER, $this->contextid);
// Create the activity directory
$this->add_step(new create_taskbasepath_directory('create_activity_directory'));
@ -269,58 +284,111 @@ abstract class backup_activity_task extends backup_task {
$settingprefix = $this->modulename . '_' . $this->moduleid . '_';
// All these are common settings to be shared by all activities.
// Define activity_include (to decide if the whole task must be really executed)
// Dependent of:
// - activities root setting.
// - section_included setting (if exists).
$settingname = $settingprefix . 'included';
$activityincluded = new backup_activity_generic_setting($settingname, base_setting::IS_BOOLEAN, true);
$activityincluded->get_ui()->set_icon(new image_icon('monologo', get_string('pluginname', $this->modulename),
$this->modulename, array('class' => 'iconlarge icon-post ml-1')));
$this->add_setting($activityincluded);
// Look for "activities" root setting.
$activities = $this->plan->get_setting('activities');
$activities->add_dependency($activityincluded);
$activityincluded = $this->add_activity_included_setting($settingprefix);
if (question_module_uses_questions($this->modulename)) {
$questionbank = $this->plan->get_setting('questionbank');
$questionbank->add_dependency($activityincluded);
}
$this->add_activity_userinfo_setting($settingprefix, $activityincluded);
// End of common activity settings, let's add the particular ones.
$this->define_my_settings();
}
/**
* Add a setting to the task. This method is used to add a setting to the task
*
* @param int|string $identifier the identifier of the setting
* @param string $type the type of the setting
* @param string|int $value the value of the setting
* @return section_backup_setting the setting added
*/
protected function add_section_setting(int|string $identifier, string $type, string|int $value): activity_backup_setting {
if ($this->is_in_subsection()) {
$setting = new backup_subactivity_generic_setting($identifier, $type, $value);
} else {
$setting = new backup_activity_generic_setting($identifier, $type, $value);
}
$this->add_setting($setting);
return $setting;
}
/**
* Add the section include setting to the task.
*
* @param string $settingprefix the identifier of the setting
* @return activity_backup_setting the setting added
*/
protected function add_activity_included_setting(string $settingprefix): activity_backup_setting {
// Define activity_include (to decide if the whole task must be really executed)
// Dependent of:
// - activities root setting.
// - sectionincluded setting (if exists).
$settingname = $settingprefix . 'included';
if ($this->is_in_subsection()) {
$activityincluded = new backup_subactivity_generic_setting($settingname, base_setting::IS_BOOLEAN, true);
} else {
$activityincluded = new backup_activity_generic_setting($settingname, base_setting::IS_BOOLEAN, true);
}
$activityincluded->get_ui()->set_icon(new image_icon('monologo', get_string('pluginname', $this->modulename),
$this->modulename, array('class' => 'iconlarge icon-post ml-1')));
$this->add_setting($activityincluded);
// Look for "activities" root setting.
$activities = $this->plan->get_setting('activities');
$activities->add_dependency($activityincluded);
// Look for "sectionincluded" section setting (if exists).
$settingname = 'section_' . $this->sectionid . '_included';
if ($this->plan->setting_exists($settingname)) {
$sectionincluded = $this->plan->get_setting($settingname);
$sectionincluded->add_dependency($activityincluded);
}
return $activityincluded;
}
// Define activityuserinfo. Dependent of:
/**
* Add the section userinfo setting to the task.
*
* @param string $settingprefix the identifier of the setting
* @param activity_backup_setting $includefield the setting to depend on
* @return activity_backup_setting the setting added
*/
protected function add_activity_userinfo_setting(
string $settingprefix,
activity_backup_setting $includefield
): activity_backup_setting {
// Define activity_userinfo. Dependent of:
// - users root setting.
// - sectionuserinfo setting (if exists).
// - activityincluded setting.
// - includefield setting.
$settingname = $settingprefix . 'userinfo';
$activityuserinfo = new backup_activity_userinfo_setting($settingname, base_setting::IS_BOOLEAN, true);
if ($this->is_in_subsection()) {
$activityuserinfo = new backup_subactivity_userinfo_setting($settingname, base_setting::IS_BOOLEAN, true);
} else {
$activityuserinfo = new backup_activity_userinfo_setting($settingname, base_setting::IS_BOOLEAN, true);
}
$activityuserinfo->get_ui()->set_label('-');
$activityuserinfo->get_ui()->set_visually_hidden_label(
get_string('includeuserinfo_instance', 'core_backup', $this->name)
);
$this->add_setting($activityuserinfo);
// Look for "users" root setting.
$users = $this->plan->get_setting('users');
$users->add_dependency($activityuserinfo);
// Look for "sectionuserinfo" section setting (if exists).
$settingname = 'section_' . $this->sectionid . '_userinfo';
if ($this->plan->setting_exists($settingname)) {
$sectionuserinfo = $this->plan->get_setting($settingname);
$sectionuserinfo->add_dependency($activityuserinfo);
}
// Look for "activityincluded" setting.
$activityincluded->add_dependency($activityuserinfo);
// End of common activity settings, let's add the particular ones.
$this->define_my_settings();
$includefield->add_dependency($activityuserinfo);
return $activityuserinfo;
}
/**

View File

@ -133,6 +133,9 @@ abstract class backup_plan_builder {
try {
$plan->add_task(backup_factory::get_backup_activity_task($controller->get_format(), $id));
// Some activities may have delegated section integrations.
self::build_delegated_section_plan($controller, $id);
// For the given activity, add as many block tasks as necessary
$blockids = backup_plan_dbops::get_blockids_from_moduleid($id);
foreach ($blockids as $blockid) {
@ -150,6 +153,44 @@ abstract class backup_plan_builder {
}
}
/**
* Build a course module delegated section backup plan.
* @param backup_controller $controller
* @param int $cmid the parent course module id.
*/
protected static function build_delegated_section_plan($controller, $cmid) {
global $CFG, $DB;
// Check moduleid exists.
if (!$coursemodule = get_coursemodule_from_id(false, $cmid)) {
$controller->log(get_string('error_course_module_not_found', 'backup', $cmid), backup::LOG_WARNING);
}
$classname = 'mod_' . $coursemodule->modname . '\courseformat\sectiondelegate';
if (!class_exists($classname)) {
return;
}
$sectionid = null;
try {
$sectionid = $classname::delegated_section_id($coursemodule);
} catch (dml_exception $error) {
$controller->log(get_string('error_delegate_section_not_found', 'backup', $cmid), backup::LOG_WARNING);
return;
}
$plan = $controller->get_plan();
$sectiontask = backup_factory::get_backup_section_task($controller->get_format(), $sectionid);
$sectiontask->set_delegated_cm($cmid);
$plan->add_task($sectiontask);
// For the given section, add as many activity tasks as necessary.
$coursemodules = backup_plan_dbops::get_modules_from_sectionid($sectionid);
foreach ($coursemodules as $coursemodule) {
if (plugin_supports('mod', $coursemodule->modname, FEATURE_BACKUP_MOODLE2)) {
self::build_activity_plan($controller, $coursemodule->id);
}
}
}
/**
* Build one 1-section backup
*/
@ -185,8 +226,13 @@ abstract class backup_plan_builder {
// For the given course, add as many section tasks as necessary
$sections = backup_plan_dbops::get_sections_from_courseid($id);
foreach ($sections as $section) {
self::build_section_plan($controller, $section);
foreach ($sections as $sectionid) {
// Delegated sections are not course responsability.
$sectiondata = backup_plan_dbops::get_section_from_id($sectionid);
if (!empty($sectiondata->component)) {
continue;
}
self::build_section_plan($controller, $sectionid);
}
// For the given course, add as many block tasks as necessary

View File

@ -37,6 +37,16 @@ class backup_section_task extends backup_task {
protected $sectionid;
/**
* @var stdClass $section The database section object.
*/
protected stdClass $section;
/**
* @var int|null $delegatedcmid the course module that is delegating this section (if any)
*/
protected ?int $delegatedcmid = null;
/**
* Constructor - instantiates one object of this class
*/
@ -48,11 +58,45 @@ class backup_section_task extends backup_task {
throw new backup_task_exception('section_task_section_not_found', $sectionid);
}
$this->section = $section;
$this->sectionid = $sectionid;
parent::__construct($name, $plan);
}
/**
* Set the course module that is delegating this section.
*
* Delegated section can belong to any kind of plugin. However, when a delegated
* section belongs to a course module, the UI will present all settings according.
*
* @param int $cmid the course module id that is delegating this section
*/
public function set_delegated_cm(int $cmid) {
$this->delegatedcmid = $cmid;
}
/**
* Get the course module that is delegating this section.
*
* @return int|null the course module id that is delegating this section
*/
public function get_delegated_cm(): ?int {
return $this->delegatedcmid;
}
/**
* Get the delegate activity modname (if any).
*
* @return string|null the modname of the delegated activity
*/
public function get_modname(): ?string {
if (empty($this->section->component)) {
return null;
}
return core_component::normalize_component($this->section->component)[1];
}
public function get_sectionid() {
return $this->sectionid;
}
@ -72,11 +116,11 @@ class backup_section_task extends backup_task {
// Set the backup::VAR_CONTEXTID setting to course context as far as next steps require that
$coursectxid = context_course::instance($this->get_courseid())->id;
$this->add_setting(new backup_activity_generic_setting(backup::VAR_CONTEXTID, base_setting::IS_INTEGER, $coursectxid));
$this->add_section_setting(backup::VAR_CONTEXTID, base_setting::IS_INTEGER, $coursectxid);
// Add some extra settings that related processors are going to need
$this->add_setting(new backup_activity_generic_setting(backup::VAR_SECTIONID, base_setting::IS_INTEGER, $this->sectionid));
$this->add_setting(new backup_activity_generic_setting(backup::VAR_COURSEID, base_setting::IS_INTEGER, $this->get_courseid()));
$this->add_section_setting(backup::VAR_SECTIONID, base_setting::IS_INTEGER, $this->sectionid);
$this->add_section_setting(backup::VAR_COURSEID, base_setting::IS_INTEGER, $this->get_courseid());
// Create the section directory
$this->add_step(new create_taskbasepath_directory('create_section_directory'));
@ -145,32 +189,105 @@ class backup_section_task extends backup_task {
// All the settings related to this activity will include this prefix.
$settingprefix = 'section_' . $this->sectionid . '_';
// All these are common settings to be shared by all sections.
$incudefield = $this->add_section_included_setting($settingprefix);
$this->add_section_userinfo_setting($settingprefix, $incudefield);
}
$section = $DB->get_record('course_sections', array('id' => $this->sectionid), '*', MUST_EXIST);
$course = $DB->get_record('course', array('id' => $section->course), '*', MUST_EXIST);
/**
* Add a setting to the task. This method is used to add a setting to the task
*
* @param int|string $identifier the identifier of the setting
* @param string $type the type of the setting
* @param string|int $value the value of the setting
* @return section_backup_setting the setting added
*/
protected function add_section_setting(int|string $identifier, string $type, string|int $value): section_backup_setting {
if ($this->get_delegated_cm()) {
$setting = new backup_subsection_generic_setting($identifier, $type, $value);
} else {
$setting = new backup_section_generic_setting($identifier, $type, $value);
}
$this->add_setting($setting);
return $setting;
}
/**
* Add the section included setting to the task.
*
* @param string $settingprefix the identifier of the setting
* @return section_backup_setting the setting added
*/
protected function add_section_included_setting(string $settingprefix): section_backup_setting {
global $DB;
$course = $DB->get_record('course', ['id' => $this->section->course], '*', MUST_EXIST);
// Define sectionincluded (to decide if the whole task must be really executed).
$settingname = $settingprefix . 'included';
$sectionincluded = new backup_section_included_setting($settingname, base_setting::IS_BOOLEAN, true);
$sectionincluded->get_ui()->set_label(get_section_name($course, $section));
$delegatedcmid = $this->get_delegated_cm();
if ($delegatedcmid) {
$sectionincluded = new backup_subsection_included_setting($settingname, base_setting::IS_BOOLEAN, true);
// Subsections depends on the parent activity included setting.
$settingname = $this->get_modname() . '_' . $delegatedcmid . '_included';
if ($this->plan->setting_exists($settingname)) {
$cmincluded = $this->plan->get_setting($settingname);
$cmincluded->add_dependency(
$sectionincluded,
);
}
$sectionincluded->get_ui()->set_label(get_string('subsectioncontent', 'backup'));
} else {
$sectionincluded = new backup_section_included_setting($settingname, base_setting::IS_BOOLEAN, true);
$sectionincluded->get_ui()->set_label(get_section_name($course, $this->section));
}
$this->add_setting($sectionincluded);
return $sectionincluded;
}
/**
* Add the section userinfo setting to the task.
*
* @param string $settingprefix the identifier of the setting
* @param section_backup_setting $includefield the setting to depend on
* @return section_backup_setting the setting added
*/
protected function add_section_userinfo_setting(
string $settingprefix,
section_backup_setting $includefield
): section_backup_setting {
// Define sectionuserinfo. Dependent of:
// - users root setting.
// - sectionincluded setting.
// - section_included setting.
$settingname = $settingprefix . 'userinfo';
$sectionuserinfo = new backup_section_userinfo_setting($settingname, base_setting::IS_BOOLEAN, true);
$delegatedcmid = $this->get_delegated_cm();
if ($delegatedcmid) {
$sectionuserinfo = new backup_subsection_userinfo_setting($settingname, base_setting::IS_BOOLEAN, true);
// Subsections depends on the parent activity included setting.
$settingname = $this->get_modname() . '_' . $delegatedcmid . '_userinfo';
if ($this->plan->setting_exists($settingname)) {
$cmincluded = $this->plan->get_setting($settingname);
$cmincluded->add_dependency(
$sectionuserinfo,
);
}
} else {
$sectionuserinfo = new backup_section_userinfo_setting($settingname, base_setting::IS_BOOLEAN, true);
}
$sectionuserinfo->get_ui()->set_label(get_string('includeuserinfo', 'backup'));
$sectionuserinfo->get_ui()->set_visually_hidden_label(
get_string('section_prefix', 'core_backup', $section->name ?: $section->section)
get_string('section_prefix', 'core_backup', $this->section->name ?: $this->section->section)
);
$this->add_setting($sectionuserinfo);
// Look for "users" root setting.
$users = $this->plan->get_setting('users');
$users->add_dependency($sectionuserinfo);
// Look for "section_included" section setting.
$sectionincluded->add_dependency($sectionuserinfo);
$includefield->add_dependency($sectionuserinfo);
return $sectionuserinfo;
}
}

View File

@ -185,6 +185,45 @@ class backup_section_included_setting extends section_backup_setting {}
*/
class backup_section_userinfo_setting extends section_backup_setting {}
/**
* Subsection base class (section delegated to a course module).
*/
class subsection_backup_setting extends section_backup_setting {
/**
* Class constructor.
*
* @param string $name Name of the setting
* @param string $vtype Type of the setting, for example base_setting::IS_TEXT
* @param mixed $value Value of the setting
* @param bool $visibility Is the setting visible in the UI, for example base_setting::VISIBLE
* @param int $status Status of the setting with regards to the locking, for example base_setting::NOT_LOCKED
*/
public function __construct($name, $vtype, $value = null, $visibility = self::VISIBLE, $status = self::NOT_LOCKED) {
parent::__construct($name, $vtype, $value, $visibility, $status);
$this->level = self::SUBSECTION_LEVEL;
}
}
/**
* generic section setting to pass various settings between tasks and steps
*/
class backup_subsection_generic_setting extends subsection_backup_setting {
}
/**
* Setting to define if one section is included or no. Activities _included
* settings depend of them if available
*/
class backup_subsection_included_setting extends subsection_backup_setting {
}
/**
* section backup setting to control if section will include
* user information or no, depends of @backup_users_setting
*/
class backup_subsection_userinfo_setting extends subsection_backup_setting {
}
// Activity backup settings
@ -206,6 +245,46 @@ class backup_activity_included_setting extends activity_backup_setting {}
*/
class backup_activity_userinfo_setting extends activity_backup_setting {}
/**
* Subactivity base class (activity inside a delegated section).
*/
class subactivity_backup_setting extends activity_backup_setting {
/**
* Class constructor.
*
* @param string $name Name of the setting
* @param string $vtype Type of the setting, for example base_setting::IS_TEXT
* @param mixed $value Value of the setting
* @param bool $visibility Is the setting visible in the UI, for example base_setting::VISIBLE
* @param int $status Status of the setting with regards to the locking, for example base_setting::NOT_LOCKED
*/
public function __construct($name, $vtype, $value = null, $visibility = self::VISIBLE, $status = self::NOT_LOCKED) {
parent::__construct($name, $vtype, $value, $visibility, $status);
$this->level = self::SUBACTIVITY_LEVEL;
}
}
/**
* Generic subactivity (activity inside a delegated section) setting to pass various settings between tasks and steps
*/
class backup_subactivity_generic_setting extends subactivity_backup_setting {
}
/**
* Subactivity (activity inside a delegated section) backup setting to control if activity will
* be included or no, depends of @backup_activities_setting and
* optionally parent section included setting
*/
class backup_subactivity_included_setting extends subactivity_backup_setting {
}
/**
* Subactivity (activity inside a delegated section) backup setting to control if activity will include
* user information or no, depends of @backup_users_setting
*/
class backup_subactivity_userinfo_setting extends subactivity_backup_setting {
}
/**
* Root setting to control if backup will include content bank content or no
*/

View File

@ -119,6 +119,20 @@ abstract class backup_activity_structure_step extends backup_structure_step {
// Return the root element (activity)
return $activity;
}
/**
* Set a delegate section itemid mapping.
*
* @param string $pluginname the name of the plugin that is delegating the section.
* @param int $itemid the itemid of the section being delegated.
*/
protected function set_delegated_section_mapping(string $pluginname, int $itemid) {
backup_structure_dbops::insert_backup_ids_record(
$this->get_backupid(),
"course_section::$pluginname::$itemid",
$this->task->get_moduleid()
);
}
}
/**
@ -2122,14 +2136,19 @@ class backup_main_structure_step extends backup_structure_step {
$activities = new backup_nested_element('activities');
$activity = new backup_nested_element('activity', null, array(
'moduleid', 'sectionid', 'modulename', 'title',
'directory'));
$activity = new backup_nested_element(
'activity',
null,
['moduleid', 'sectionid', 'modulename', 'title', 'directory', 'insubsection']
);
$sections = new backup_nested_element('sections');
$section = new backup_nested_element('section', null, array(
'sectionid', 'title', 'directory'));
$section = new backup_nested_element(
'section',
null,
['sectionid', 'title', 'directory', 'parentcmid', 'modname']
);
$course = new backup_nested_element('course', null, array(
'courseid', 'title', 'directory'));

View File

@ -100,6 +100,15 @@ abstract class restore_activity_task extends restore_task {
return $this->moduleid;
}
/**
* Return if the activity is inside a subsection.
*
* @return bool
*/
public function is_in_subsection(): bool {
return !empty($this->info->insubsection);
}
/**
* Returns the old course module id (cmid of activity which will be restored)
*
@ -288,42 +297,79 @@ abstract class restore_activity_task extends restore_task {
* Define the common setting that any restore activity will have
*/
protected function define_settings() {
// All the settings related to this activity will include this prefix
$settingprefix = $this->info->modulename . '_' . $this->info->moduleid . '_';
// All these are common settings to be shared by all activities
$activityincluded = $this->add_activity_included_setting($settingprefix);
$this->add_activity_userinfo_setting($settingprefix, $activityincluded);
// Define activity_include (to decide if the whole task must be really executed)
// End of common activity settings, let's add the particular ones.
$this->define_my_settings();
}
/**
* Add the activity included setting to the task.
*
* @param string $settingprefix the identifier of the setting
* @return activity_backup_setting the setting added
*/
protected function add_activity_included_setting(string $settingprefix): activity_backup_setting {
// Define activity_included (to decide if the whole task must be really executed)
// Dependent of:
// - activities root setting
// - section_included setting (if exists)
// - activities root setting.
// - sectionincluded setting (if exists).
$settingname = $settingprefix . 'included';
$activity_included = new restore_activity_generic_setting($settingname, base_setting::IS_BOOLEAN, true);
$activity_included->get_ui()->set_icon(new image_icon('monologo', get_string('pluginname', $this->modulename),
$this->modulename, array('class' => 'iconlarge icon-post ml-1')));
$this->add_setting($activity_included);
// Look for "activities" root setting
$activities = $this->plan->get_setting('activities');
$activities->add_dependency($activity_included);
// Look for "section_included" section setting (if exists)
$settingname = 'section_' . $this->info->sectionid . '_included';
if ($this->plan->setting_exists($settingname)) {
$section_included = $this->plan->get_setting($settingname);
$section_included->add_dependency($activity_included);
if ($this->is_in_subsection()) {
$activityincluded = new restore_subactivity_generic_setting($settingname, base_setting::IS_BOOLEAN, true);
} else {
$activityincluded = new restore_activity_generic_setting($settingname, base_setting::IS_BOOLEAN, true);
}
// Define activity_userinfo. Dependent of:
// - users root setting
// - section_userinfo setting (if exists)
// - activity_included setting.
$activityincluded->get_ui()->set_icon(new image_icon('monologo', get_string('pluginname', $this->modulename),
$this->modulename, ['class' => 'iconlarge icon-post ml-1']));
$this->add_setting($activityincluded);
// Look for "activities" root setting.
$activities = $this->plan->get_setting('activities');
$activities->add_dependency($activityincluded);
// Look for "sectionincluded" section setting (if exists).
$settingname = 'section_' . $this->info->sectionid . '_included';
if ($this->plan->setting_exists($settingname)) {
$sectionincluded = $this->plan->get_setting($settingname);
$sectionincluded->add_dependency($activityincluded);
}
return $activityincluded;
}
/**
* Add the activity userinfo setting to the task.
*
* @param string $settingprefix the identifier of the setting
* @param activity_backup_setting $includefield the activity included setting
* @return activity_backup_setting the setting added
*/
protected function add_activity_userinfo_setting(
string $settingprefix,
activity_backup_setting $includefield
): activity_backup_setting {
// Define activityuserinfo. Dependent of:
// - users root setting.
// - sectionuserinfo setting (if exists).
// - activity included setting.
$settingname = $settingprefix . 'userinfo';
$defaultvalue = false;
if (isset($this->info->settings[$settingname]) && $this->info->settings[$settingname]) { // Only enabled when available
$defaultvalue = true;
}
$activity_userinfo = new restore_activity_userinfo_setting($settingname, base_setting::IS_BOOLEAN, $defaultvalue);
if ($this->is_in_subsection()) {
$activityuserinfo = new restore_subactivity_userinfo_setting($settingname, base_setting::IS_BOOLEAN, $defaultvalue);
} else {
$activityuserinfo = new restore_activity_userinfo_setting($settingname, base_setting::IS_BOOLEAN, $defaultvalue);
}
if (!$defaultvalue) {
// This is a bit hacky, but if there is no user data to restore, then
// we replace the standard check-box with a select menu with the
@ -333,30 +379,34 @@ abstract class restore_activity_task extends restore_task {
// It would probably be better design to have a special UI class
// setting_ui_checkbox_or_no, rather than this hack, but I am not
// going to do that today.
$activity_userinfo->set_ui(new backup_setting_ui_select($activity_userinfo, '-',
array(0 => get_string('no'))));
$activityuserinfo->set_ui(
new backup_setting_ui_select(
$activityuserinfo,
'-',
[0 => get_string('no')]
)
);
} else {
$activity_userinfo->get_ui()->set_label('-');
$activityuserinfo->get_ui()->set_label('-');
}
$this->add_setting($activity_userinfo);
$this->add_setting($activityuserinfo);
// Look for "users" root setting
// Look for "users" root setting.
$users = $this->plan->get_setting('users');
$users->add_dependency($activity_userinfo);
$users->add_dependency($activityuserinfo);
// Look for "section_userinfo" section setting (if exists)
// Look for "sectionuserinfo" section setting (if exists).
$settingname = 'section_' . $this->info->sectionid . '_userinfo';
if ($this->plan->setting_exists($settingname)) {
$section_userinfo = $this->plan->get_setting($settingname);
$section_userinfo->add_dependency($activity_userinfo);
$sectionuserinfo = $this->plan->get_setting($settingname);
$sectionuserinfo->add_dependency($activityuserinfo);
}
// Look for "activity_included" setting.
$activity_included->add_dependency($activity_userinfo);
// Look for "activity included" setting.
$includefield->add_dependency($activityuserinfo);
// End of common activity settings, let's add the particular ones.
$this->define_my_settings();
return $activityuserinfo;
}
/**

View File

@ -144,6 +144,9 @@ abstract class restore_plan_builder {
$plan->add_task($task);
$controller->get_progress()->progress();
// Some activities may have delegated section integrations.
self::build_delegated_section_plan($controller, $infoactivity->moduleid);
// For the given activity path, add as many block tasks as necessary
// TODO: Add blocks, we need to introspect xml here
$blocks = backup_general_helper::get_blocks_from_path($task->get_taskbasepath());
@ -161,6 +164,30 @@ abstract class restore_plan_builder {
}
/**
* Build a course module delegated section backup plan.
* @param restore_controller $controller
* @param int $cmid the parent course module id.
*/
protected static function build_delegated_section_plan($controller, $cmid) {
$info = $controller->get_info();
// Find if some section depends on that course module.
$delegatedsectionid = null;
foreach ($info->sections as $sectionid => $section) {
// Delegated sections are not course responsability.
if (isset($section->parentcmid) && $section->parentcmid == $cmid) {
$delegatedsectionid = $sectionid;
break;
}
}
if (!$delegatedsectionid) {
return;
}
self::build_section_plan($controller, $delegatedsectionid);
}
/**
* Restore one 1-section backup
*/
@ -215,6 +242,10 @@ abstract class restore_plan_builder {
// For the given course, add as many section tasks as necessary
foreach ($info->sections as $sectionid => $section) {
// Delegated sections are not course responsability.
if (isset($section->parentcmid) && !empty($section->parentcmid)) {
continue;
}
self::build_section_plan($controller, $sectionid);
}
}

View File

@ -56,6 +56,30 @@ class restore_section_task extends restore_task {
return $this->get_basepath() . '/sections/section_' . $this->info->sectionid;
}
/**
* Get the course module that is delegating this section.
*
* @return int|null the course module id that is delegating this section
*/
public function get_delegated_cm(): ?int {
if (!isset($this->info->parentcmid) || empty($this->info->parentcmid)) {
return null;
}
return intval($this->info->parentcmid);
}
/**
* Get the delegated activity modname if any.
*
* @return string|null the modname of the delegated activity
*/
public function get_modname(): ?string {
if (!isset($this->info->modname) || empty($this->info->modname)) {
return null;
}
return $this->info->modname;
}
public function set_sectionid($sectionid) {
$this->sectionid = $sectionid;
}
@ -150,36 +174,95 @@ class restore_section_task extends restore_task {
* Define the common setting that any restore section will have
*/
protected function define_settings() {
// All the settings related to this activity will include this prefix
$settingprefix = 'section_' . $this->info->sectionid . '_';
// All these are common settings to be shared by all sections
// All these are common settings to be shared by all sections.
$sectionincluded = $this->add_section_included_setting($settingprefix);
$this->add_section_userinfo_setting($settingprefix, $sectionincluded);
}
// Define section_included (to decide if the whole task must be really executed)
/**
* Add the section included setting to the task.
*
* @param string $settingprefix the identifier of the setting
* @return section_backup_setting the setting added
*/
protected function add_section_included_setting(string $settingprefix): section_backup_setting {
global $DB;
// Define sectionincluded (to decide if the whole task must be really executed).
$settingname = $settingprefix . 'included';
$section_included = new restore_section_included_setting($settingname, base_setting::IS_BOOLEAN, true);
if (is_number($this->info->title)) {
$label = get_string('includesection', 'backup', $this->info->title);
} elseif (empty($this->info->title)) { // Don't throw error if title is empty, gracefully continue restore.
$this->log('Section title missing in backup for section id '.$this->info->sectionid, backup::LOG_WARNING, $this->name);
$label = get_string('unnamedsection', 'backup');
} else {
$label = $this->info->title;
}
$section_included->get_ui()->set_label($label);
$this->add_setting($section_included);
// Define section_userinfo. Dependent of:
// - users root setting
// - section_included setting.
$delegatedcmid = $this->get_delegated_cm();
if ($delegatedcmid) {
$sectionincluded = new restore_subsection_included_setting($settingname, base_setting::IS_BOOLEAN, true);
// Subsections depends on the parent activity included setting.
$settingname = $this->get_modname() . '_' . $delegatedcmid . '_included';
if ($this->plan->setting_exists($settingname)) {
$cmincluded = $this->plan->get_setting($settingname);
$cmincluded->add_dependency(
$sectionincluded,
);
}
$label = get_string('subsectioncontent', 'backup');
} else {
$sectionincluded = new restore_section_included_setting($settingname, base_setting::IS_BOOLEAN, true);
if (is_number($this->info->title)) {
$label = get_string('includesection', 'backup', $this->info->title);
} else if (empty($this->info->title)) { // Don't throw error if title is empty, gracefully continue restore.
$this->log(
'Section title missing in backup for section id ' . $this->info->sectionid,
backup::LOG_WARNING,
$this->name
);
$label = get_string('unnamedsection', 'backup');
} else {
$label = $this->info->title;
}
}
$sectionincluded->get_ui()->set_label($label);
$this->add_setting($sectionincluded);
return $sectionincluded;
}
/**
* Add the section userinfo setting to the task.
*
* @param string $settingprefix the identifier of the setting
* @param section_backup_setting $includefield the section included setting
* @return section_backup_setting the setting added
*/
protected function add_section_userinfo_setting(
string $settingprefix,
section_backup_setting $includefield
): section_backup_setting {
// Define sectionuserinfo. Dependent of:
// - users root setting.
// - sectionincluded setting.
$settingname = $settingprefix . 'userinfo';
$defaultvalue = false;
if (isset($this->info->settings[$settingname]) && $this->info->settings[$settingname]) { // Only enabled when available
$defaultvalue = true;
}
$section_userinfo = new restore_section_userinfo_setting($settingname, base_setting::IS_BOOLEAN, $defaultvalue);
$delegatedcmid = $this->get_delegated_cm();
if ($delegatedcmid) {
$sectionuserinfo = new restore_subsection_userinfo_setting($settingname, base_setting::IS_BOOLEAN, $defaultvalue);
// Subsections depends on the parent activity included setting.
$settingname = $this->get_modname() . '_' . $delegatedcmid . '_userinfo';
if ($this->plan->setting_exists($settingname)) {
$cmincluded = $this->plan->get_setting($settingname);
$cmincluded->add_dependency(
$sectionuserinfo,
);
}
} else {
$sectionuserinfo = new restore_section_userinfo_setting($settingname, base_setting::IS_BOOLEAN, $defaultvalue);
}
if (!$defaultvalue) {
// This is a bit hacky, but if there is no user data to restore, then
// we replace the standard check-box with a select menu with the
@ -189,19 +272,23 @@ class restore_section_task extends restore_task {
// It would probably be better design to have a special UI class
// setting_ui_checkbox_or_no, rather than this hack, but I am not
// going to do that today.
$section_userinfo->set_ui(new backup_setting_ui_select($section_userinfo, get_string('includeuserinfo','backup'),
array(0 => get_string('no'))));
$sectionuserinfo->set_ui(
new backup_setting_ui_select($sectionuserinfo,
get_string('includeuserinfo', 'backup'), [0 => get_string('no')])
);
} else {
$section_userinfo->get_ui()->set_label(get_string('includeuserinfo','backup'));
$sectionuserinfo->get_ui()->set_label(get_string('includeuserinfo', 'backup'));
}
$this->add_setting($section_userinfo);
$this->add_setting($sectionuserinfo);
// Look for "users" root setting.
$users = $this->plan->get_setting('users');
$users->add_dependency($section_userinfo);
$users->add_dependency($sectionuserinfo);
// Look for "section_included" section setting.
$section_included->add_dependency($section_userinfo);
// Look for "section included" section setting.
$includefield->add_dependency($sectionuserinfo);
return $sectionuserinfo;
}
}

View File

@ -222,6 +222,39 @@ class restore_section_included_setting extends restore_section_generic_setting {
*/
class restore_section_userinfo_setting extends restore_section_generic_setting {}
/**
* Subsection base class (delegated section).
*/
class restore_subsection_generic_setting extends restore_section_generic_setting {
/**
* Class constructor.
*
* @param string $name Name of the setting
* @param string $vtype Type of the setting, for example base_setting::IS_TEXT
* @param mixed $value Value of the setting
* @param bool $visibility Is the setting visible in the UI, for example base_setting::VISIBLE
* @param int $status Status of the setting with regards to the locking, for example base_setting::NOT_LOCKED
*/
public function __construct($name, $vtype, $value = null, $visibility = self::VISIBLE, $status = self::NOT_LOCKED) {
parent::__construct($name, $vtype, $value, $visibility, $status);
$this->level = self::SUBSECTION_LEVEL;
}
}
/**
* Setting to define if one subsection is included or no.
*
* Activities _included settings depend of them if available.
*/
class restore_subsection_included_setting extends restore_subsection_generic_setting {
}
/**
* Subsection backup setting to control if section will include
* user information or no, depends of @restore_users_setting.
*/
class restore_subsection_userinfo_setting extends restore_subsection_generic_setting {
}
// Activity backup settings
@ -243,6 +276,41 @@ class restore_activity_included_setting extends restore_activity_generic_setting
*/
class restore_activity_userinfo_setting extends restore_activity_generic_setting {}
/**
* Generic subactivity setting to pass various settings between tasks and steps
*/
class restore_subactivity_generic_setting extends restore_activity_generic_setting {
/**
* Class constructor.
*
* @param string $name Name of the setting
* @param string $vtype Type of the setting, for example base_setting::IS_TEXT
* @param mixed $value Value of the setting
* @param bool $visibility Is the setting visible in the UI, for example base_setting::VISIBLE
* @param int $status Status of the setting with regards to the locking, for example base_setting::NOT_LOCKED
*/
public function __construct($name, $vtype, $value = null, $visibility = self::VISIBLE, $status = self::NOT_LOCKED) {
parent::__construct($name, $vtype, $value, $visibility, $status);
$this->level = self::SUBACTIVITY_LEVEL;
}
}
/**
* Subactivity backup setting to control if activity will be included or no.
*
* Depends of restore_activities_setting and optionally parent section included setting.
*/
class restore_subactivity_included_setting extends restore_subactivity_generic_setting {
}
/**
* Subactivity backup setting to control if activity will include user information.
*
* Depends of restore_users_setting.
*/
class restore_subactivity_userinfo_setting extends restore_subactivity_generic_setting {
}
/**
* root setting to control if restore will create content bank content or no
*/

View File

@ -1612,8 +1612,28 @@ class restore_section_structure_step extends restore_structure_step {
$section->course = $this->get_courseid();
$section->section = $data->number;
$section->timemodified = $data->timemodified ?? 0;
$section->component = null;
$section->itemid = null;
$secrec = $DB->get_record(
'course_sections',
['course' => $this->get_courseid(), 'section' => $data->number, 'component' => null]
);
$createsection = empty($secrec);
// Delegated sections are always restored as new sections.
if (!empty($data->component)) {
$section->itemid = $this->get_delegated_section_mapping($data->component, $data->itemid);
// If the delegate component does not set the mapping id, the section must be converted
// into a regular section. Otherwise, it won't be accessible.
$createsection = $createsection || $section->itemid !== null;
$section->component = ($section->itemid !== null) ? $data->component : null;
// The section number will be always the last of the course, no matter the case.
$section->section = $this->get_last_section_number($this->get_courseid()) + 1;
}
// Section doesn't exist, create it with all the info from backup
if (!$secrec = $DB->get_record('course_sections', ['course' => $this->get_courseid(), 'section' => $data->number])) {
if ($createsection) {
$section->name = $data->name;
$section->summary = $data->summary;
$section->summaryformat = $data->summaryformat;
@ -1629,8 +1649,10 @@ class restore_section_structure_step extends restore_structure_step {
$data, true);
}
}
$section->component = $data->component ?? null;
$section->itemid = $data->itemid ?? null;
// Delegated sections should be always after the normal sections.
$this->displace_delegated_sections_after($section->section);
$newitemid = $DB->insert_record('course_sections', $section);
$section->id = $newitemid;
@ -1802,6 +1824,57 @@ class restore_section_structure_step extends restore_structure_step {
// Add section related files, with 'course_section' itemid to match
$this->add_related_files('course', 'section', 'course_section');
}
/**
* Create a delegate section mapping.
*
* @param string $component the component name (frankenstyle)
* @param int $oldsectionid The old section id.
* @return int|null The new section id or null if not found.
*/
protected function get_delegated_section_mapping($component, $oldsectionid): ?int {
$result = $this->get_mappingid("course_section::$component", $oldsectionid, null);
return $result;
}
/**
* Displace delegated sections after the given section number.
*
* @param int $sectionnum The section number.
*/
protected function displace_delegated_sections_after(int $sectionnum): void {
global $DB;
$sectionstomove = $DB->get_records_select(
'course_sections',
'course = ? AND component IS NOT NULL',
[$this->get_courseid()],
'section DESC', 'id, section'
);
foreach ($sectionstomove as $section) {
$sectionnum++;
$section->section = $sectionnum;
$DB->update_record('course_sections', $section);
}
}
/**
* Get the last section number in the course.
*
* @param int $courseid The course id.
* @param bool $includedelegated If true, include delegated sections in the count.
* @return int The last section number.
*/
protected function get_last_section_number(int $courseid, bool $includedelegated = false): int {
global $DB;
$delegtadefilter = $includedelegated ? '' : ' AND component IS NULL';
return (int) $DB->get_field_sql(
'SELECT max(section) from {course_sections} WHERE course = ?' . $delegtadefilter,
[$courseid]
);
}
}
/**
@ -4905,6 +4978,17 @@ abstract class restore_activity_structure_step extends restore_structure_step {
$oldid = $this->task->get_old_activityid();
$this->set_mapping($modulename, $oldid, $newitemid, true);
}
/**
* Create a delegate section mapping.
*
* @param string $component The component name (frankenstyle)
* @param int $olditemid The old section id.
* @param int $newitemid The new section id.
*/
protected function set_delegated_section_mapping($component, $olditemid, $newitemid) {
$this->set_mapping("course_section::$component", $olditemid, $newitemid);
}
}
/**

View File

@ -103,7 +103,7 @@ class restore_stepslib_test extends \advanced_testcase {
}
/**
* Test for the section structure step included elements.
* Test for delegate section behaviour.
*
* @covers \restore_section_structure_step::process_section
*/
@ -114,7 +114,7 @@ class restore_stepslib_test extends \advanced_testcase {
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course(['numsections' => 2, 'format' => 'topics']);
// Section 2 has an existing delegate class.
// Section 2 has an existing delegate class for component that is not an activity.
course_update_section(
$course,
$DB->get_record('course_sections', ['course' => $course->id, 'section' => 2]),
@ -130,16 +130,18 @@ class restore_stepslib_test extends \advanced_testcase {
$originalsections = get_fast_modinfo($course->id)->get_section_info_all();
$restoredsections = get_fast_modinfo($newcourseid)->get_section_info_all();
$this->assertEquals(count($originalsections), count($restoredsections));
// Delegated sections depends on the plugin to be backuped and restored.
// In this case, the plugin is not backuped and restored, so the section is not restored.
$this->assertEquals(3, count($originalsections));
$this->assertEquals(2, count($restoredsections));
$validatefields = ['name', 'summary', 'summaryformat', 'visible', 'component', 'itemid'];
$this->assertEquals($originalsections[1]->name, $restoredsections[1]->name);
foreach ($validatefields as $field) {
$this->assertEquals($originalsections[0]->$field, $restoredsections[0]->$field);
$this->assertEquals($originalsections[1]->$field, $restoredsections[1]->$field);
$this->assertEquals($originalsections[2]->$field, $restoredsections[2]->$field);
}
}
}

View File

@ -234,7 +234,9 @@ abstract class backup_controller_dbops extends backup_dbops {
'sectionid' => $task->get_sectionid(),
'modulename' => $task->get_modulename(),
'title' => $task->get_name(),
'directory' => 'activities/' . $task->get_modulename() . '_' . $task->get_moduleid());
'directory' => 'activities/' . $task->get_modulename() . '_' . $task->get_moduleid(),
'insubsection' => ($task->is_in_subsection()) ? 1 : '',
);
// Now get activity settings
// Calculate prefix to find valid settings
@ -246,7 +248,7 @@ abstract class backup_controller_dbops extends backup_dbops {
continue;
}
// Validate level is correct (activity)
if ($setting->get_level() != backup_setting::ACTIVITY_LEVEL) {
if (!in_array($setting->get_level(), [backup_setting::ACTIVITY_LEVEL, backup_setting::SUBACTIVITY_LEVEL])) {
throw new backup_controller_exception('setting_not_activity_level', $setting);
}
$settinginfo = array(
@ -268,7 +270,10 @@ abstract class backup_controller_dbops extends backup_dbops {
$contentinfo = array(
'sectionid' => $task->get_sectionid(),
'title' => $task->get_name(),
'directory' => 'sections/' . 'section_' . $task->get_sectionid());
'directory' => 'sections/' . 'section_' . $task->get_sectionid(),
'parentcmid' => $task->get_delegated_cm() ?? '',
'modname' => $task->get_modname() ?? '',
);
// Now get section settings
// Calculate prefix to find valid settings
@ -280,7 +285,7 @@ abstract class backup_controller_dbops extends backup_dbops {
continue;
}
// Validate level is correct (section)
if ($setting->get_level() != backup_setting::SECTION_LEVEL) {
if (!in_array($setting->get_level(), [backup_setting::SECTION_LEVEL, backup_setting::SUBSECTION_LEVEL])) {
throw new backup_controller_exception('setting_not_section_level', $setting);
}
$settinginfo = array(

View File

@ -120,6 +120,17 @@ abstract class backup_plan_dbops extends backup_dbops {
return $sectionsarr;
}
/**
* Given one section id, returns the full section record.
*
* @param int $sectionid
* @return stdClass
*/
public static function get_section_from_id($sectionid): stdClass {
global $DB;
return $DB->get_record('course_sections', ['id' => $sectionid]);
}
/**
* Given one course id, return its format in DB
*/

View File

@ -103,7 +103,20 @@ abstract class base_task implements checksumable, executable, loggable {
}
}
/**
* Check if a setting with the given name exists.
*
* This method find first in the current settings and then in the plan settings.
*
* @param string $name Setting name
* @return bool
*/
public function setting_exists($name) {
foreach ($this->settings as $setting) {
if ($setting->get_name() == $name) {
return true;
}
}
return $this->plan->setting_exists($name);
}

View File

@ -37,6 +37,12 @@ abstract class backup_setting extends base_setting implements checksumable {
const SECTION_LEVEL = 9;
const ACTIVITY_LEVEL = 13;
/** @var int the subsection level. */
const SUBSECTION_LEVEL = 17;
/** @var int the activity inside a subsection level. */
const SUBACTIVITY_LEVEL = 21;
/** @var int Level of the setting, eg {@link self::ROOT_LEVEL} */
protected $level;
@ -96,7 +102,12 @@ abstract class backup_setting extends base_setting implements checksumable {
}
// Check the dependency level is >= current level
if ($dependentsetting->get_level() < $this->level) {
throw new backup_setting_exception('cannot_add_upper_level_dependency');
throw new backup_setting_exception('cannot_add_upper_level_dependency', [
$dependentsetting->get_level(),
$dependentsetting->get_name(),
$this->level,
$this->get_name(),
]);
}
parent::add_dependency($dependentsetting, $type, $options);
}

View File

@ -256,9 +256,11 @@ abstract class backup_setting_ui extends base_setting_ui {
$this->name = 'course_'.$setting->get_name();
break;
case backup_setting::SECTION_LEVEL :
case backup_setting::SUBSECTION_LEVEL:
$this->name = 'section_'.$setting->get_name();
break;
case backup_setting::ACTIVITY_LEVEL :
case backup_setting::SUBACTIVITY_LEVEL:
$this->name = 'activity_'.$setting->get_name();
break;
}
@ -329,9 +331,10 @@ abstract class backup_setting_ui extends base_setting_ui {
// If a task has been provided and the label is not already set meaningfully
// we will attempt to improve it.
if (!is_null($task) && $this->label == $this->setting->get_name() && strpos($this->setting->get_name(), '_include') !== false) {
if ($this->setting->get_level() == backup_setting::SECTION_LEVEL) {
$level = $this->setting->get_level();
if ($level == backup_setting::SECTION_LEVEL || $level == backup_setting::SUBSECTION_LEVEL) {
$this->label = get_string('includesection', 'backup', $task->get_name());
} else if ($this->setting->get_level() == backup_setting::ACTIVITY_LEVEL) {
} else if ($level == backup_setting::ACTIVITY_LEVEL || $level == backup_setting::SUBACTIVITY_LEVEL) {
$this->label = $task->get_name();
}
}

View File

@ -48,22 +48,10 @@ abstract class base_moodleform extends moodleform {
protected $uistage = null;
/**
* True if we have a course div open, false otherwise
* @var bool
* Group stack to control open and closed div groups.
* @var array
*/
protected $coursediv = false;
/**
* True if we have a section div open, false otherwise
* @var bool
*/
protected $sectiondiv = false;
/**
* True if we have an activity div open, false otherwise
* @var bool
*/
protected $activitydiv = false;
protected array $groupstack = [];
/**
* Creates the form
@ -177,20 +165,12 @@ abstract class base_moodleform extends moodleform {
}
/**
* Closes any open divs
* Closes any open divs.
*/
public function close_task_divs() {
if ($this->activitydiv) {
while (!empty($this->groupstack)) {
$this->_form->addElement('html', html_writer::end_tag('div'));
$this->activitydiv = false;
}
if ($this->sectiondiv) {
$this->_form->addElement('html', html_writer::end_tag('div'));
$this->sectiondiv = false;
}
if ($this->coursediv) {
$this->_form->addElement('html', html_writer::end_tag('div'));
$this->coursediv = false;
array_pop($this->groupstack);
}
}
@ -244,7 +224,7 @@ abstract class base_moodleform extends moodleform {
list($identifier, $component) = $setting->get_help();
$this->_form->addHelpButton($setting->get_ui_name(), $identifier, $component);
}
$this->_form->addElement('html', html_writer::end_tag('div'));
$this->pop_group();
}
$this->_form->setDefaults($defaults);
return true;
@ -265,54 +245,42 @@ abstract class base_moodleform extends moodleform {
* @param backup_setting $setting
*/
protected function add_html_formatting(backup_setting $setting) {
$mform = $this->_form;
$isincludesetting = (strpos($setting->get_name(), '_include') !== false);
if ($isincludesetting && $setting->get_level() != backup_setting::ROOT_LEVEL) {
switch ($setting->get_level()) {
case backup_setting::COURSE_LEVEL:
if ($this->activitydiv) {
$this->_form->addElement('html', html_writer::end_tag('div'));
$this->activitydiv = false;
}
if ($this->sectiondiv) {
$this->_form->addElement('html', html_writer::end_tag('div'));
$this->sectiondiv = false;
}
if ($this->coursediv) {
$this->_form->addElement('html', html_writer::end_tag('div'));
}
$mform->addElement('html', html_writer::start_tag('div', array('class' => 'grouped_settings course_level')));
$mform->addElement('html', html_writer::start_tag('div', array('class' => 'include_setting course_level')));
$this->coursediv = true;
$this->pop_groups_to('course');
$this->push_group_start('course', 'grouped_settings course_level');
$this->push_group_start(null, 'include_setting course_level');
break;
case backup_setting::SECTION_LEVEL:
if ($this->activitydiv) {
$this->_form->addElement('html', html_writer::end_tag('div'));
$this->activitydiv = false;
}
if ($this->sectiondiv) {
$this->_form->addElement('html', html_writer::end_tag('div'));
}
$mform->addElement('html', html_writer::start_tag('div', array('class' => 'grouped_settings section_level')));
$mform->addElement('html', html_writer::start_tag('div', array('class' => 'include_setting section_level')));
$this->sectiondiv = true;
$this->pop_groups_to('course');
$this->push_group_start('section', 'grouped_settings section_level');
$this->push_group_start(null, 'include_setting section_level');
break;
case backup_setting::ACTIVITY_LEVEL:
if ($this->activitydiv) {
$this->_form->addElement('html', html_writer::end_tag('div'));
}
$mform->addElement('html', html_writer::start_tag('div', array('class' => 'grouped_settings activity_level')));
$mform->addElement('html', html_writer::start_tag('div', array('class' => 'include_setting activity_level')));
$this->activitydiv = true;
$this->pop_groups_to('section');
$this->push_group_start('activity', 'grouped_settings activity_level');
$this->push_group_start(null, 'include_setting activity_level');
break;
case backup_setting::SUBSECTION_LEVEL:
$this->pop_groups_to('section');
$this->push_group_start('subsection', 'grouped_settings subsection_level');
$this->push_group_start(null, 'normal_setting');
break;
case backup_setting::SUBACTIVITY_LEVEL:
$this->pop_groups_to('subsection');
$this->push_group_start('subactivity', 'grouped_settings activity_level');
$this->push_group_start(null, 'include_setting activity_level');
break;
default:
$mform->addElement('html', html_writer::start_tag('div', array('class' => 'normal_setting')));
$this->push_group_start(null, 'normal_setting');
break;
}
} else if ($setting->get_level() == backup_setting::ROOT_LEVEL) {
$mform->addElement('html', html_writer::start_tag('div', array('class' => 'root_setting')));
$this->push_group_start('root', 'root_setting');
} else {
$mform->addElement('html', html_writer::start_tag('div', array('class' => 'normal_setting')));
$this->push_group_start(null, 'normal_setting');
}
}
@ -350,12 +318,55 @@ abstract class base_moodleform extends moodleform {
$label .= $OUTPUT->render($labelicon);
}
$this->_form->addElement('static', 'static_'.$settingui->get_name(), $label, $settingui->get_static_value().$icon);
$this->_form->addElement('html', html_writer::end_tag('div'));
$this->pop_group();
}
$this->_form->addElement('hidden', $settingui->get_name(), $settingui->get_value());
$this->_form->setType($settingui->get_name(), $settingui->get_param_validation());
}
/**
* Pushes a group start to the form.
*
* This method will create a new group div in the form and add it to the group stack.
* The name can be used to close all stacked groups up to a certain group.
*
* @param string|null $name The name of the group, if any.
* @param string $classes The classes to add to the div.
*/
protected function push_group_start(?string $name, string $classes) {
$mform = $this->_form;
$this->groupstack[] = $name;
$mform->addElement('html', html_writer::start_tag('div', ['class' => $classes]));
}
/**
* Pops groups from the stack until the given group name is reached.
*
* @param string $name The name of the group to pop to.
*/
protected function pop_groups_to(string $name) {
if (empty($this->groupstack)) {
return;
}
while (!empty($this->groupstack) && end($this->groupstack) !== $name) {
$this->pop_group();
}
}
/**
* Pops a group from the stack and closes the div.
*
* @return string|null The name of the group that was popped, or null if the stack is empty.
*/
protected function pop_group(): ?string {
if (empty($this->groupstack)) {
return null;
}
$mform = $this->_form;
$mform->addElement('html', html_writer::end_tag('div'));
return array_pop($this->groupstack);
}
/**
* Adds dependencies to the form recursively
*

View File

@ -52,6 +52,29 @@ abstract class sectiondelegatemodule extends sectiondelegate {
);
}
/**
* Get the delegated section id controlled by a specific cm.
*
* This method is used when reverse search is needed bu we cannot access the database.
* This happens mostly on backup and restore. Do NOT use for normal operations.
*
* @param stdClass|cm_info $cm a course module compatible data structure.
* @return int the section id.
*/
public static function delegated_section_id(stdClass|cm_info $cm): int {
global $DB;
return $DB->get_field(
'course_sections',
'id',
[
'course' => $cm->course,
'component' => explode('\\', static::class)[0],
'itemid' => $cm->instance,
],
MUST_EXIST
);
}
/**
* Get the parent section of the current delegated section.
*

View File

@ -198,6 +198,7 @@ $string['enableasyncbackup_help'] = 'If enabled, backup and restore operations w
$string['enterasearch'] = 'Enter a search';
$string['error_block_for_module_not_found'] = 'Orphan block instance (id: {$a->bid}) for course module (id: {$a->mid}) found. This block will not be backed up';
$string['error_course_module_not_found'] = 'Orphan course module (id: {$a}) found. This module will not be backed up.';
$string['error_delegate_section_not_found'] = 'Missing delegate section form course module (id: {$a}. The section will not be backed up.';
$string['errorcopyingbackupfile'] = "Failed to copy the backup file to the temporary folder before restoring.";
$string['errorfilenamerequired'] = 'You must enter a valid filename for this backup';
$string['errorfilenametoolong'] = 'The filename must be less than 255 characters in length.';
@ -416,6 +417,7 @@ $string['skipmodifdayshelp'] = 'Choose to skip courses that have not been modifi
$string['skipmodifprev'] = 'Skip courses not modified since previous backup';
$string['skipmodifprevhelp'] = 'Choose whether to skip courses that have not been modified since the last automatic backup. This requires logging to be enabled.';
$string['status'] = 'Status';
$string['subsectioncontent'] = 'Subsection content';
$string['successful'] = 'Backup successful';
$string['successfulcopy'] = 'Copy successful';
$string['successfulrestore'] = 'Restore successful';

View File

@ -12,6 +12,15 @@
padding: $card-spacer-x;
margin-bottom: $card-spacer-x;
}
&.subsection_level {
background-color: $card-bg;
@include border-radius($card-border-radius);
border: $card-border-width solid $card-border-color;
@include clearfix;
padding: $card-spacer-x;
margin: 0 $card-spacer-x $card-spacer-x $card-spacer-x;
}
}
/* These are long labels with checkboxes on the right. */
@ -202,4 +211,3 @@
background: $backup-restore-state12-bg;
}
}

View File

@ -34997,6 +34997,18 @@ img.userpicture {
clear: both;
content: "";
}
.path-backup .mform .grouped_settings.subsection_level {
background-color: #fff;
border-radius: 0.5rem;
border: 1px solid rgba(0, 0, 0, 0.125);
padding: 1.25rem;
margin: 0 1.25rem 1.25rem 1.25rem;
}
.path-backup .mform .grouped_settings.subsection_level::after {
display: block;
clear: both;
content: "";
}
.path-backup .mform .include_setting {
width: 50%;
display: inline-block;

View File

@ -34997,6 +34997,18 @@ img.userpicture {
clear: both;
content: "";
}
.path-backup .mform .grouped_settings.subsection_level {
background-color: #fff;
border-radius: 0.25rem;
border: 1px solid rgba(0, 0, 0, 0.125);
padding: 1.25rem;
margin: 0 1.25rem 1.25rem 1.25rem;
}
.path-backup .mform .grouped_settings.subsection_level::after {
display: block;
clear: both;
content: "";
}
.path-backup .mform .include_setting {
width: 50%;
display: inline-block;