Skip to content

Commit

Permalink
MDL-65478 backup, course format: Handle editor elements in course format
Browse files Browse the repository at this point in the history
Modified course format options reading and writing to be able to handle Editor elements by enabling them to split array values into
multiple values before inserting into database, and combining multiple values into an array when reading from the database.
Modified backup and restore code to use backup_nested_elements, and to interact directly with the database.

Co-authored-by: Jason den Dulk <jasondendulk@catalyst-au.net>
Co-authored-by: Matthew Hilton <matthewhilton@catalyst-au.net>
  • Loading branch information
jaypha and matthewhilton committed May 3, 2022
1 parent e36fb75 commit 43cbc05
Show file tree
Hide file tree
Showing 8 changed files with 295 additions and 20 deletions.
18 changes: 12 additions & 6 deletions backup/moodle2/backup_stepslib.php
Expand Up @@ -475,6 +475,11 @@ protected function define_structure() {
'shortname', 'type', 'value', 'valueformat'
));

$courseformatoptions = new backup_nested_element('courseformatoptions');
$courseformatoption = new backup_nested_element('courseformatoption', [], [
'courseid', 'format', 'sectionid', 'name', 'value'
]);

// attach format plugin structure to $course element, only one allowed
$this->add_plugin_structure('format', $course, false);

Expand Down Expand Up @@ -512,17 +517,14 @@ protected function define_structure() {
$course->add_child($customfields);
$customfields->add_child($customfield);

$course->add_child($courseformatoptions);
$courseformatoptions->add_child($courseformatoption);

// Set the sources

$courserec = $DB->get_record('course', array('id' => $this->task->get_courseid()));
$courserec->contextid = $this->task->get_contextid();

$formatoptions = course_get_format($courserec)->get_format_options();
$course->add_final_elements(array_keys($formatoptions));
foreach ($formatoptions as $key => $value) {
$courserec->$key = $value;
}

// Add 'numsections' in order to be able to restore in previous versions of Moodle.
// Even though Moodle does not officially support restore into older verions of Moodle from the
// version where backup was made, without 'numsections' restoring will go very wrong.
Expand All @@ -544,6 +546,10 @@ protected function define_structure() {
backup_helper::is_sqlparam('course'),
backup::VAR_PARENTID));

$courseformatoption->set_source_sql('SELECT id, format, sectionid, name, value
FROM {course_format_options}
WHERE courseid = ?', [ backup::VAR_PARENTID ]);

$handler = core_course\customfield\course_handler::create();
$fieldsforbackup = $handler->get_instance_data_for_backup($this->task->get_courseid());
$customfield->set_source_array($fieldsforbackup);
Expand Down
27 changes: 23 additions & 4 deletions backup/moodle2/restore_stepslib.php
Expand Up @@ -1806,7 +1806,8 @@ protected function define_structure() {
$category = new restore_path_element('category', '/course/category');
$tag = new restore_path_element('tag', '/course/tags/tag');
$customfield = new restore_path_element('customfield', '/course/customfields/customfield');
$allowed_module = new restore_path_element('allowed_module', '/course/allowed_modules/module');
$courseformatoptions = new restore_path_element('course_format_option', '/course/courseformatoptions/courseformatoption');
$allowedmodule = new restore_path_element('allowed_module', '/course/allowed_modules/module');

// Apply for 'format' plugins optional paths at course level
$this->add_plugin_structure('format', $course);
Expand All @@ -1829,7 +1830,7 @@ protected function define_structure() {
// Apply for admin tool plugins optional paths at course level.
$this->add_plugin_structure('tool', $course);

return array($course, $category, $tag, $customfield, $allowed_module);
return array($course, $category, $tag, $customfield, $allowedmodule, $courseformatoptions);
}

/**
Expand Down Expand Up @@ -1951,8 +1952,6 @@ public function process_course($data) {
// Course record ready, update it
$DB->update_record('course', $data);

course_get_format($data)->update_course_format_options($data);

// Role name aliases
restore_dbops::set_course_role_names($this->get_restoreid(), $this->get_courseid());
}
Expand Down Expand Up @@ -1980,6 +1979,26 @@ public function process_customfield($data) {
$handler->restore_instance_data_from_backup($this->task, $data);
}

/**
* Processes a course format option.
*
* @param array $data The record being restored.
* @throws base_step_exception
* @throws dml_exception
*/
public function process_course_format_option(array $data) : void {
global $DB;

$courseid = $this->get_courseid();
$record = $DB->get_record('course_format_options', [ 'courseid' => $courseid, 'name' => $data['name'] ], 'id');
if ($record !== false) {
$DB->update_record('course_format_options', (object) [ 'id' => $record->id, 'value' => $data['value'] ]);
} else {
$data['courseid'] = $courseid;
$DB->insert_record('course_format_options', (object) $data);
}
}

public function process_allowed_module($data) {
$data = (object)$data;

Expand Down
16 changes: 7 additions & 9 deletions course/format/classes/base.php
Expand Up @@ -908,15 +908,12 @@ public function get_format_options($section = null) {
'format' => $this->format,
'sectionid' => $sectionid
), '', 'id,name,value');
$indexedrecords = [];
foreach ($records as $record) {
if (array_key_exists($record->name, $this->formatoptions[$sectionid])) {
$value = $record->value;
if ($value !== null && isset($options[$record->name]['type'])) {
// This will convert string value to number if needed.
$value = clean_param($value, $options[$record->name]['type']);
}
$this->formatoptions[$sectionid][$record->name] = $value;
}
$indexedrecords[$record->name] = $record->value;
}
foreach ($options as $optionname => $option) {
contract_value($this->formatoptions[$sectionid], $indexedrecords, $option, $optionname);
}
}
}
Expand Down Expand Up @@ -1011,7 +1008,7 @@ protected function validate_format_options(array $rawdata, int $sectionid = null
$data = array_intersect_key($rawdata, $allformatoptions);
foreach ($data as $key => $value) {
$option = $allformatoptions[$key] + ['type' => PARAM_RAW, 'element_type' => null, 'element_attributes' => [[]]];
$data[$key] = clean_param($value, $option['type']);
expand_value($data, $data, $option, $key);
if ($option['element_type'] === 'select' && !array_key_exists($data[$key], $option['element_attributes'][0])) {
// Value invalid for select element, skip.
unset($data[$key]);
Expand Down Expand Up @@ -1060,6 +1057,7 @@ protected function update_format_options($data, $sectionid = null) {
if (array_key_exists('default', $option)) {
$defaultoptions[$key] = $option['default'];
}
expand_value($defaultoptions, $defaultoptions, $option, $key);
$cached[$key] = ($sectionid === 0 || !empty($option['cache']));
}
$records = $DB->get_records('course_format_options',
Expand Down
64 changes: 64 additions & 0 deletions course/format/lib.php
Expand Up @@ -135,3 +135,67 @@ public function get_section_number(): int {
return 1;
}
}

/**
* 'Converts' a value from what is stored in the database into what is used by edit forms.
*
* @param array $dest The destination array
* @param array $source The source array
* @param array $option The definition structure of the option.
* @param string $optionname The name of the option, as provided in the definition.
*/
function contract_value(array &$dest, array $source, array $option, string $optionname) : void {
if (substr($optionname, -7) == '_editor') { // Suffix '_editor' indicates that the element is an editor.
$name = substr($optionname, 0, -7);
if (isset($source[$name])) {
$dest[$optionname] = [
'text' => clean_param_if_not_null($source[$name], $option['type'] ?? PARAM_RAW),
'format' => clean_param_if_not_null($source[$name . 'format'], PARAM_INT),
];
}
} else {
if (isset($source[$optionname])) {
$dest[$optionname] = clean_param_if_not_null($source[$optionname], $option['type'] ?? PARAM_RAW);
}
}
}

/**
* Cleans the given param, unless it is null.
*
* @param mixed $param The variable we are cleaning.
* @param string $type Expected format of param after cleaning.
* @return mixed Null if $param is null, otherwise the cleaned value.
* @throws coding_exception
*/
function clean_param_if_not_null($param, string $type = PARAM_RAW) {
if ($param === null) {
return null;
} else {
return clean_param($param, $type);
}
}

/**
* 'Converts' a value from what is used in edit forms into a value(s) to be stored in the database.
*
* @param array $dest The destination array
* @param array $source The source array
* @param array $option The definition structure of the option.
* @param string $optionname The name of the option, as provided in the definition.
*/
function expand_value(array &$dest, array $source, array $option, string $optionname) : void {
if (substr($optionname, -7) == '_editor') { // Suffix '_editor' indicates that the element is an editor.
$name = substr($optionname, 0, -7);
if (is_string($source[$optionname])) {
$dest[$name] = clean_param($source[$optionname], $option['type'] ?? PARAM_RAW);
$dest[$name . 'format'] = 1;
} else {
$dest[$name] = clean_param($source[$optionname]['text'], $option['type'] ?? PARAM_RAW);
$dest[$name . 'format'] = clean_param($source[$optionname]['format'], PARAM_INT);
}
unset($dest[$optionname]);
} else {
$dest[$optionname] = clean_param($source[$optionname], $option['type'] ?? PARAM_RAW);
}
}
29 changes: 29 additions & 0 deletions course/format/tests/base_test.php
Expand Up @@ -36,6 +36,35 @@ public static function setupBeforeClass(): void {
require_once($CFG->dirroot . '/course/format/tests/fixtures/format_theunittest_output_course_format_invalidoutput.php');
}

/**
* Tests the save and load functionality.
*
* @author Jason den Dulk
* @covers \core_courseformat
*/
public function test_courseformat_saveandload() {
$this->resetAfterTest();

$courseformatoptiondata = (object) [
"hideoddsections" => 1,
'summary_editor' => [
'text' => '<p>Somewhere over the rainbow</p><p>The <b>quick</b> brown fox jumpos over the lazy dog.</p>',
'format' => 1
]
];
$generator = $this->getDataGenerator();
$course1 = $generator->create_course(array('format' => 'theunittest'));
$this->assertEquals('theunittest', $course1->format);
course_create_sections_if_missing($course1, array(0, 1));

$courseformat = course_get_format($course1);
$courseformat->update_course_format_options($courseformatoptiondata);

$savedcourseformatoptiondata = $courseformat->get_format_options();

$this->assertEqualsCanonicalizing($courseformatoptiondata, (object) $savedcourseformatoptiondata);
}

public function test_available_hook() {
global $DB;
$this->resetAfterTest();
Expand Down
10 changes: 9 additions & 1 deletion course/format/tests/fixtures/format_theunittest.php
Expand Up @@ -37,6 +37,10 @@ public function course_format_options($foreditform = false) {
'default' => 0,
'type' => PARAM_INT,
),
'summary_editor' => array(
'default' => '',
'type' => PARAM_RAW,
),
);
}
if ($foreditform && !isset($courseformatoptions['hideoddsections']['label'])) {
Expand All @@ -51,6 +55,10 @@ public function course_format_options($foreditform = false) {
'element_type' => 'select',
'element_attributes' => array($sectionmenu),
),
'summary_editor' => array(
'label' => 'Summary Text',
'element_type' => 'editor',
),
);
$courseformatoptions = array_merge_recursive($courseformatoptions, $courseformatoptionsedit);
}
Expand All @@ -74,4 +82,4 @@ public function section_get_available_hook(section_info $section, &$available, &
}
}
}
}
}
46 changes: 46 additions & 0 deletions course/tests/backup/restore_test.php
Expand Up @@ -26,6 +26,7 @@

require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
require_once($CFG->dirroot . '/course/format/tests/fixtures/format_theunittest.php');

/**
* Course restore testcase.
Expand Down Expand Up @@ -534,4 +535,49 @@ public function test_restore_course_startdate_in_existing_course_without_permiss
$this->assertEquals($chat2->chattime, $restoredchat2->chattime);
$this->assertEquals($c2->startdate + 1 * WEEKSECS, $restoredchat2->chattime);
}

/**
* Tests course restore with editor in course format.
*
* @author Matthew Hilton
* @covers \core_courseformat
*/
public function test_restore_editor_courseformat() {
$this->resetAfterTest();

// Setup user with restore permissions.
$dg = $this->getDataGenerator();
$u1 = $dg->create_user();

$managers = get_archetype_roles('manager');
$manager = array_shift($managers);
$dg->role_assign($manager->id, $u1->id);

// Create a course with an editor item in the course format.
$courseformatoptiondata = (object) [
"hideoddsections" => 1,
'summary_editor' => [
'text' => '<p>Somewhere over the rainbow</p><p>The <b>quick</b> brown fox jumpos over the lazy dog.</p>',
'format' => 1
]
];
$course1 = $dg->create_course(['format' => 'theunittest']);
$course2 = $dg->create_course(['format' => 'theunittest']);
$this->assertEquals('theunittest', $course1->format);
course_create_sections_if_missing($course1, array(0, 1));

// Set the course format.
$courseformat = course_get_format($course1);
$courseformat->update_course_format_options($courseformatoptiondata);

// Backup and restore the course.
$backupid = $this->backup_course($course1->id);
$this->restore_to_existing_course($backupid, $course2->id, $u1->id);

// Get the restored course format.
$restoredformat = course_get_format($course2);
$restoredformatoptions = $restoredformat->get_format_options();

$this->assertEqualsCanonicalizing($courseformatoptiondata, (object) $restoredformatoptions);
}
}

0 comments on commit 43cbc05

Please sign in to comment.