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 = "
@@ -67,6 +69,7 @@
- {{#str}} name {{/str}}
+ {{#str}} name {{/str}}
+ {{#str}} description {{/str}}
{{#showmanage}}{{#str}} action {{/str}}{{/showmanage}}