diff --git a/mod/data/classes/form/save_as_preset.php b/mod/data/classes/form/save_as_preset.php index 968718eeb66..76e09613836 100644 --- a/mod/data/classes/form/save_as_preset.php +++ b/mod/data/classes/form/save_as_preset.php @@ -21,6 +21,7 @@ use moodle_exception; use moodle_url; use core_form\dynamic_form; use mod_data\manager; +use mod_data\preset; /** * Save database as preset form. @@ -40,10 +41,16 @@ class save_as_preset extends dynamic_form { $this->_form->setType('d', PARAM_INT); $this->_form->addElement('hidden', 'action', 'save2'); $this->_form->setType('action', PARAM_ALPHANUM); + $this->_form->addElement('text', 'name', get_string('name'), ['size' => 60]); $this->_form->setType('name', PARAM_FILE); $this->_form->addRule('name', null, 'required'); - $this->_form->addElement('checkbox', 'overwrite', '', get_string('overrwritedesc', 'data')); + + // Overwrite checkbox will be hidden by default. It will only appear if there is an error when saving the preset. + $this->_form->addElement('checkbox', 'overwrite', '', get_string('overrwritedesc', 'data'), ['class' => 'hidden']); + + $this->_form->addElement('textarea', 'description', get_string('description'), ['rows' => 5, 'cols' => 60]); + $this->_form->setType('name', PARAM_TEXT); } /** @@ -91,11 +98,22 @@ class save_as_preset extends dynamic_form { } else { // If the preset exists now then we need to throw an error. $sitepresets = $manager->get_available_saved_presets(); + $usercandelete = false; foreach ($sitepresets as $preset) { if ($formdata['name'] == $preset->name) { - $errors['name'] = get_string('errorpresetexists', 'data'); + if (data_user_can_delete_preset($context, $preset)) { + $errors['name'] = get_string('errorpresetexists', 'data'); + $usercandelete = true; + } else { + $errors['name'] = get_string('errorpresetexistsbutnotoverwrite', 'data'); + } + break; } } + // If there are some errors, the checkbox should be displayed, to let users overwrite the preset. + if (!empty($errors) && $usercandelete) { + $this->_form->getElement('overwrite')->removeAttribute('class'); + } } return $errors; @@ -139,8 +157,8 @@ class save_as_preset extends dynamic_form { $context = \context_module::instance($cm->id, MUST_EXIST); try { + $manager = manager::create_from_instance($data); if (!empty($this->get_data()->overwrite)) { - $manager = manager::create_from_coursemodule($cm); $presets = $manager->get_available_presets(); $selectedpreset = new \stdClass(); foreach ($presets as $preset) { @@ -153,7 +171,8 @@ class save_as_preset extends dynamic_form { data_delete_site_preset($this->get_data()->name); } } - data_presets_save($course, $cm, $data, $this->get_data()->name); + $preset = preset::create_from_instance($manager, $this->get_data()->name, $this->get_data()->description); + $preset->save(); $result = true; } catch (\Exception $e) { $errors[] = $e->getMessage(); diff --git a/mod/data/classes/manager.php b/mod/data/classes/manager.php index 3111326ca5f..7799c90e372 100644 --- a/mod/data/classes/manager.php +++ b/mod/data/classes/manager.php @@ -341,14 +341,7 @@ class manager { continue; } - $preset = new stdClass(); - $preset->isplugin = false; - $preset->path = $file->get_filepath(); - $preset->name = trim($preset->path, '/'); - $preset->shortname = $preset->name; - $preset->userid = $userid; - $preset->id = $file->get_id(); - $preset->storedfile = $file; + $preset = preset::create_from_storedfile($this, $file); $presets[] = $preset; } @@ -366,12 +359,7 @@ class manager { $dirs = core_component::get_plugin_list('datapreset'); foreach ($dirs as $dir => $fulldir) { if (preset::is_directory_a_preset($fulldir)) { - $preset = new stdClass(); - $preset->isplugin = true; - $preset->path = $fulldir; - $preset->userid = 0; - $preset->shortname = $dir; - $preset->name = preset::get_name_from_plugin($dir); + $preset = preset::create_from_plugin(null, $dir); $presets[] = $preset; } } diff --git a/mod/data/classes/output/presets.php b/mod/data/classes/output/presets.php index 7c1c6618b74..31898dc49ec 100644 --- a/mod/data/classes/output/presets.php +++ b/mod/data/classes/output/presets.php @@ -18,6 +18,7 @@ namespace mod_data\output; use action_menu; use action_menu_link_secondary; +use mod_data\preset; use moodle_url; use templatable; use renderable; @@ -86,12 +87,13 @@ class presets implements templatable, renderable { $presets = []; foreach ($this->presets as $preset) { $presetname = $preset->name; - if (!empty($preset->userid)) { + $userid = $preset instanceof preset ? $preset->get_userid() : $preset->userid; + if (!empty($userid)) { // If the preset has the userid field, the full name of creator it will be added to the end of the name. $userfieldsapi = \core_user\fields::for_name(); $namefields = $userfieldsapi->get_sql('', false, '', '', false)->selects; $fields = 'id, ' . $namefields; - $presetuser = \core_user::get_user($preset->userid, $fields, MUST_EXIST); + $presetuser = \core_user::get_user($userid, $fields, MUST_EXIST); $username = fullname($presetuser, true); $presetname = "{$presetname} ({$username})"; } @@ -101,7 +103,7 @@ class presets implements templatable, renderable { // Only presets saved by users can be removed (so the datapreset plugins shouldn't display the delete button). if (!$preset->isplugin && data_user_can_delete_preset($PAGE->context, $preset)) { $deleteactionurl = new moodle_url('/mod/data/preset.php', - ['d' => $this->id, 'fullname' => "{$preset->userid}/{$preset->shortname}", + ['d' => $this->id, 'fullname' => "{$userid}/{$preset->shortname}", 'action' => 'confirmdelete']); $actionmenu = new action_menu(); @@ -124,7 +126,8 @@ class presets implements templatable, renderable { 'name' => $preset->name, 'shortname' => $preset->shortname, 'fullname' => $presetname, - 'userid' => $preset->userid, + 'description' => $preset->description, + 'userid' => $userid, 'actions' => $actions, ]; } diff --git a/mod/data/classes/preset.php b/mod/data/classes/preset.php index 6647766d58b..e2234e3aa02 100644 --- a/mod/data/classes/preset.php +++ b/mod/data/classes/preset.php @@ -16,6 +16,12 @@ namespace mod_data; +use core_component; +use invalid_parameter_exception; +use SimpleXMLElement; +use stdClass; +use stored_file; + /** * Class preset for database activity. * @@ -25,6 +31,243 @@ namespace mod_data; */ class preset { + /** @var manager manager instance. */ + private $manager; + + /** @var bool whether the preset is a plugin or has been saved by the user. */ + public $isplugin; + + /** @var string The preset name. */ + public $name; + + /** @var string The preset shortname. For datapreset plugins that is the folder; for saved presets, that's the preset name. */ + public $shortname; + + /** @var string The preset description. */ + public $description; + + /** @var stored_file For saved presets that's the file object for the root folder. It's null for plugins or for presets that + * haven't been saved yet. */ + public $storedfile; + + /** + * Class constructor. + * + * @param manager|null $manager the current instance manager + * @param bool $isplugin whether the preset is a plugin or has been saved by the user + * @param string $name the preset name + * @param string $shortname the preset shortname + * @param string|null $description the preset description + * @param stored_file|null $storedfile for saved presets, that's the file for the root folder + * @throws invalid_parameter_exception + */ + protected function __construct( + ?manager $manager, + bool $isplugin, + string $name, + string $shortname, + ?string $description = '', + ?stored_file $storedfile = null + ) { + if (!$isplugin && is_null($manager)) { + throw new invalid_parameter_exception('The $manager parameter can only be null for plugin presets.'); + } + $this->manager = $manager; + $this->isplugin = $isplugin; + $this->name = $name; + $this->shortname = $shortname; + $this->description = $description; + $this->storedfile = $storedfile; + } + + /** + * Create a preset instance from a stored file. + * + * @param manager $manager the current instance manager + * @param stored_file $file the preset root folder + * @return preset|null If the given file doesn't belong to the expected component/filearea/context, null will be returned + */ + public static function create_from_storedfile(manager $manager, stored_file $file): ?self { + if ($file->get_component() != DATA_PRESET_COMPONENT + || $file->get_filearea() != DATA_PRESET_FILEAREA + || $file->get_contextid() != DATA_PRESET_CONTEXT) { + return null; + } + + $isplugin = false; + $name = trim($file->get_filepath(), '/'); + $description = static::get_attribute_value($file->get_filepath(), 'description'); + + return new self($manager, $isplugin, $name, $name, $description, $file); + } + + /** + * Create a preset instance from a plugin. + * + * @param manager|null $manager the current instance manager + * @param string $pluginname the datapreset plugin name + * @return preset|null The plugin preset or null if there is no datapreset plugin with the given name. + */ + public static function create_from_plugin(?manager $manager, string $pluginname): ?self { + $found = false; + + $plugins = array_keys(core_component::get_plugin_list('datapreset')); + foreach ($plugins as $plugin) { + if ($plugin == $pluginname) { + $found = true; + break; + } + } + + if (!$found) { + // If there is no datapreset plugin with this name, return null. + return null; + } + + $name = static::get_name_from_plugin($pluginname); + $description = static::get_description_from_plugin($pluginname); + + return new self($manager, true, $name, $pluginname, $description); + } + + /** + * Create a preset instance from a data_record entry, a preset name and a description. + * + * @param manager $manager the current instance manager + * @param string $presetname the preset name + * @param string|null $description the preset description + * @return preset + */ + public static function create_from_instance(manager $manager, string $presetname, ?string $description = ''): self { + $isplugin = false; + + $path = '/' . $presetname . '/'; + $file = static::get_file($path, '.'); + + return new self($manager, $isplugin, $presetname, $presetname, $description, $file); + } + + /** + * Save this preset. + * + * @return bool true if the preset has been saved; false otherwise. + */ + public function save(): bool { + global $USER; + + if ($this->isplugin) { + // Plugin presets can't be saved. + return false; + } + + $result = false; + if (is_null($this->storedfile)) { + // The preset hasn't been saved before. + $fs = get_file_storage(); + + // Create and save the preset.xml file, with the description, settings, fields... + $filerecord = static::get_filerecord('preset.xml', $this->get_path(), $USER->id); + $fs->create_file_from_string($filerecord, $this->generate_preset_xml()); + + // Create and save the template files. + $instance = $this->manager->get_instance(); + foreach (manager::TEMPLATES_LIST as $templatename => $templatefile) { + $filerecord->filename = $templatefile; + $fs->create_file_from_string($filerecord, $instance->{$templatename}); + } + // Update the storedfile with the one we've just saved. + $this->storedfile = static::get_file($this->get_path(), '.'); + $result = true; + } + + return $result; + } + + /** + * Export this preset. + * + * @return string the full path to the exported preset file. + */ + public function export(): string { + if ($this->isplugin) { + // For now, only saved presets can be exported. + return ''; + } + + $presetname = clean_filename($this->name) . '-preset-' . gmdate("Ymd_Hi"); + $exportsubdir = "mod_data/presetexport/$presetname"; + $exportdir = make_temp_directory($exportsubdir); + + // Generate and write the preset.xml file. + $presetxmldata = static::generate_preset_xml(); + $presetxmlfile = fopen($exportdir . '/preset.xml', 'w'); + fwrite($presetxmlfile, $presetxmldata); + fclose($presetxmlfile); + + // Write the template files. + $instance = $this->manager->get_instance(); + foreach (manager::TEMPLATES_LIST as $templatename => $templatefilename) { + $templatefile = fopen("$exportdir/$templatefilename", 'w'); + fwrite($templatefile, $instance->{$templatename}); + fclose($templatefile); + } + + // Check if all files have been generated. + if (! static::is_directory_a_preset($exportdir)) { + throw new \moodle_exception('generateerror', 'data'); + } + + $presetfilenames = array_merge(array_values(manager::TEMPLATES_LIST), ['preset.xml']); + + $filelist = []; + foreach ($presetfilenames as $filename) { + $filelist[$filename] = $exportdir . '/' . $filename; + } + + $exportfile = $exportdir.'.zip'; + file_exists($exportfile) && unlink($exportfile); + + $fp = get_file_packer('application/zip'); + $fp->archive_to_pathname($filelist, $exportfile); + + foreach ($filelist as $file) { + unlink($file); + } + rmdir($exportdir); + + return $exportfile; + } + + /** + * Return the preset author. + * + * @return int|null + */ + public function get_userid(): ?int { + if (!empty($this->storedfile)) { + return $this->storedfile->get_userid(); + } + + return null; + } + + /** + * Returns the preset path. + * + * @return string|null the preset path is null for plugins and /presetname/ for saved presets. + */ + public function get_path(): ?string { + if ($this->isplugin) { + return null; + } + + if (!empty($this->storedfile)) { + return $this->storedfile->get_filepath(); + } + + return '/' . $this->name . '/'; + } + /** * Checks if a directory contains all the required files to define a preset. * @@ -55,4 +298,179 @@ class preset { return $pluginname; } } + + /** + * Returns the description to show for a datapreset plugin. + * + * @param string $pluginname The datapreset plugin name. + * @return string The plugin preset description to display. + */ + public static function get_description_from_plugin(string $pluginname): string { + if (get_string_manager()->string_exists('modulename_help', 'datapreset_'.$pluginname)) { + return get_string('modulename_help', 'datapreset_'.$pluginname); + } else { + return ''; + } + } + + /** + * Helper to get the value of one of the elements in the presets.xml file. + * + * @param string $filepath The preset filepath. + * @param string $name Attribute name to return. + * @return string|null The attribute value; null if the it doesn't exist or the file is not a valid XML. + */ + protected static function get_attribute_value(string $filepath, string $name): ?string { + $value = null; + $presetxml = static::get_content_from_file($filepath, 'preset.xml'); + $parsedxml = simplexml_load_string($presetxml); + if ($parsedxml) { + switch ($name) { + case 'description': + if (property_exists($parsedxml, 'description')) { + $value = $parsedxml->description; + } + break; + } + } + + return $value; + } + + /** + * Helper method to get a file record given a filename, a filepath and a userid, for any of the preset files. + * + * @param string $filename The filename for the filerecord that will be returned. + * @param string $filepath The filepath for the filerecord that will be returned. + * @param int $userid The userid for the filerecord that will be returned. + * @return stdClass A filerecord object with the datapreset context, component and filearea and the given information. + */ + protected static function get_filerecord(string $filename, string $filepath, int $userid): stdClass { + $filerecord = new stdClass; + $filerecord->contextid = DATA_PRESET_CONTEXT; + $filerecord->component = DATA_PRESET_COMPONENT; + $filerecord->filearea = DATA_PRESET_FILEAREA; + $filerecord->itemid = 0; + $filerecord->filepath = $filepath; + $filerecord->userid = $userid; + $filerecord->filename = $filename; + + return $filerecord; + } + + /** + * Helper method to retrieve a file. + * + * @param string $filepath the directory to look in + * @param string $filename the name of the file we want + * @return stored_file|null the file or null if the file doesn't exist. + */ + public static function get_file(string $filepath, string $filename): ?stored_file { + $file = null; + $fs = get_file_storage(); + $fileexists = $fs->file_exists( + DATA_PRESET_CONTEXT, + DATA_PRESET_COMPONENT, + DATA_PRESET_FILEAREA, + 0, + $filepath, + $filename + ); + if ($fileexists) { + $file = $fs->get_file( + DATA_PRESET_CONTEXT, + DATA_PRESET_COMPONENT, + DATA_PRESET_FILEAREA, + 0, + $filepath, + $filename + ); + } + + return $file; + } + + /** + * Helper method to retrieve the contents of a file. + * + * @param string $filepath the directory to look in + * @param string $filename the name of the file we want + * @return string|null the contents of the file or null if the file doesn't exist. + */ + protected static function get_content_from_file(string $filepath, string $filename): ?string { + $templatefile = static::get_file($filepath, $filename); + if ($templatefile) { + return $templatefile->get_content(); + } + + return null; + } + + /** + * Helper method to generate the XML for this preset. + * + * @return string The XML for the preset + */ + protected function generate_preset_xml(): string { + global $DB; + + if ($this->isplugin) { + // Only saved presets can generate the preset.xml file. + return ''; + } + + $presetxmldata = "\n\n"; + + // Add description. + $presetxmldata .= '' . htmlspecialchars($this->description) . "\n\n"; + + // Add settings. + // Raw settings are not preprocessed during saving of presets. + $rawsettings = [ + 'intro', + 'comments', + 'requiredentries', + 'requiredentriestoview', + 'maxentries', + 'rssarticles', + 'approval', + 'manageapproved', + 'defaultsortdir', + ]; + $presetxmldata .= "\n"; + $instance = $this->manager->get_instance(); + // First, settings that do not require any conversion. + foreach ($rawsettings as $setting) { + $presetxmldata .= "<$setting>" . htmlspecialchars($instance->$setting) . "\n"; + } + + // Now specific settings. + if ($instance->defaultsort > 0 && $sortfield = data_get_field_from_id($instance->defaultsort, $instance)) { + $presetxmldata .= '' . htmlspecialchars($sortfield->field->name) . "\n"; + } else { + $presetxmldata .= "0\n"; + } + $presetxmldata .= "\n\n"; + + // Add fields. Grab all that are non-empty. + $fields = $DB->get_records('data_fields', ['dataid' => $instance->id]); + ksort($fields); + if (!empty($fields)) { + foreach ($fields as $field) { + $presetxmldata .= "\n"; + foreach ($field as $key => $value) { + if ($value != '' && $key != 'id' && $key != 'dataid') { + $presetxmldata .= "<$key>" . htmlspecialchars($value) . "\n"; + } + } + $presetxmldata .= "\n\n"; + } + } + $presetxmldata .= ''; + + // Check this content is a valid XML. + $preset = new SimpleXMLElement($presetxmldata); + + return $preset->asXML(); + } } diff --git a/mod/data/lang/en/data.php b/mod/data/lang/en/data.php index 185a714f7eb..bf600144b53 100644 --- a/mod/data/lang/en/data.php +++ b/mod/data/lang/en/data.php @@ -146,7 +146,8 @@ $string['entrieslefttoaddtoview'] = 'You must add {$a->entrieslefttoview} more e $string['entry'] = 'Entry'; $string['entrysaved'] = 'Your entry has been saved'; $string['errormustbeteacher'] = 'You need to be a teacher to use this page!'; -$string['errorpresetexists'] = 'There is already a preset with the selected name'; +$string['errorpresetexists'] = 'A preset with this name already exists.'; +$string['errorpresetexistsbutnotoverwrite'] = 'A preset with this name already exists. Choose a different name.'; $string['errormustsupplyvalue'] = 'You must supply a value here.'; $string['example'] = 'Database module example'; $string['excel'] = 'Excel'; @@ -307,7 +308,7 @@ $string['optionaldescription'] = 'Short description (optional)'; $string['optionalfilename'] = 'Filename (optional)'; $string['other'] = 'Other'; $string['overwrite'] = 'Overwrite'; -$string['overrwritedesc'] = 'Overwrite the preset if it already exists'; +$string['overrwritedesc'] = 'Replace existing preset with this name and overwrite its contents'; $string['overwritesettings'] = 'Overwrite current settings'; $string['page-mod-data-x'] = 'Any database activity module page'; $string['pagesize'] = 'Entries per page'; diff --git a/mod/data/lib.php b/mod/data/lib.php index f11f58cf6f4..a1820a6bc14 100644 --- a/mod/data/lib.php +++ b/mod/data/lib.php @@ -3450,52 +3450,16 @@ function data_extend_settings_navigation(settings_navigation $settings, navigati * @param stdClass $data The database record * @param string $path * @return bool + * @deprecated since Moodle 4.1 MDL-75142 - please, use the preset::save() function instead. + * @todo MDL-75189 This will be deleted in Moodle 4.5. + * @see preset::save() */ function data_presets_save($course, $cm, $data, $path) { - global $USER; - $fs = get_file_storage(); - $filerecord = new stdClass; - $filerecord->contextid = DATA_PRESET_CONTEXT; - $filerecord->component = DATA_PRESET_COMPONENT; - $filerecord->filearea = DATA_PRESET_FILEAREA; - $filerecord->itemid = 0; - $filerecord->filepath = '/'.$path.'/'; - $filerecord->userid = $USER->id; + debugging('data_presets_save() is deprecated. Please use preset::save() instead.', DEBUG_DEVELOPER); - $filerecord->filename = 'preset.xml'; - $fs->create_file_from_string($filerecord, data_presets_generate_xml($course, $cm, $data)); - - $filerecord->filename = 'singletemplate.html'; - $fs->create_file_from_string($filerecord, $data->singletemplate); - - $filerecord->filename = 'listtemplateheader.html'; - $fs->create_file_from_string($filerecord, $data->listtemplateheader); - - $filerecord->filename = 'listtemplate.html'; - $fs->create_file_from_string($filerecord, $data->listtemplate); - - $filerecord->filename = 'listtemplatefooter.html'; - $fs->create_file_from_string($filerecord, $data->listtemplatefooter); - - $filerecord->filename = 'addtemplate.html'; - $fs->create_file_from_string($filerecord, $data->addtemplate); - - $filerecord->filename = 'rsstemplate.html'; - $fs->create_file_from_string($filerecord, $data->rsstemplate); - - $filerecord->filename = 'rsstitletemplate.html'; - $fs->create_file_from_string($filerecord, $data->rsstitletemplate); - - $filerecord->filename = 'csstemplate.css'; - $fs->create_file_from_string($filerecord, $data->csstemplate); - - $filerecord->filename = 'jstemplate.js'; - $fs->create_file_from_string($filerecord, $data->jstemplate); - - $filerecord->filename = 'asearchtemplate.html'; - $fs->create_file_from_string($filerecord, $data->asearchtemplate); - - return true; + $manager = manager::create_from_instance($data); + $preset = preset::create_from_instance($manager, $path); + return $preset->save(); } /** @@ -3506,151 +3470,42 @@ function data_presets_save($course, $cm, $data, $path) { * @param stdClass $cm The course module record * @param stdClass $data The database record * @return string The XML for the preset + * @deprecated since Moodle 4.1 MDL-75142 - please, use the protected preset::generate_preset_xml() function instead. + * @todo MDL-75189 This will be deleted in Moodle 4.5. + * @see preset::generate_preset_xml() */ function data_presets_generate_xml($course, $cm, $data) { - global $DB; - - // Assemble "preset.xml": - $presetxmldata = "\n\n"; - - // Raw settings are not preprocessed during saving of presets - $raw_settings = array( - 'intro', - 'comments', - 'requiredentries', - 'requiredentriestoview', - 'maxentries', - 'rssarticles', - 'approval', - 'manageapproved', - 'defaultsortdir' + debugging( + 'data_presets_generate_xml() is deprecated. Please use the protected preset::generate_preset_xml() instead.', + DEBUG_DEVELOPER ); - $presetxmldata .= "\n"; - // First, settings that do not require any conversion - foreach ($raw_settings as $setting) { - $presetxmldata .= "<$setting>" . htmlspecialchars($data->$setting) . "\n"; - } - - // Now specific settings - if ($data->defaultsort > 0 && $sortfield = data_get_field_from_id($data->defaultsort, $data)) { - $presetxmldata .= '' . htmlspecialchars($sortfield->field->name) . "\n"; - } else { - $presetxmldata .= "0\n"; - } - $presetxmldata .= "\n\n"; - // Now for the fields. Grab all that are non-empty - $fields = $DB->get_records('data_fields', array('dataid'=>$data->id)); - ksort($fields); - if (!empty($fields)) { - foreach ($fields as $field) { - $presetxmldata .= "\n"; - foreach ($field as $key => $value) { - if ($value != '' && $key != 'id' && $key != 'dataid') { - $presetxmldata .= "<$key>" . htmlspecialchars($value) . "\n"; - } - } - $presetxmldata .= "\n\n"; - } - } - $presetxmldata .= ''; - return $presetxmldata; + $manager = manager::create_from_instance($data); + $preset = preset::create_from_instance($manager, $data->name); + $reflection = new \ReflectionClass(preset::class); + $method = $reflection->getMethod('generate_preset_xml'); + $method->setAccessible(true); + return $method->invokeArgs($preset, []); } +/** + * Export current fields and presets. + * + * @param stdClass $course The course the database module belongs to. + * @param stdClass $cm The course module record + * @param stdClass $data The database record + * @param bool $tostorage + * @return string the full path to the exported preset file. + * @deprecated since Moodle 4.1 MDL-75142 - please, use the preset::export() function instead. + * @todo MDL-75189 This will be deleted in Moodle 4.5. + * @see preset::export() + */ function data_presets_export($course, $cm, $data, $tostorage=false) { - global $CFG, $DB; + debugging('data_presets_export() is deprecated. Please use preset::export() instead.', DEBUG_DEVELOPER); - $presetname = clean_filename($data->name) . '-preset-' . gmdate("Ymd_Hi"); - $exportsubdir = "mod_data/presetexport/$presetname"; - make_temp_directory($exportsubdir); - $exportdir = "$CFG->tempdir/$exportsubdir"; - - // Assemble "preset.xml": - $presetxmldata = data_presets_generate_xml($course, $cm, $data); - - // After opening a file in write mode, close it asap - $presetxmlfile = fopen($exportdir . '/preset.xml', 'w'); - fwrite($presetxmlfile, $presetxmldata); - fclose($presetxmlfile); - - // Now write the template files - $singletemplate = fopen($exportdir . '/singletemplate.html', 'w'); - fwrite($singletemplate, $data->singletemplate); - fclose($singletemplate); - - $listtemplateheader = fopen($exportdir . '/listtemplateheader.html', 'w'); - fwrite($listtemplateheader, $data->listtemplateheader); - fclose($listtemplateheader); - - $listtemplate = fopen($exportdir . '/listtemplate.html', 'w'); - fwrite($listtemplate, $data->listtemplate); - fclose($listtemplate); - - $listtemplatefooter = fopen($exportdir . '/listtemplatefooter.html', 'w'); - fwrite($listtemplatefooter, $data->listtemplatefooter); - fclose($listtemplatefooter); - - $addtemplate = fopen($exportdir . '/addtemplate.html', 'w'); - fwrite($addtemplate, $data->addtemplate); - fclose($addtemplate); - - $rsstemplate = fopen($exportdir . '/rsstemplate.html', 'w'); - fwrite($rsstemplate, $data->rsstemplate); - fclose($rsstemplate); - - $rsstitletemplate = fopen($exportdir . '/rsstitletemplate.html', 'w'); - fwrite($rsstitletemplate, $data->rsstitletemplate); - fclose($rsstitletemplate); - - $csstemplate = fopen($exportdir . '/csstemplate.css', 'w'); - fwrite($csstemplate, $data->csstemplate); - fclose($csstemplate); - - $jstemplate = fopen($exportdir . '/jstemplate.js', 'w'); - fwrite($jstemplate, $data->jstemplate); - fclose($jstemplate); - - $asearchtemplate = fopen($exportdir . '/asearchtemplate.html', 'w'); - fwrite($asearchtemplate, $data->asearchtemplate); - fclose($asearchtemplate); - - // Check if all files have been generated - if (! preset::is_directory_a_preset($exportdir)) { - throw new \moodle_exception('generateerror', 'data'); - } - - $filenames = array( - 'preset.xml', - 'singletemplate.html', - 'listtemplateheader.html', - 'listtemplate.html', - 'listtemplatefooter.html', - 'addtemplate.html', - 'rsstemplate.html', - 'rsstitletemplate.html', - 'csstemplate.css', - 'jstemplate.js', - 'asearchtemplate.html' - ); - - $filelist = array(); - foreach ($filenames as $filename) { - $filelist[$filename] = $exportdir . '/' . $filename; - } - - $exportfile = $exportdir.'.zip'; - file_exists($exportfile) && unlink($exportfile); - - $fp = get_file_packer('application/zip'); - $fp->archive_to_pathname($filelist, $exportfile); - - foreach ($filelist as $file) { - unlink($file); - } - rmdir($exportdir); - - // Return the full path to the exported preset file: - return $exportfile; + $manager = manager::create_from_instance($data); + $preset = preset::create_from_instance($manager, $data->name); + return $preset->export(); } /** @@ -3999,7 +3854,8 @@ function data_user_can_delete_preset($context, $preset) { return true; } else { $candelete = false; - if ($preset->userid == $USER->id) { + $userid = $preset instanceof preset ? $preset->get_userid() : $preset->userid; + if ($userid == $USER->id) { $candelete = true; } return $candelete; diff --git a/mod/data/preset.php b/mod/data/preset.php index e10370c71a2..52b350f5e50 100644 --- a/mod/data/preset.php +++ b/mod/data/preset.php @@ -29,6 +29,7 @@ */ use mod_data\manager; +use mod_data\preset; require_once('../../config.php'); require_once($CFG->dirroot.'/mod/data/lib.php'); @@ -84,7 +85,8 @@ if ($action === 'export') { throw new \moodle_exception('headersent'); } - $exportfile = data_presets_export($course, $cm, $data); + $preset = preset::create_from_instance($manager, $data->name); + $exportfile = $preset->export(); $exportfilename = basename($exportfile); header("Content-Type: application/download\n"); header("Content-Disposition: attachment; filename=\"$exportfilename\""); diff --git a/mod/data/preset/imagegallery/lang/en/datapreset_imagegallery.php b/mod/data/preset/imagegallery/lang/en/datapreset_imagegallery.php index 2f97aab4fb4..ea93e0f8a89 100644 --- a/mod/data/preset/imagegallery/lang/en/datapreset_imagegallery.php +++ b/mod/data/preset/imagegallery/lang/en/datapreset_imagegallery.php @@ -24,5 +24,6 @@ */ $string['modulename'] = 'Image gallery'; +$string['modulename_help'] = 'Use this preset to collect images.'; $string['pluginname'] = 'Image gallery'; $string['privacy:metadata'] = 'The Image gallery plugin does not store any personal data.'; diff --git a/mod/data/preset_form.php b/mod/data/preset_form.php index 5881214b105..03ec52a9858 100644 --- a/mod/data/preset_form.php +++ b/mod/data/preset_form.php @@ -17,7 +17,8 @@ class data_existing_preset_form extends moodleform { $this->_form->setType('action', PARAM_ALPHANUM); $delete = get_string('delete'); foreach ($this->_customdata['presets'] as $preset) { - $this->_form->addElement('radio', 'fullname', null, ' '.$preset->description, $preset->userid.'/'.$preset->shortname); + $userid = $preset instanceof \mod_data\preset ? $preset->get_userid() : $preset->userid; + $this->_form->addElement('radio', 'fullname', null, ' '.$preset->description, $userid.'/'.$preset->shortname); } $this->_form->addElement('submit', 'importexisting', get_string('choose')); } diff --git a/mod/data/templates/presets.mustache b/mod/data/templates/presets.mustache index 06754453dfe..18fecfb445c 100644 --- a/mod/data/templates/presets.mustache +++ b/mod/data/templates/presets.mustache @@ -31,6 +31,7 @@ "name": "Image gallery", "shortname": "imagegallery", "fullname": "Image gallery", + "description": "Use this preset to collect images", "userid": 0, "actions": [] }, @@ -56,7 +57,8 @@ - {{#str}} name {{/str}} + {{#str}} name {{/str}} + {{#str}} description {{/str}} {{#showmanage}}{{#str}} action {{/str}}{{/showmanage}} @@ -67,6 +69,7 @@ {{fullname}} + {{description}} {{#actions}}
diff --git a/mod/data/tests/behat/data_presets.feature b/mod/data/tests/behat/data_presets.feature index d8f5f09965d..bbc653c896b 100644 --- a/mod/data/tests/behat/data_presets.feature +++ b/mod/data/tests/behat/data_presets.feature @@ -18,9 +18,9 @@ Feature: Users can view and manage data presets | activity | name | intro | course | idnumber | | data | Mountain landscapes | n | C1 | data1 | And the following "mod_data > presets" exist: - | database | name | - | data1 | Saved preset 1 | - | data1 | Saved preset 2 | + | database | name | description | + | data1 | Saved preset 1 | The preset1 has description | + | data1 | Saved preset 2 | | @javascript Scenario: Admins can delete saved presets @@ -51,7 +51,9 @@ Feature: Users can view and manage data presets When I follow "Presets" Then I should see "Choose a preset to use as a starting point." And I should see "Image gallery" + And I should see "Use this preset to collect images." in the "Image gallery" "table_row" And I should see "Saved preset 1" + And I should see "The preset1 has description" in the "Saved preset 1" "table_row" And I should see "Saved preset 2" And I should see "Saved preset by teacher1" # Plugin presets can't be removed. @@ -79,3 +81,60 @@ Feature: Users can view and manage data presets Then I should see "Image gallery" And I should not see "Saved preset 1" And I should not see "Saved preset 2" + + @javascript + Scenario: Teachers can save presets + Given the following "mod_data > fields" exist: + | database | type | name | description | + | data1 | text | Test field name | Test field description | + And I am on the "Mountain landscapes" "data activity" page logged in as teacher1 + And I follow "Templates" + When I click on "Save as preset" "button" + Then I should see "Name" in the "Save all fields and templates as preset" "dialogue" + And I should see "Description" in the "Save all fields and templates as preset" "dialogue" + And "Replace existing preset with this name and overwrite its contents" "checkbox" should not be visible + # Teacher should be able to save preset. + And I set the field "Name" to "Saved preset by teacher1" + And I set the field "Description" to "My funny description goes here." + And I click on "Save" "button" in the "Save all fields and templates as preset" "dialogue" + And I should see "Saved successfully. Your preset will now be available across the site." + And I follow "Presets" + And I should see "Saved preset by teacher1" + And I should see "My funny description goes here." in the "Saved preset by teacher1" "table_row" + # Teacher can't overwrite an existing preset that they haven't created. + And I follow "Templates" + And I click on "Save as preset" "button" + And I set the field "Name" to "Saved preset 1" + And I click on "Save" "button" in the "Save all fields and templates as preset" "dialogue" + And I should see "A preset with this name already exists. Choose a different name." + And "Replace existing preset with this name and overwrite its contents" "checkbox" should not be visible + # Teacher can overwrite existing presets created by them, but they are not overwritten if the checkbox is not marked. + And I set the field "Name" to "Saved preset by teacher1" + And I set the field "Description" to "This is a new description that shouldn't be saved." + And I click on "Save" "button" in the "Save all fields and templates as preset" "dialogue" + And I should see "A preset with this name already exists." + And "Replace existing preset with this name and overwrite its contents" "checkbox" should be visible + # Confirm the checkbox is still displayed and nothing happens if it's not checked and no change is done in the name. + And I click on "Save" "button" in the "Save all fields and templates as preset" "dialogue" + And I should see "A preset with this name already exists." + And "Replace existing preset with this name and overwrite its contents" "checkbox" should be visible + And I click on "Cancel" "button" in the "Save all fields and templates as preset" "dialogue" + And I follow "Presets" + And I should see "Saved preset by teacher1" + And I should see "My funny description goes here." in the "Saved preset by teacher1" "table_row" + And I should not see "This is a new description that shouldn't be saved." + # But teacher can overwrite existing presets created by them. + But I follow "Templates" + And I click on "Save as preset" "button" + And I set the field "Name" to "Saved preset by teacher1" + And I set the field "Description" to "This is a new description that will be overwritten." + And I click on "Save" "button" in the "Save all fields and templates as preset" "dialogue" + And I should see "A preset with this name already exists." + And "Replace existing preset with this name and overwrite its contents" "checkbox" should be visible + And I click on "Replace existing preset with this name and overwrite its contents" "checkbox" in the "Save all fields and templates as preset" "dialogue" + And I click on "Save" "button" in the "Save all fields and templates as preset" "dialogue" + And I should see "Saved successfully. Your preset will now be available across the site." + And I follow "Presets" + And I should see "Saved preset by teacher1" + And I should see "This is a new description that will be overwritten." in the "Saved preset by teacher1" "table_row" + And I should not see "My funny description goes here." in the "Saved preset by teacher1" "table_row" diff --git a/mod/data/tests/generator/lib.php b/mod/data/tests/generator/lib.php index 8ed2ea440ca..a1497e9faa7 100644 --- a/mod/data/tests/generator/lib.php +++ b/mod/data/tests/generator/lib.php @@ -24,6 +24,7 @@ */ use mod_data\manager; +use mod_data\preset; defined('MOODLE_INTERNAL') || die(); @@ -369,22 +370,27 @@ class mod_data_generator extends testing_module_generator { * * @param stdClass $instance The mod_data instance. * @param stdClass|null $record The preset information, like 'name'. - * @return bool Whether the preset has been created or not. + * @return preset The preset that has been created. */ - public function create_preset(stdClass $instance, stdClass $record = null): bool { - global $DB; - + public function create_preset(stdClass $instance, stdClass $record = null): preset { if (is_null($record)) { $record = new stdClass(); } // Fill in optional values if not specified. - if (!isset($record->name)) { - $record->name = 'New preset ' . microtime(); + $presetname = 'New preset ' . microtime(); + if (isset($record->name)) { + $presetname = $record->name; + } + $presetdescription = null; + if (isset($record->description)) { + $presetdescription = $record->description; } - $course = $DB->get_record('course', ['id' => $instance->course], '*', MUST_EXIST); - $cm = get_coursemodule_from_instance(manager::MODULE, $instance->id, $course->id, null, MUST_EXIST); - return data_presets_save($course, $cm, $instance, $record->name); + $manager = manager::create_from_instance($instance); + $preset = preset::create_from_instance($manager, $presetname, $presetdescription); + $preset->save(); + + return $preset; } } diff --git a/mod/data/tests/generator_test.php b/mod/data/tests/generator_test.php index b7e78d9a40c..24a87938d94 100644 --- a/mod/data/tests/generator_test.php +++ b/mod/data/tests/generator_test.php @@ -16,6 +16,8 @@ namespace mod_data; +use stdClass; + /** * PHPUnit data generator testcase. * @@ -234,9 +236,13 @@ class generator_test extends \advanced_testcase { /** * Test for create_preset(). * + * @dataProvider create_preset_provider * @covers ::create_preset + * @param stdClass|null $record data for the preset that will be created (like name or description) */ - public function test_create_preset() { + public function test_create_preset(?stdClass $record) { + global $USER; + $this->resetAfterTest(); $this->setAdminUser(); @@ -249,27 +255,55 @@ class generator_test extends \advanced_testcase { $savedpresets = $manager->get_available_saved_presets(); $this->assertEmpty($savedpresets); - // Create one preset with the default configuration. + // Create one preset with the configuration in $record. $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); - $result = $plugingenerator->create_preset($activity); - $this->assertTrue($result); + $preset = $plugingenerator->create_preset($activity, $record); + // Check the preset has been saved. $savedpresets = $manager->get_available_saved_presets(); $this->assertCount(1, $savedpresets); - $preset = reset($savedpresets); - $this->assertStringStartsWith('New preset', $preset->name); - - // Create another preset with a specific name. - $record = (object) [ - 'name' => 'World recipes preset', - ]; - $result = $plugingenerator->create_preset($activity, $record); - $this->assertTrue($result); - $savedpresets = $manager->get_available_saved_presets(); - $this->assertCount(2, $savedpresets); - foreach ($savedpresets as $preset) { - if (!str_starts_with($preset->name, 'New preset')) { - $this->assertEquals('World recipes preset', $preset->name); - } + // Check the preset name has the expected value. + if (is_null($record) || !property_exists($record, 'name')) { + $this->assertStringStartsWith('New preset', $preset->name); + } else { + $this->assertEquals($record->name, $preset->name); } + // Check the preset description has the expected value. + if (is_null($record) || !property_exists($record, 'description')) { + $this->assertEmpty($preset->description); + } else { + $this->assertEquals($record->description, $preset->description); + } + // Check the file has been updated properly. + $this->assertNotNull($preset->storedfile); + $this->assertEquals($USER->id, $preset->get_userid()); + } + + /** + * Data provider for test_create_preset(). + * + * @return array + */ + public function create_preset_provider(): array { + return [ + 'Create using the default configuration' => [ + 'record' => null, + ], + 'Create with a given name but no description' => [ + 'record' => (object) [ + 'name' => 'World recipes preset', + ], + ], + 'Create with a given description but no name' => [ + 'record' => (object) [ + 'description' => 'This is a preset to collect the most popular world recipes.', + ], + ], + 'Create with a given name and description' => [ + 'record' => (object) [ + 'name' => 'World recipes preset', + 'description' => 'This is a preset to collect the most popular world recipes.', + ], + ], + ]; } } diff --git a/mod/data/tests/preset_test.php b/mod/data/tests/preset_test.php index 3c07c7e5ebc..4b77063454e 100644 --- a/mod/data/tests/preset_test.php +++ b/mod/data/tests/preset_test.php @@ -16,6 +16,10 @@ namespace mod_data; +use file_archive; +use stdClass; +use zip_archive; + /** * Preset tests class for mod_data. * @@ -27,6 +31,306 @@ namespace mod_data; */ class preset_test extends \advanced_testcase { + /** + * Test for static create_from_plugin method. + * + * @covers ::create_from_plugin + */ + public function test_create_from_plugin() { + $this->resetAfterTest(); + $this->setAdminUser(); + + // Check create_from_plugin is working as expected when an existing plugin is given. + $pluginname = 'imagegallery'; + $result = preset::create_from_plugin(null, $pluginname); + $this->assertTrue($result->isplugin); + $this->assertEquals(get_string('modulename', "datapreset_$pluginname"), $result->name); + $this->assertEquals($pluginname, $result->shortname); + $this->assertEquals(get_string('modulename_help', "datapreset_$pluginname"), $result->description); + $this->assertEmpty($result->get_userid()); + $this->assertEmpty($result->storedfile); + $this->assertNull($result->get_path()); + + // Check create_from_plugin is working as expected when an unexisting plugin is given. + $pluginname = 'unexisting'; + $result = preset::create_from_plugin(null, $pluginname); + $this->assertNull($result); + } + + /** + * Test for static create_from_storedfile method. + * + * @covers ::create_from_storedfile + */ + public function test_create_from_storedfile() { + global $USER; + + $this->resetAfterTest(); + $this->setAdminUser(); + + // Create a course and a database activity. + $course = $this->getDataGenerator()->create_course(); + $activity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); + $manager = manager::create_from_instance($activity); + + // Create a saved preset. + $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); + $record = (object) [ + 'name' => 'Testing preset name', + 'description' => 'Testing preset description', + ]; + $plugingenerator->create_preset($activity, $record); + $savedpresets = $manager->get_available_saved_presets(); + $savedpreset = reset($savedpresets); + + // Check create_from_storedfile is working as expected with a valid preset file. + $result = preset::create_from_storedfile($manager, $savedpreset->storedfile); + $this->assertFalse($result->isplugin); + $this->assertEquals($record->name, $result->name); + $this->assertEquals($record->name, $result->shortname); + $this->assertEquals($record->description, $result->description); + $this->assertEquals($savedpreset->storedfile->get_userid(), $result->get_userid()); + $this->assertNotEmpty($result->storedfile); + $this->assertEquals('/' . $record->name . '/', $result->get_path()); + + // Check create_from_storedfile is not creating a preset object when an invalid file is given. + $draftid = file_get_unused_draft_itemid(); + $filerecord = [ + 'component' => 'user', + 'filearea' => 'draft', + 'contextid' => \context_user::instance($USER->id)->id, + 'itemid' => $draftid, + 'filename' => 'preset.xml', + 'filepath' => '/' + ]; + $fs = get_file_storage(); + $file = $fs->create_file_from_string($filerecord, 'This is the file content'); + $result = preset::create_from_storedfile($manager, $file); + $this->assertNull($result); + } + + /** + * Test for static create_from_instance method. + * + * @covers ::create_from_instance + */ + public function test_create_from_instance() { + $this->resetAfterTest(); + $this->setAdminUser(); + + // Create a course and a database activity. + $course = $this->getDataGenerator()->create_course(); + $activity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); + $manager = manager::create_from_instance($activity); + + // Create a saved preset. + $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); + $record = (object) [ + 'name' => 'Testing preset name', + 'description' => 'Testing preset description', + ]; + $plugingenerator->create_preset($activity, $record); + $savedpresets = $manager->get_available_saved_presets(); + $savedpreset = reset($savedpresets); + + // Check create_from_instance is working as expected when a preset with this name exists. + $result = preset::create_from_instance($manager, $record->name, $record->description); + $this->assertFalse($result->isplugin); + $this->assertEquals($record->name, $result->name); + $this->assertEquals($record->name, $result->shortname); + $this->assertEquals($record->description, $result->description); + $this->assertEquals($savedpreset->storedfile->get_userid(), $result->get_userid()); + $this->assertNotEmpty($result->storedfile); + $this->assertEquals('/' . $record->name . '/', $result->get_path()); + + // Check create_from_instance is working as expected when there is no preset with the given name. + $presetname = 'Unexisting preset'; + $presetdescription = 'This is the description for the unexisting preset'; + $result = preset::create_from_instance($manager, $presetname, $presetdescription); + $this->assertFalse($result->isplugin); + $this->assertEquals($presetname, $result->name); + $this->assertEquals($presetname, $result->shortname); + $this->assertEquals($presetdescription, $result->description); + $this->assertEmpty($result->get_userid()); + $this->assertEmpty($result->storedfile); + $this->assertEquals('/' . $presetname . '/', $result->get_path()); + } + + /** + * Test for the save a preset method. + * + * @covers ::save + */ + public function test_save() { + $this->resetAfterTest(); + $this->setAdminUser(); + + // Save should return false when trying to save a plugin preset. + $preset = preset::create_from_plugin(null, 'imagegallery'); + $result = $preset->save(); + $this->assertFalse($result); + + // Create a course and a database activity. + $course = $this->getDataGenerator()->create_course(); + $activity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); + $manager = manager::create_from_instance($activity); + + // Add a field to the activity. + $fieldrecord = new stdClass(); + $fieldrecord->name = 'field-1'; + $fieldrecord->type = 'text'; + $datagenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); + $datagenerator->create_field($fieldrecord, $activity); + + // Create a saved preset. + $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); + $record = (object) [ + 'name' => 'Testing preset name', + 'description' => 'Testing preset description', + ]; + $plugingenerator->create_preset($activity, $record); + + // Save should return false when trying to save an existing saved preset. + $preset = preset::create_from_instance($manager, $record->name, $record->description); + $result = $preset->save(); + $this->assertFalse($result); + + // The preset should be saved when it's new and there is no any other having the same name. + $savedpresets = $manager->get_available_saved_presets(); + $this->assertCount(1, $savedpresets); + $presetname = 'New preset'; + $presetdescription = 'This is the description for the new preset'; + $preset = preset::create_from_instance($manager, $presetname, $presetdescription); + $result = $preset->save(); + $this->assertTrue($result); + // Check the preset has been created. + $savedpresets = $manager->get_available_saved_presets(); + $this->assertCount(2, $savedpresets); + $savedpresetsnames = array_map(function($preset) { + return $preset->name; + }, $savedpresets); + $this->assertContains($presetname, $savedpresetsnames); + } + + /** + * Test for the export a preset method. + * + * @covers ::export + */ + public function test_export() { + $this->resetAfterTest(); + $this->setAdminUser(); + + // Export should return empty string when trying to export a plugin preset. + $preset = preset::create_from_plugin(null, 'imagegallery'); + $result = $preset->export(); + $this->assertEmpty($result); + + // Create a course and a database activity. + $course = $this->getDataGenerator()->create_course(); + $activity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); + $manager = manager::create_from_instance($activity); + + // Add a field to the activity. + $fieldrecord = new stdClass(); + $fieldrecord->name = 'field-1'; + $fieldrecord->type = 'text'; + $datagenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); + $datagenerator->create_field($fieldrecord, $activity); + + // For now, default templates are not created automatically. This will be changed in MDL-75234. + foreach (manager::TEMPLATES_LIST as $templatename => $notused) { + data_generate_default_template($activity, $templatename); + } + + $preset = preset::create_from_instance($manager, $activity->name); + $result = $preset->export(); + $presetfilenames = array_merge(array_values(manager::TEMPLATES_LIST), ['preset.xml']); + + $ziparchive = new zip_archive(); + $ziparchive->open($result, file_archive::OPEN); + $files = $ziparchive->list_files(); + foreach ($files as $file) { + $this->assertContains($file->pathname, $presetfilenames); + // Check the file is not empty (except CSS, JS and listtemplateheader/footer files which are empty by default). + $ishtmlorxmlfile = str_ends_with($file->pathname, '.html') || str_ends_with($file->pathname, '.xml'); + $islistheader = $file->pathname != manager::TEMPLATES_LIST['listtemplateheader']; + $islistfooter = $file->pathname != manager::TEMPLATES_LIST['listtemplatefooter']; + if ($ishtmlorxmlfile && !$islistheader && !$islistfooter) { + $this->assertGreaterThan(0, $file->size); + } + } + $ziparchive->close(); + } + + /** + * Test for get_userid(). + * + * @covers ::get_userid + */ + public function test_get_userid() { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $activity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); + $user = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); + $this->setUser($user); + + // Check userid is null for plugin preset. + $manager = manager::create_from_instance($activity); + $pluginpresets = $manager->get_available_plugin_presets(); + $pluginpreset = reset($pluginpresets); + $this->assertNull($pluginpreset->get_userid()); + + // Check userid meets the user that has created the preset when it's a saved preset. + $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); + $savedpreset = (object) [ + 'name' => 'Preset created by teacher', + ]; + $plugingenerator->create_preset($activity, $savedpreset); + $savedpresets = $manager->get_available_saved_presets(); + $savedpreset = reset($savedpresets); + $this->assertEquals($user->id, $savedpreset->get_userid()); + + // Check userid is null when preset hasn't any file associated. + $preset = preset::create_from_instance($manager, 'Unexisting preset'); + $this->assertNull($preset->get_userid()); + } + + /** + * Test for get_path(). + * + * @covers ::get_path + */ + public function test_get_path() { + $this->resetAfterTest(); + $this->setAdminUser(); + + $course = $this->getDataGenerator()->create_course(); + $activity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); + + // Check path is null for plugin preset. + $manager = manager::create_from_instance($activity); + $pluginpresets = $manager->get_available_plugin_presets(); + $pluginpreset = reset($pluginpresets); + $this->assertNull($pluginpreset->get_path()); + + // Check path meets expected value when it's a saved preset. + $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); + $savedpreset = (object) [ + 'name' => 'Saved preset', + ]; + $plugingenerator->create_preset($activity, $savedpreset); + $savedpresets = $manager->get_available_saved_presets(); + $savedpreset = reset($savedpresets); + $this->assertEquals("/{$savedpreset->name}/", $savedpreset->get_path()); + + // Check path is /presetname/ when preset hasn't any file associated. + $presetname = 'Unexisting preset'; + $preset = preset::create_from_instance($manager, $presetname); + $this->assertEquals("/{$presetname}/", $preset->get_path()); + } + /** * Test for is_directory_a_preset(). * @@ -86,4 +390,137 @@ class preset_test extends \advanced_testcase { $this->assertEquals($presetshortname, $name); } + /** + * Test for get_description_from_plugin(). + * + * @covers ::get_description_from_plugin + */ + public function test_get_description_from_plugin() { + $this->resetAfterTest(); + $this->setAdminUser(); + + // The expected name for plugins with modulename in lang is this value. + $description = preset::get_description_from_plugin('imagegallery'); + $this->assertEquals('Use this preset to collect images.', $description); + + // However, if the plugin doesn't exist or the modulename is not defined, empty string will be returned. + $presetshortname = 'nonexistingpreset'; + $description = preset::get_description_from_plugin($presetshortname); + $this->assertEmpty($description); + } + + /** + * Test for generate_preset_xml(). + * + * @covers ::generate_preset_xml + * @dataProvider generate_preset_xml_provider + * @param array $params activity config settings + * @param string|null $description preset description + */ + public function test_generate_preset_xml(array $params, ?string $description) { + $this->resetAfterTest(); + $this->setAdminUser(); + + // Make accessible the method. + $reflection = new \ReflectionClass(preset::class); + $method = $reflection->getMethod('generate_preset_xml'); + $method->setAccessible(true); + + // The method should return empty string when trying to generate preset.xml for a plugin preset. + $preset = preset::create_from_plugin(null, 'imagegallery'); + $result = $method->invokeArgs($preset, []); + $this->assertEmpty($result); + + // Create a course and a database activity. + $course = $this->getDataGenerator()->create_course(); + $activity = $this->getDataGenerator()->create_module(manager::MODULE, array_merge(['course' => $course], $params)); + + // Add a field to the activity. + $fieldrecord = new stdClass(); + $fieldrecord->name = 'field-1'; + $fieldrecord->type = 'text'; + $datagenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); + $datagenerator->create_field($fieldrecord, $activity); + + $manager = manager::create_from_instance($activity); + $preset = preset::create_from_instance($manager, $activity->name, $description); + + // Call the generate_preset_xml method. + $result = $method->invokeArgs($preset, []); + // Check is a valid XML. + $parsedxml = simplexml_load_string($result); + // Check the description has the expected value. + $this->assertEquals($description, strval($parsedxml->description)); + // Check settings have the expected values. + foreach ($params as $paramname => $paramvalue) { + $this->assertEquals($paramvalue, strval($parsedxml->settings->{$paramname})); + } + // Check field have the expected values. + $this->assertEquals($fieldrecord->name, strval($parsedxml->field->name)); + $this->assertEquals($fieldrecord->type, strval($parsedxml->field->type)); + } + + /** + * Data provider for generate_preset_xml(). + * + * @return array + */ + public function generate_preset_xml_provider(): array { + return [ + 'Generate preset.xml with the default params and empty description' => [ + 'params' => [], + 'description' => null, + ], + 'Generate preset.xml with a description but the default params' => [ + 'params' => [], + 'description' => 'This is a description', + ], + 'Generate preset.xml with empty description but changing some params' => [ + 'params' => [ + 'requiredentries' => 2, + 'approval' => 1, + ], + 'description' => null, + ], + 'Generate preset.xml with a description and changing some params' => [ + 'params' => [ + 'maxentries' => 5, + 'manageapproved' => 0, + ], + 'description' => 'This is a description', + ], + ]; + } + + /** + * Test for get_file(). + * + * @covers ::get_file + */ + public function test_get_file() { + $this->resetAfterTest(); + $this->setAdminUser(); + + // Create a course and a database activity. + $course = $this->getDataGenerator()->create_course(); + $activity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); + $manager = manager::create_from_instance($activity); + + $presetname = 'Saved preset'; + // Check file doesn't exist if the preset hasn't been saved yet. + $preset = preset::create_from_instance($manager, $presetname); + $file = preset::get_file($preset->get_path(), 'preset.xml'); + $this->assertNull($file); + + // Check file is not empty when there is a saved preset with this name. + $preset->save(); + $file = preset::get_file($preset->get_path(), 'preset.xml'); + $this->assertNotNull($file); + $this->assertStringContainsString($presetname, $file->get_filepath()); + $this->assertEquals('preset.xml', $file->get_filename()); + + // Check invalid preset file name doesn't exist. + $file = preset::get_file($preset->get_path(), 'unexistingpreset.xml'); + $this->assertNull($file); + } } diff --git a/mod/data/upgrade.txt b/mod/data/upgrade.txt index 88ca5336c9b..c7659d1baea 100644 --- a/mod/data/upgrade.txt +++ b/mod/data/upgrade.txt @@ -9,6 +9,9 @@ information provided here is intended especially for developers. - data_get_available_presets - data_get_available_site_presets - data_preset_name + - data_presets_export + - data_presets_generate_xml + - data_presets_save - is_directory_a_preset === 3.7 ===