MDL-65478 backup, course format: Handle editor elements in course format
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 <[email protected]> Co-authored-by: Matthew Hilton <[email protected]>
This commit is contained in:
committed by
Matthew Hilton
co-authored by
Matthew Hilton
parent
e36fb751e5
commit
43cbc05a2d
@@ -475,6 +475,11 @@ class backup_course_structure_step extends backup_structure_step {
|
||||
'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);
|
||||
|
||||
@@ -512,17 +517,14 @@ class backup_course_structure_step extends backup_structure_step {
|
||||
$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.
|
||||
@@ -544,6 +546,10 @@ class backup_course_structure_step extends backup_structure_step {
|
||||
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);
|
||||
|
||||
@@ -1806,7 +1806,8 @@ class restore_course_structure_step extends restore_structure_step {
|
||||
$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);
|
||||
@@ -1829,7 +1830,7 @@ class restore_course_structure_step extends restore_structure_step {
|
||||
// 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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1951,8 +1952,6 @@ class restore_course_structure_step extends restore_structure_step {
|
||||
// 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());
|
||||
}
|
||||
@@ -1980,6 +1979,26 @@ class restore_course_structure_step extends restore_structure_step {
|
||||
$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;
|
||||
|
||||
|
||||
@@ -908,15 +908,12 @@ abstract class base {
|
||||
'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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1011,7 +1008,7 @@ abstract class base {
|
||||
$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]);
|
||||
@@ -1060,6 +1057,7 @@ abstract class base {
|
||||
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',
|
||||
|
||||
@@ -135,3 +135,67 @@ class format_site extends course_format {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,35 @@ class base_test extends advanced_testcase {
|
||||
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();
|
||||
|
||||
+9
-1
@@ -37,6 +37,10 @@ class format_theunittest extends core_courseformat\base {
|
||||
'default' => 0,
|
||||
'type' => PARAM_INT,
|
||||
),
|
||||
'summary_editor' => array(
|
||||
'default' => '',
|
||||
'type' => PARAM_RAW,
|
||||
),
|
||||
);
|
||||
}
|
||||
if ($foreditform && !isset($courseformatoptions['hideoddsections']['label'])) {
|
||||
@@ -51,6 +55,10 @@ class format_theunittest extends core_courseformat\base {
|
||||
'element_type' => 'select',
|
||||
'element_attributes' => array($sectionmenu),
|
||||
),
|
||||
'summary_editor' => array(
|
||||
'label' => 'Summary Text',
|
||||
'element_type' => 'editor',
|
||||
),
|
||||
);
|
||||
$courseformatoptions = array_merge_recursive($courseformatoptions, $courseformatoptionsedit);
|
||||
}
|
||||
@@ -74,4 +82,4 @@ class format_theunittest extends core_courseformat\base {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ global $CFG;
|
||||
|
||||
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.
|
||||
@@ -534,4 +535,49 @@ class restore_test extends \advanced_testcase {
|
||||
$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,0 +1,105 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace core_course;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
global $CFG;
|
||||
require_once($CFG->dirroot . '/course/lib.php');
|
||||
require_once($CFG->dirroot . '/course/format/tests/fixtures/format_theunittest.php');
|
||||
require_once($CFG->dirroot . '/course/format/lib.php');
|
||||
|
||||
/**
|
||||
* Course format function unit tests
|
||||
*
|
||||
* @package core_course
|
||||
* @copyright 2021 Catalyst IT Pty Ltd
|
||||
* @author Jason den Dulk
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class course_format_function_test extends \basic_testcase {
|
||||
|
||||
/**
|
||||
* Tests clean_param_if_not_null function
|
||||
* @covers ::clean_param_if_not_null
|
||||
*/
|
||||
public function test_clean_param_if_not_null() {
|
||||
$this->assertNull(clean_param_if_not_null(null));
|
||||
$n = '3x';
|
||||
$this->assertEquals(clean_param($n, PARAM_INT), clean_param_if_not_null($n, PARAM_INT));
|
||||
$this->assertEquals(clean_param($n, PARAM_RAW), clean_param_if_not_null($n, PARAM_RAW));
|
||||
$this->assertEquals(clean_param($n, PARAM_ALPHANUM), clean_param_if_not_null($n, PARAM_ALPHANUM));
|
||||
$this->assertEquals(clean_param($n, PARAM_ALPHA), clean_param_if_not_null($n, PARAM_ALPHA));
|
||||
$s = '<abc>xyz</abc>';
|
||||
$this->assertEquals(clean_param($s, PARAM_ALPHANUM), clean_param_if_not_null($s, PARAM_ALPHANUM));
|
||||
$this->assertEquals(clean_param($s, PARAM_RAW), clean_param_if_not_null($s, PARAM_RAW));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests contract_value function
|
||||
* @covers ::contract_value
|
||||
*/
|
||||
public function test_contract_value() {
|
||||
$input = [
|
||||
'abc' => '<p>All together Now</p>',
|
||||
'abcformat' => '1',
|
||||
'jolly' => 'Roger'
|
||||
];
|
||||
$expected = [
|
||||
'abc_editor' => [ 'text' => $input['abc'], 'format' => $input['abcformat'] ],
|
||||
'jolly' => $input['jolly'],
|
||||
];
|
||||
$defs = [
|
||||
'abc_editor' => [],
|
||||
'jolly' => [ 'type' => PARAM_ALPHA ],
|
||||
];
|
||||
$dest = [];
|
||||
|
||||
foreach ($defs as $name => $def) {
|
||||
contract_value($dest, $input, $def, $name);
|
||||
}
|
||||
|
||||
$this->assertEquals($expected, $dest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests expand_value function
|
||||
* @covers ::expand_value
|
||||
*/
|
||||
public function test_expand_value() {
|
||||
$input = [
|
||||
'abc_editor' => [ 'text' => '<p>All together Now</p>', 'format' => '1' ],
|
||||
'jolly' => 'Roger',
|
||||
];
|
||||
$expected = [
|
||||
'abc' => $input['abc_editor']['text'],
|
||||
'abcformat' => $input['abc_editor']['format'],
|
||||
'jolly' => $input['jolly'],
|
||||
];
|
||||
$defs = [
|
||||
'abc_editor' => [],
|
||||
'jolly' => [ 'type' => PARAM_ALPHA ],
|
||||
];
|
||||
$dest = [];
|
||||
|
||||
foreach ($defs as $name => $def) {
|
||||
expand_value($dest, $input, $def, $name);
|
||||
}
|
||||
|
||||
$this->assertEquals($expected, $dest);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user