From abeae6acb277f4e2c8912f1558cb0199fd91c025 Mon Sep 17 00:00:00 2001 From: Sara Arjona Date: Mon, 11 Jul 2022 18:19:37 +0200 Subject: [PATCH 1/7] MDL-75148 mod_data: Add missing template (adv search) While covering is_directory_a_preset() with PHPUnit tests, it has been identified that there was a missing file in the Image gallery preset. The template will be reviewed in the future (in a separate issue) but, for now, I'm going to add the default template for advanced search. --- mod/data/preset/imagegallery/asearchtemplate.html | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 mod/data/preset/imagegallery/asearchtemplate.html diff --git a/mod/data/preset/imagegallery/asearchtemplate.html b/mod/data/preset/imagegallery/asearchtemplate.html new file mode 100644 index 00000000000..8e807baf3ea --- /dev/null +++ b/mod/data/preset/imagegallery/asearchtemplate.html @@ -0,0 +1,12 @@ +
+ + + + + + + + + +
Title: [[title]]
Caption: [[caption]]
+
From 9d10f7d19eaccff5c0fe1a4215fd91e2537f0c45 Mon Sep 17 00:00:00 2001 From: Sara Arjona Date: Thu, 7 Jul 2022 17:03:07 +0200 Subject: [PATCH 2/7] MDL-75148 mod_data: Add isplugin info to presets This activity currently supports two different preset types: - Datapreset plugins, that can be installed copying them to the mod/data/preset folder. - Presets saved manually by users. This commit adds an attribute to the presets to mark them, in order to identify them later (because, for instance, the plugins can't be removed). Apart from that, the methods in lib.php, involved with this issue, have been deprecated. New methods have been implemented in the manager class, covering them with PHPUnit tests. --- mod/data/classes/form/save_as_preset.php | 10 +- mod/data/classes/manager.php | 78 +++++++++ mod/data/classes/preset.php | 58 +++++++ mod/data/field.php | 12 +- mod/data/lib.php | 105 ++++------- mod/data/preset.php | 27 +-- mod/data/tests/manager_test.php | 212 +++++++++++++++++++++++ mod/data/tests/preset_test.php | 89 ++++++++++ mod/data/upgrade.txt | 5 + 9 files changed, 508 insertions(+), 88 deletions(-) create mode 100644 mod/data/classes/preset.php create mode 100644 mod/data/tests/preset_test.php diff --git a/mod/data/classes/form/save_as_preset.php b/mod/data/classes/form/save_as_preset.php index 13bcd15cfe0..968718eeb66 100644 --- a/mod/data/classes/form/save_as_preset.php +++ b/mod/data/classes/form/save_as_preset.php @@ -20,6 +20,7 @@ use context; use moodle_exception; use moodle_url; use core_form\dynamic_form; +use mod_data\manager; /** * Save database as preset form. @@ -72,9 +73,11 @@ class save_as_preset extends dynamic_form { $errors = parent::validation($formdata, $files); $context = $this->get_context_for_dynamic_submission(); + $cm = get_coursemodule_from_id('', $context->instanceid, 0, false, MUST_EXIST); + $manager = manager::create_from_coursemodule($cm); if (!empty($formdata['overwrite'])) { - $presets = data_get_available_presets($context); + $presets = $manager->get_available_presets(); $selectedpreset = new \stdClass(); foreach ($presets as $preset) { if ($preset->name == $formdata['name']) { @@ -87,7 +90,7 @@ class save_as_preset extends dynamic_form { } } else { // If the preset exists now then we need to throw an error. - $sitepresets = data_get_available_site_presets($context); + $sitepresets = $manager->get_available_saved_presets(); foreach ($sitepresets as $preset) { if ($formdata['name'] == $preset->name) { $errors['name'] = get_string('errorpresetexists', 'data'); @@ -137,7 +140,8 @@ class save_as_preset extends dynamic_form { try { if (!empty($this->get_data()->overwrite)) { - $presets = data_get_available_presets($context); + $manager = manager::create_from_coursemodule($cm); + $presets = $manager->get_available_presets(); $selectedpreset = new \stdClass(); foreach ($presets as $preset) { if ($preset->name == $this->get_data()->name) { diff --git a/mod/data/classes/manager.php b/mod/data/classes/manager.php index ecabc6add7c..3111326ca5f 100644 --- a/mod/data/classes/manager.php +++ b/mod/data/classes/manager.php @@ -23,6 +23,7 @@ use data_field_base; use mod_data\event\course_module_viewed; use mod_data\event\template_viewed; use mod_data\event\template_updated; +use core_component; use stdClass; /** @@ -300,4 +301,81 @@ class manager { return true; } + + /** + * Returns an array of all the available presets. + * + * @return array A list with the datapreset plugins and the presets saved by users. + */ + public function get_available_presets(): array { + // First load the datapreset plugins that exist within the modules preset dir. + $pluginpresets = static::get_available_plugin_presets(); + + // Then find the presets that people have saved. + $savedpresets = static::get_available_saved_presets(); + + return array_merge($pluginpresets, $savedpresets); + } + + /** + * Returns an array of all the presets that users have saved to the site. + * + * @return array A list with the preset saved by the users. + */ + public function get_available_saved_presets(): array { + global $USER; + + $presets = []; + + $fs = get_file_storage(); + $files = $fs->get_area_files(DATA_PRESET_CONTEXT, DATA_PRESET_COMPONENT, DATA_PRESET_FILEAREA); + if (empty($files)) { + return $presets; + } + $canviewall = has_capability('mod/data:viewalluserpresets', $this->get_context()); + foreach ($files as $file) { + $isnotdirectory = ($file->is_directory() && $file->get_filepath() == '/') || !$file->is_directory(); + $userid = $file->get_userid(); + $cannotviewfile = !$canviewall && $userid != $USER->id; + if ($isnotdirectory || $cannotviewfile) { + 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; + $presets[] = $preset; + } + + return $presets; + } + + /** + * Returns an array of all the available plugin presets. + * + * @return array A list with the datapreset plugins. + */ + public static function get_available_plugin_presets(): array { + $presets = []; + + $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); + $presets[] = $preset; + } + } + + return $presets; + } } diff --git a/mod/data/classes/preset.php b/mod/data/classes/preset.php new file mode 100644 index 00000000000..6647766d58b --- /dev/null +++ b/mod/data/classes/preset.php @@ -0,0 +1,58 @@ +. + +namespace mod_data; + +/** + * Class preset for database activity. + * + * @package mod_data + * @copyright 2022 Sara Arjona + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class preset { + + /** + * Checks if a directory contains all the required files to define a preset. + * + * @param string $directory The patch to check if it contains the preset files or not. + * @return bool True if the directory contains all the preset files; false otherwise. + */ + public static function is_directory_a_preset(string $directory): bool { + $status = true; + $directory = rtrim($directory, '/\\') . '/'; + $presetfilenames = array_merge(array_values(manager::TEMPLATES_LIST), ['preset.xml']); + foreach ($presetfilenames as $filename) { + $status &= file_exists($directory.$filename); + } + + return $status; + } + + /** + * Returns the best name to show for a datapreset plugin. + * + * @param string $pluginname The datapreset plugin name. + * @return string The plugin preset name to display. + */ + public static function get_name_from_plugin(string $pluginname): string { + if (get_string_manager()->string_exists('modulename', 'datapreset_'.$pluginname)) { + return get_string('modulename', 'datapreset_'.$pluginname); + } else { + return $pluginname; + } + } +} diff --git a/mod/data/field.php b/mod/data/field.php index 21368cc8e3d..de20d658f86 100644 --- a/mod/data/field.php +++ b/mod/data/field.php @@ -23,6 +23,8 @@ * @package mod_data */ +use mod_data\manager; + require_once('../../config.php'); require_once('lib.php'); require_once($CFG->dirroot.'/mod/data/preset_form.php'); @@ -94,7 +96,8 @@ if ($id) { require_login($course, true, $cm); -$context = context_module::instance($cm->id); +$manager = manager::create_from_coursemodule($cm); +$context = $manager->get_context(); require_capability('mod/data:managetemplates', $context); $formimportzip = new data_import_preset_zip_form(); @@ -315,10 +318,10 @@ switch ($mode) { } } else { echo $OUTPUT->heading(get_string('presets', 'data'), 2, 'mb-4'); - $presets = data_get_available_presets($context); - $presetstable = new \mod_data\output\presets($data->id, $presets, + $presets = $manager->get_available_presets(); + $presetsdata = new \mod_data\output\presets($data->id, $presets, new \moodle_url('/mod/data/field.php')); - echo $renderer->render_presets($presetstable, false); + echo $renderer->render_presets($presetsdata); } echo $OUTPUT->footer(); exit; @@ -463,4 +466,3 @@ if (($mode == 'new') && (!empty($newtype))) { // Adding a new field. /// Finish the page echo $OUTPUT->footer(); - diff --git a/mod/data/lib.php b/mod/data/lib.php index 2bcf4d8f795..f11f58cf6f4 100644 --- a/mod/data/lib.php +++ b/mod/data/lib.php @@ -22,6 +22,7 @@ */ use mod_data\manager; +use mod_data\preset; defined('MOODLE_INTERNAL') || die(); @@ -2030,52 +2031,30 @@ function data_convert_to_roles($data, $teacherroles=array(), $studentroles=array * @param string $shortname * @param string $path * @return string + * @deprecated since Moodle 4.1 MDL-75148 - please, use the preset::get_name_from_plugin() function instead. + * @todo MDL-75189 This will be deleted in Moodle 4.5. + * @see preset::get_name_from_plugin() */ function data_preset_name($shortname, $path) { + debugging('data_preset_name() is deprecated. Please use preset::get_name_from_plugin() instead.', DEBUG_DEVELOPER); - // We are looking inside the preset itself as a first choice, but also in normal data directory - $string = get_string('modulename', 'datapreset_'.$shortname); - - if (substr($string, 0, 1) == '[') { - return $shortname; - } else { - return $string; - } + return preset::get_name_from_plugin($shortname); } /** * Returns an array of all the available presets. * * @return array + * @deprecated since Moodle 4.1 MDL-75148 - please, use the manager::get_available_presets() function instead. + * @todo MDL-75189 This will be deleted in Moodle 4.5. + * @see manager::get_available_presets() */ function data_get_available_presets($context) { - global $CFG, $USER; + debugging('data_get_available_presets() is deprecated. Please use manager::get_available_presets() instead.', DEBUG_DEVELOPER); - $presets = array(); - - // First load the ratings sub plugins that exist within the modules preset dir - if ($dirs = core_component::get_plugin_list('datapreset')) { - foreach ($dirs as $dir=>$fulldir) { - if (is_directory_a_preset($fulldir)) { - $preset = new stdClass(); - $preset->path = $fulldir; - $preset->userid = 0; - $preset->shortname = $dir; - $preset->name = data_preset_name($dir, $fulldir); - if (file_exists($fulldir.'/screenshot.jpg')) { - $preset->screenshot = $CFG->wwwroot.'/mod/data/preset/'.$dir.'/screenshot.jpg'; - } else if (file_exists($fulldir.'/screenshot.png')) { - $preset->screenshot = $CFG->wwwroot.'/mod/data/preset/'.$dir.'/screenshot.png'; - } else if (file_exists($fulldir.'/screenshot.gif')) { - $preset->screenshot = $CFG->wwwroot.'/mod/data/preset/'.$dir.'/screenshot.gif'; - } - $presets[] = $preset; - } - } - } - // Now add to that the site presets that people have saved - $presets = data_get_available_site_presets($context, $presets); - return $presets; + $cm = get_coursemodule_from_id('', $context->instanceid, 0, false, MUST_EXIST); + $manager = manager::create_from_coursemodule($cm); + return $manager->get_available_presets(); } /** @@ -2084,30 +2063,20 @@ function data_get_available_presets($context) { * @param stdClass $context The context that we are looking from. * @param array $presets * @return array An array of presets + * @deprecated since Moodle 4.1 MDL-75148 - please, use the manager::get_available_saved_presets() function instead. + * @todo MDL-75189 This will be deleted in Moodle 4.5. + * @see manager::get_available_saved_presets() */ function data_get_available_site_presets($context, array $presets=array()) { - global $USER; + debugging( + 'data_get_available_site_presets() is deprecated. Please use manager::get_available_saved_presets() instead.', + DEBUG_DEVELOPER + ); - $fs = get_file_storage(); - $files = $fs->get_area_files(DATA_PRESET_CONTEXT, DATA_PRESET_COMPONENT, DATA_PRESET_FILEAREA); - $canviewall = has_capability('mod/data:viewalluserpresets', $context); - if (empty($files)) { - return $presets; - } - foreach ($files as $file) { - if (($file->is_directory() && $file->get_filepath()=='/') || !$file->is_directory() || (!$canviewall && $file->get_userid() != $USER->id)) { - continue; - } - $preset = new stdClass; - $preset->path = $file->get_filepath(); - $preset->name = trim($preset->path, '/'); - $preset->shortname = $preset->name; - $preset->userid = $file->get_userid(); - $preset->id = $file->get_id(); - $preset->storedfile = $file; - $presets[] = $preset; - } - return $presets; + $cm = get_coursemodule_from_id('', $context->instanceid, 0, false, MUST_EXIST); + $manager = manager::create_from_coursemodule($cm); + $savedpresets = $manager->get_available_saved_presets(); + return array_merge($presets, $savedpresets); } /** @@ -2261,22 +2230,18 @@ function data_in_readonly_period($data) { } /** - * @return bool + * Check if the files in a directory are the expected for a preset. + * + * @return bool Wheter the defined $directory has or not all the expected preset files. + * + * @deprecated since Moodle 4.1 MDL-75148 - please, use the preset::is_directory_a_preset() function instead. + * @todo MDL-75189 This will be deleted in Moodle 4.5. + * @see manager::is_directory_a_preset() */ function is_directory_a_preset($directory) { - $directory = rtrim($directory, '/\\') . '/'; - $status = file_exists($directory.'singletemplate.html') && - file_exists($directory.'listtemplate.html') && - file_exists($directory.'listtemplateheader.html') && - file_exists($directory.'listtemplatefooter.html') && - file_exists($directory.'addtemplate.html') && - file_exists($directory.'rsstemplate.html') && - file_exists($directory.'rsstitletemplate.html') && - file_exists($directory.'csstemplate.css') && - file_exists($directory.'jstemplate.js') && - file_exists($directory.'preset.xml'); + debugging('is_directory_a_preset() is deprecated. Please use preset::is_directory_a_preset() instead.', DEBUG_DEVELOPER); - return $status; + return preset::is_directory_a_preset($directory); } /** @@ -2350,7 +2315,7 @@ abstract class data_preset_importer { require_once($CFG->libdir.'/xmlize.php'); $fs = $fileobj = null; - if (!is_directory_a_preset($this->directory)) { + if (!preset::is_directory_a_preset($this->directory)) { //maybe the user requested a preset stored in the Moodle file storage $fs = get_file_storage(); @@ -3650,7 +3615,7 @@ function data_presets_export($course, $cm, $data, $tostorage=false) { fclose($asearchtemplate); // Check if all files have been generated - if (! is_directory_a_preset($exportdir)) { + if (! preset::is_directory_a_preset($exportdir)) { throw new \moodle_exception('generateerror', 'data'); } diff --git a/mod/data/preset.php b/mod/data/preset.php index dcdb3a2f93d..e10370c71a2 100644 --- a/mod/data/preset.php +++ b/mod/data/preset.php @@ -28,21 +28,27 @@ * @package mod_data */ +use mod_data\manager; + require_once('../../config.php'); require_once($CFG->dirroot.'/mod/data/lib.php'); require_once($CFG->dirroot.'/mod/data/preset_form.php'); -$id = optional_param('id', 0, PARAM_INT); // The course module id. +// The course module id. +$id = optional_param('id', 0, PARAM_INT); +$manager = null; if ($id) { - $cm = get_coursemodule_from_id('data', $id, null, null, MUST_EXIST); - $course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST); - $data = $DB->get_record('data', array('id' => $cm->instance), '*', MUST_EXIST); + list($course, $cm) = get_course_and_cm_from_cmid($id, manager::MODULE); + $manager = manager::create_from_coursemodule($cm); + $data = $manager->get_instance(); } else { - $d = required_param('d', PARAM_INT); // database activity id - $data = $DB->get_record('data', array('id' => $d), '*', MUST_EXIST); - $course = $DB->get_record('course', array('id' => $data->course), '*', MUST_EXIST); - $cm = get_coursemodule_from_instance('data', $data->id, $course->id, null, MUST_EXIST); + // We must have the database activity id. + $d = required_param('d', PARAM_INT); + $data = $DB->get_record('data', ['id' => $d], '*', MUST_EXIST); + $manager = manager::create_from_instance($data); + $cm = $manager->get_coursemodule(); + $course = get_course($cm->course); } $action = optional_param('action', 'view', PARAM_ALPHA); // The page action. @@ -52,7 +58,8 @@ if (!in_array($action, $allowedactions)) { throw new moodle_exception('invalidaccess'); } -$context = context_module::instance($cm->id, MUST_EXIST); +$context = $manager->get_context(); + require_login($course, false, $cm); require_capability('mod/data:managetemplates', $context); @@ -70,7 +77,7 @@ $data->cmidnumber = $cm->idnumber; $data->instance = $cm->instance; $renderer = $PAGE->get_renderer('mod_data'); -$presets = data_get_available_presets($context); +$presets = $manager->get_available_presets(); if ($action === 'export') { if (headers_sent()) { diff --git a/mod/data/tests/manager_test.php b/mod/data/tests/manager_test.php index 9f5dd7e43f4..55c3e34fc2d 100644 --- a/mod/data/tests/manager_test.php +++ b/mod/data/tests/manager_test.php @@ -18,6 +18,7 @@ namespace mod_data; use context_module; use moodle_url; +use core_component; /** * Manager tests class for mod_data. @@ -161,4 +162,215 @@ class manager_test extends \advanced_testcase { $this->assertEventContextNotUsed($event); $this->assertNotEmpty($event->get_name()); } + + /** + * Test for get_available_presets(). + * + * @covers ::get_available_presets + */ + public function test_get_available_presets() { + global $DB; + + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $user = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); + $this->setUser($user); + + $activity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); + $cm = get_coursemodule_from_id(manager::MODULE, $activity->cmid, 0, false, MUST_EXIST); + + // Check available presets meet the datapreset plugins when there are no any preset saved by users. + $datapresetplugins = core_component::get_plugin_list('datapreset'); + $manager = manager::create_from_coursemodule($cm); + $presets = $manager->get_available_presets(); + $this->assertCount(count($datapresetplugins), $presets); + // Confirm that, at least, the "Image gallery" is one of them. + $namepresets = array_map(function($preset) { + return $preset->name; + }, $presets); + $this->assertContains('Image gallery', $namepresets); + + // Login as admin and create some presets saved manually by users. + $this->setAdminUser(); + $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); + $savedpresets = []; + for ($i = 1; $i <= 3; $i++) { + $preset = (object) [ + 'name' => 'Preset name ' . $i, + ]; + $plugingenerator->create_preset($activity, $preset); + $savedpresets[] = $preset; + } + $savedpresetsnames = array_map(function($preset) { + return $preset->name; + }, $savedpresets); + $this->setUser($user); + + // Check available presets meet the datapreset plugins + presets saved manually by users. + $presets = $manager->get_available_presets(); + $this->assertCount(count($datapresetplugins) + count($savedpresets), $presets); + // Confirm that, apart from the "Image gallery" preset, the ones created manually have been also returned. + $namepresets = array_map(function($preset) { + return $preset->name; + }, $presets); + $this->assertContains('Image gallery', $namepresets); + foreach ($savedpresets as $savedpreset) { + $this->assertContains($savedpreset->name, $namepresets); + } + // Check all the presets have the proper value for the isplugin attribute. + foreach ($presets as $preset) { + if (in_array($preset->name, $savedpresetsnames)) { + $this->assertFalse($preset->isplugin); + } else { + $this->assertTrue($preset->isplugin); + } + } + + // Unassign the capability to the teacher role and check that only plugin presets are returned (because the saved presets + // have been created by admin). + $teacherrole = $DB->get_record('role', ['shortname' => 'teacher']); + unassign_capability('mod/data:viewalluserpresets', $teacherrole->id); + $presets = $manager->get_available_presets(); + $this->assertCount(count($datapresetplugins), $presets); + // Confirm that, at least, the "Image gallery" is one of them. + $namepresets = array_map(function($preset) { + return $preset->name; + }, $presets); + $this->assertContains('Image gallery', $namepresets); + foreach ($savedpresets as $savedpreset) { + $this->assertNotContains($savedpreset->name, $namepresets); + } + + // Create a preset with the current user and check that, although the viewalluserpresets is not assigned to the teacher + // role, the preset is returned because the teacher is the owner. + $savedpreset = (object) [ + 'name' => 'Preset created by teacher', + ]; + $plugingenerator->create_preset($activity, $savedpreset); + $presets = $manager->get_available_presets(); + // The presets total is all the plugin presets plus the preset created by the teacher. + $this->assertCount(count($datapresetplugins) + 1, $presets); + // Confirm that, at least, the "Image gallery" is one of them. + $namepresets = array_map(function($preset) { + return $preset->name; + }, $presets); + $this->assertContains('Image gallery', $namepresets); + // Confirm that savedpresets are still not returned. + foreach ($savedpresets as $savedpreset) { + $this->assertNotContains($savedpreset->name, $namepresets); + } + // Confirm the new preset created by the teacher is returned too. + $this->assertContains('Preset created by teacher', $namepresets); + } + + /** + * Test for get_available_plugin_presets(). + * + * @covers ::get_available_plugin_presets + */ + public function test_get_available_plugin_presets() { + $this->resetAfterTest(); + $this->setAdminUser(); + + $course = $this->getDataGenerator()->create_course(); + $activity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); + + // Check available plugin presets meet the datapreset plugins. + $datapresetplugins = core_component::get_plugin_list('datapreset'); + $manager = manager::create_from_instance($activity); + $presets = $manager->get_available_plugin_presets(); + $this->assertCount(count($datapresetplugins), $presets); + // Confirm that, at least, the "Image gallery" is one of them. + $namepresets = array_map(function($preset) { + return $preset->name; + }, $presets); + $this->assertContains('Image gallery', $namepresets); + + // Create a preset saved manually by users. + $savedpreset = (object) [ + 'name' => 'Preset name 1', + ]; + $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); + $plugingenerator->create_preset($activity, $savedpreset); + + // Check available plugin presets don't contain the preset saved manually. + $presets = $manager->get_available_plugin_presets(); + $this->assertCount(count($datapresetplugins), $presets); + // Confirm that, at least, the "Image gallery" is one of them. + $namepresets = array_map(function($preset) { + return $preset->name; + }, $presets); + $this->assertContains('Image gallery', $namepresets); + // Confirm that the preset saved manually hasn't been returned. + $this->assertNotContains($savedpreset->name, $namepresets); + // Check all the presets have the proper value for the isplugin attribute. + foreach ($presets as $preset) { + $this->assertTrue($preset->isplugin); + } + } + + /** + * Test for get_available_saved_presets(). + * + * @covers ::get_available_saved_presets + */ + public function test_get_available_saved_presets() { + global $DB; + + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $user = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); + $this->setUser($user); + + $activity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); + $cm = get_coursemodule_from_id(manager::MODULE, $activity->cmid, 0, false, MUST_EXIST); + + // Check available saved presets is empty (because, for now, no user preset has been created). + $manager = manager::create_from_coursemodule($cm); + $presets = $manager->get_available_saved_presets(); + $this->assertCount(0, $presets); + + // Create some presets saved manually by the admin user. + $this->setAdminUser(); + $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); + $savedpresets = []; + for ($i = 1; $i <= 3; $i++) { + $preset = (object) [ + 'name' => 'Preset name ' . $i, + ]; + $plugingenerator->create_preset($activity, $preset); + $savedpresets[] = $preset; + } + // Create one more preset saved manually by the teacher user. + $this->setUser($user); + $teacherpreset = (object) [ + 'name' => 'Preset created by teacher', + ]; + $plugingenerator->create_preset($activity, $teacherpreset); + $savedpresets[] = $teacherpreset; + + $savedpresetsnames = array_map(function($preset) { + return $preset->name; + }, $savedpresets); + + // Check available saved presets only contain presets saved manually by users. + $presets = $manager->get_available_saved_presets(); + $this->assertCount(count($savedpresets), $presets); + // Confirm that it contains only the presets created manually. + foreach ($presets as $preset) { + $this->assertContains($preset->name, $savedpresetsnames); + $this->assertFalse($preset->isplugin); + } + + // Unassign the mod/data:viewalluserpresets capability to the teacher role and check that saved presets are not returned. + $teacherrole = $DB->get_record('role', ['shortname' => 'teacher']); + unassign_capability('mod/data:viewalluserpresets', $teacherrole->id); + + $presets = $manager->get_available_saved_presets(); + $this->assertCount(1, $presets); + $preset = reset($presets); + $this->assertEquals($teacherpreset->name, $preset->name); + } } diff --git a/mod/data/tests/preset_test.php b/mod/data/tests/preset_test.php new file mode 100644 index 00000000000..3c07c7e5ebc --- /dev/null +++ b/mod/data/tests/preset_test.php @@ -0,0 +1,89 @@ +. + +namespace mod_data; + +/** + * Preset tests class for mod_data. + * + * @package mod_data + * @category test + * @copyright 2022 Sara Arjona + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \mod_data\preset + */ +class preset_test extends \advanced_testcase { + + /** + * Test for is_directory_a_preset(). + * + * @dataProvider is_directory_a_preset_provider + * @covers ::is_directory_a_preset + * @param string $directory + * @param bool $expected + */ + public function test_is_directory_a_preset(string $directory, bool $expected): void { + $this->resetAfterTest(); + $this->setAdminUser(); + + $result = preset::is_directory_a_preset($directory); + $this->assertEquals($expected, $result); + } + + /** + * Data provider for test_is_directory_a_preset(). + * + * @return array + */ + public function is_directory_a_preset_provider(): array { + global $CFG; + + return [ + 'Valid preset directory' => [ + 'directory' => $CFG->dirroot . '/mod/data/preset/imagegallery', + 'expected' => true, + ], + 'Invalid preset directory' => [ + 'directory' => $CFG->dirroot . '/mod/data/field/checkbox', + 'expected' => false, + ], + 'Unexisting preset directory' => [ + 'directory' => $CFG->dirroot . 'unexistingdirectory', + 'expected' => false, + ], + ]; + } + + /** + * Test for get_name_from_plugin(). + * + * @covers ::get_name_from_plugin + */ + public function test_get_name_from_plugin() { + $this->resetAfterTest(); + $this->setAdminUser(); + + // The expected name for plugins with modulename in lang is this value. + $name = preset::get_name_from_plugin('imagegallery'); + $this->assertEquals('Image gallery', $name); + + // However, if the plugin doesn't exist or the modulename is not defined, the preset shortname will be returned. + $presetshortname = 'nonexistingpreset'; + $name = preset::get_name_from_plugin($presetshortname); + $this->assertEquals($presetshortname, $name); + } + +} diff --git a/mod/data/upgrade.txt b/mod/data/upgrade.txt index 95ad4c8f212..88ca5336c9b 100644 --- a/mod/data/upgrade.txt +++ b/mod/data/upgrade.txt @@ -5,6 +5,11 @@ information provided here is intended especially for developers. * The method data_view is now deprecated. Use $maganer->set_module_viewed instead. * The data_print_template function is now deprecated and replaced by mod_data\template class. * The data_print_ratings function now has an extra $print to get the ratings output instead of printing it directly. +* The following functions have been deprecated because they have been moved to the manager class: + - data_get_available_presets + - data_get_available_site_presets + - data_preset_name + - is_directory_a_preset === 3.7 === * External functions get_entries, get_entry and search_entries now return an additional field "tags" containing the entry tags. From 13264c71079260a61e934941f27964b2f5d70efe Mon Sep 17 00:00:00 2001 From: Sara Arjona Date: Mon, 11 Jul 2022 18:25:21 +0200 Subject: [PATCH 3/7] MDL-75148 mod_data: Add create_preset to generator --- mod/data/tests/generator/lib.php | 26 +++++++++++++++++++ mod/data/tests/generator_test.php | 43 +++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/mod/data/tests/generator/lib.php b/mod/data/tests/generator/lib.php index 719552dfa24..8ed2ea440ca 100644 --- a/mod/data/tests/generator/lib.php +++ b/mod/data/tests/generator/lib.php @@ -23,6 +23,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +use mod_data\manager; + defined('MOODLE_INTERNAL') || die(); @@ -361,4 +363,28 @@ class mod_data_generator extends testing_module_generator { return $recordid; } + + /** + * Creates a preset from a mod_data instance. + * + * @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. + */ + public function create_preset(stdClass $instance, stdClass $record = null): bool { + global $DB; + + if (is_null($record)) { + $record = new stdClass(); + } + + // Fill in optional values if not specified. + if (!isset($record->name)) { + $record->name = 'New preset ' . microtime(); + } + + $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); + } } diff --git a/mod/data/tests/generator_test.php b/mod/data/tests/generator_test.php index 2b09fbc1c52..b7e78d9a40c 100644 --- a/mod/data/tests/generator_test.php +++ b/mod/data/tests/generator_test.php @@ -23,6 +23,7 @@ namespace mod_data; * @category phpunit * @copyright 2012 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \mod_data_generator */ class generator_test extends \advanced_testcase { public function test_generator() { @@ -229,4 +230,46 @@ class generator_test extends \advanced_testcase { $this->assertEquals(array('Cats', 'mice'), array_values(\core_tag_tag::get_item_tags_array('mod_data', 'data_records', $datarecordid))); } + + /** + * Test for create_preset(). + * + * @covers ::create_preset + */ + public function test_create_preset() { + $this->resetAfterTest(); + $this->setAdminUser(); + + $course = $this->getDataGenerator()->create_course(); + $activity = $this->getDataGenerator()->create_module(manager::MODULE, ['course' => $course]); + $cm = get_coursemodule_from_id(manager::MODULE, $activity->cmid, 0, false, MUST_EXIST); + + // Check initially there are no saved presets. + $manager = manager::create_from_coursemodule($cm); + $savedpresets = $manager->get_available_saved_presets(); + $this->assertEmpty($savedpresets); + + // Create one preset with the default configuration. + $plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_data'); + $result = $plugingenerator->create_preset($activity); + $this->assertTrue($result); + $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); + } + } + } } From ad0595b4b4e446ac66997767c81480b69dc19b48 Mon Sep 17 00:00:00 2001 From: Sara Arjona Date: Mon, 11 Jul 2022 18:26:23 +0200 Subject: [PATCH 4/7] MDL-75148 mod_data: Implement preset behat generator --- .../generator/behat_mod_data_generator.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/mod/data/tests/generator/behat_mod_data_generator.php b/mod/data/tests/generator/behat_mod_data_generator.php index 1ae6097238d..8f39d2d02c5 100644 --- a/mod/data/tests/generator/behat_mod_data_generator.php +++ b/mod/data/tests/generator/behat_mod_data_generator.php @@ -49,6 +49,12 @@ class behat_mod_data_generator extends behat_generator_base { 'required' => ['database', 'name'], 'switchids' => ['database' => 'databaseid'], ], + 'presets' => [ + 'singular' => 'preset', + 'datagenerator' => 'preset', + 'required' => ['database', 'name'], + 'switchids' => ['database' => 'databaseid'], + ], ]; } @@ -124,6 +130,19 @@ class behat_mod_data_generator extends behat_generator_base { } } + /** + * Saves a preset. + * + * @param array $data Preset data. + */ + protected function process_preset(array $data): void { + global $DB; + + $instance = $DB->get_record('data', ['id' => $data['databaseid']], '*', MUST_EXIST); + + $this->get_data_generator()->create_preset($instance, (object) $data); + } + /** * Get the module data generator. * From 7d88bc07541a4c25c897e556e2a7c1ae515c1818 Mon Sep 17 00:00:00 2001 From: Sara Arjona Date: Thu, 7 Jul 2022 17:00:40 +0200 Subject: [PATCH 5/7] MDL-75148 mod_data: Move Import button to right This commit moves the Import button from the tertiary navigation to the right. --- mod/data/templates/presets_action_bar.mustache | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mod/data/templates/presets_action_bar.mustache b/mod/data/templates/presets_action_bar.mustache index 6511ecde826..f7a93a979f8 100644 --- a/mod/data/templates/presets_action_bar.mustache +++ b/mod/data/templates/presets_action_bar.mustache @@ -25,7 +25,7 @@ } }}
-
+
From 06b86fdfc343bc5fbd728bc8d1ad371d848f05c4 Mon Sep 17 00:00:00 2001 From: Sara Arjona Date: Thu, 7 Jul 2022 17:08:15 +0200 Subject: [PATCH 6/7] MDL-75148 mod_data: Enable Use preset button when one is selected The 'Use preset' button will be enabled only when a preset is selected. --- mod/data/amd/build/selectpreset.min.js | 5 +- mod/data/amd/build/selectpreset.min.js.map | 2 +- mod/data/amd/src/selectpreset.js | 55 ++++++++++++++-------- 3 files changed, 40 insertions(+), 22 deletions(-) diff --git a/mod/data/amd/build/selectpreset.min.js b/mod/data/amd/build/selectpreset.min.js index 87f886a27b4..9e64d02194d 100644 --- a/mod/data/amd/build/selectpreset.min.js +++ b/mod/data/amd/build/selectpreset.min.js @@ -1,10 +1,11 @@ -define("mod_data/selectpreset",["exports","core/notification","core/str"],(function(_exports,_notification,_str){var obj; +define("mod_data/selectpreset",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0; /** * Javascript module to control the form responsible for selecting a preset. * * @module mod_data/selectpreset * @copyright 2021 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_notification=(obj=_notification)&&obj.__esModule?obj:{default:obj};const selectors_selectPresetButton='input[name="selectpreset"]',selectors_selectedPresetRadioButton='input[name="fullname"]:checked';_exports.init=()=>{document.querySelector(selectors_selectPresetButton).addEventListener("click",(event=>{if(event.preventDefault(),document.querySelectorAll(selectors_selectedPresetRadioButton).length>0){event.target.closest("form").submit()}else(0,_str.get_string)("presetnotselected","mod_data").then((str=>_notification.default.addNotification({type:"error",message:str}))).catch(_notification.default.exception)}))}})); + */ +const selectors_presetRadioButton='input[name="fullname"]',selectors_selectPresetButton='input[name="selectpreset"]',selectors_selectedPresetRadioButton='input[name="fullname"]:checked';_exports.init=()=>{const radioButton=document.querySelectorAll(selectors_presetRadioButton);disableUsePresetButton(),radioButton.forEach((elem=>{elem.addEventListener("change",(function(event){event.preventDefault(),disableUsePresetButton()}))}))};const disableUsePresetButton=()=>{let selectPresetButton=document.querySelector(selectors_selectPresetButton);document.querySelectorAll(selectors_selectedPresetRadioButton).length>0?(selectPresetButton.removeAttribute("disabled"),selectPresetButton.classList.remove("btn-secondary"),selectPresetButton.classList.add("btn-primary")):(selectPresetButton.setAttribute("disabled",!0),selectPresetButton.classList.remove("btn-primary"),selectPresetButton.classList.add("btn-secondary"))}})); //# sourceMappingURL=selectpreset.min.js.map \ No newline at end of file diff --git a/mod/data/amd/build/selectpreset.min.js.map b/mod/data/amd/build/selectpreset.min.js.map index 2c640a2e26d..6ec272a58b0 100644 --- a/mod/data/amd/build/selectpreset.min.js.map +++ b/mod/data/amd/build/selectpreset.min.js.map @@ -1 +1 @@ -{"version":3,"file":"selectpreset.min.js","sources":["../src/selectpreset.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Javascript module to control the form responsible for selecting a preset.\n *\n * @module mod_data/selectpreset\n * @copyright 2021 Mihail Geshoski \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Notification from 'core/notification';\nimport {get_string as getString} from 'core/str';\n\nconst selectors = {\n selectPresetButton: 'input[name=\"selectpreset\"]',\n selectedPresetRadioButton: 'input[name=\"fullname\"]:checked',\n};\n\n/**\n * Initialize module.\n */\nexport const init = () => {\n const selectPresetButton = document.querySelector(selectors.selectPresetButton);\n\n selectPresetButton.addEventListener('click', event => {\n event.preventDefault();\n // Validate whether there is a selected preset before submitting the form.\n if (document.querySelectorAll(selectors.selectedPresetRadioButton).length > 0) {\n const presetsForm = event.target.closest('form');\n presetsForm.submit();\n } else {\n // No selected presets. Display an error message to user.\n getString('presetnotselected', 'mod_data').then((str) => {\n return Notification.addNotification({\n type: 'error',\n message: str\n });\n }).catch(Notification.exception);\n }\n });\n};\n"],"names":["selectors","document","querySelector","addEventListener","event","preventDefault","querySelectorAll","length","target","closest","submit","then","str","Notification","addNotification","type","message","catch","exception"],"mappings":";;;;;;;4JA0BMA,6BACkB,6BADlBA,oCAEyB,+CAMX,KACWC,SAASC,cAAcF,8BAE/BG,iBAAiB,SAASC,WACzCA,MAAMC,iBAEFJ,SAASK,iBAAiBN,qCAAqCO,OAAS,EAAG,CACvDH,MAAMI,OAAOC,QAAQ,QAC7BC,iCAGF,oBAAqB,YAAYC,MAAMC,KACtCC,sBAAaC,gBAAgB,CAChCC,KAAM,QACNC,QAASJ,QAEdK,MAAMJ,sBAAaK"} \ No newline at end of file +{"version":3,"file":"selectpreset.min.js","sources":["../src/selectpreset.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Javascript module to control the form responsible for selecting a preset.\n *\n * @module mod_data/selectpreset\n * @copyright 2021 Mihail Geshoski \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nconst selectors = {\n presetRadioButton: 'input[name=\"fullname\"]',\n selectPresetButton: 'input[name=\"selectpreset\"]',\n selectedPresetRadioButton: 'input[name=\"fullname\"]:checked',\n};\n\n/**\n * Initialize module.\n */\nexport const init = () => {\n const radioButton = document.querySelectorAll(selectors.presetRadioButton);\n\n // Initialize the \"Use preset\" button properly.\n disableUsePresetButton();\n\n radioButton.forEach((elem) => {\n elem.addEventListener('change', function(event) {\n event.preventDefault();\n // Enable the \"Use preset\" button when any of the radio buttons in the presets list is checked.\n disableUsePresetButton();\n });\n });\n\n};\n\n/**\n * Decide whether to disable or not the \"Use preset\" button.\n * When there is no preset selected, the button should be displayed disabled; otherwise, it will appear enabled as a primary button.\n *\n * @method\n * @private\n */\nconst disableUsePresetButton = () => {\n let selectPresetButton = document.querySelector(selectors.selectPresetButton);\n const selectedRadioButton = document.querySelectorAll(selectors.selectedPresetRadioButton);\n\n if (selectedRadioButton.length > 0) {\n // There is one preset selected, so the button should be enabled.\n selectPresetButton.removeAttribute('disabled');\n selectPresetButton.classList.remove('btn-secondary');\n selectPresetButton.classList.add('btn-primary');\n } else {\n // There is no any preset selected, so the button should be disabled.\n selectPresetButton.setAttribute('disabled', true);\n selectPresetButton.classList.remove('btn-primary');\n selectPresetButton.classList.add('btn-secondary');\n }\n};\n"],"names":["selectors","radioButton","document","querySelectorAll","disableUsePresetButton","forEach","elem","addEventListener","event","preventDefault","selectPresetButton","querySelector","length","removeAttribute","classList","remove","add","setAttribute"],"mappings":";;;;;;;;MAuBMA,4BACiB,yBADjBA,6BAEkB,6BAFlBA,oCAGyB,+CAMX,WACVC,YAAcC,SAASC,iBAAiBH,6BAG9CI,yBAEAH,YAAYI,SAASC,OACjBA,KAAKC,iBAAiB,UAAU,SAASC,OACrCA,MAAMC,iBAENL,sCAaNA,uBAAyB,SACvBM,mBAAqBR,SAASS,cAAcX,8BACpBE,SAASC,iBAAiBH,qCAE9BY,OAAS,GAE7BF,mBAAmBG,gBAAgB,YACnCH,mBAAmBI,UAAUC,OAAO,iBACpCL,mBAAmBI,UAAUE,IAAI,iBAGjCN,mBAAmBO,aAAa,YAAY,GAC5CP,mBAAmBI,UAAUC,OAAO,eACpCL,mBAAmBI,UAAUE,IAAI"} \ No newline at end of file diff --git a/mod/data/amd/src/selectpreset.js b/mod/data/amd/src/selectpreset.js index 3a5a1487974..1ac23b10959 100644 --- a/mod/data/amd/src/selectpreset.js +++ b/mod/data/amd/src/selectpreset.js @@ -21,10 +21,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -import Notification from 'core/notification'; -import {get_string as getString} from 'core/str'; - const selectors = { + presetRadioButton: 'input[name="fullname"]', selectPresetButton: 'input[name="selectpreset"]', selectedPresetRadioButton: 'input[name="fullname"]:checked', }; @@ -33,22 +31,41 @@ const selectors = { * Initialize module. */ export const init = () => { - const selectPresetButton = document.querySelector(selectors.selectPresetButton); + const radioButton = document.querySelectorAll(selectors.presetRadioButton); - selectPresetButton.addEventListener('click', event => { - event.preventDefault(); - // Validate whether there is a selected preset before submitting the form. - if (document.querySelectorAll(selectors.selectedPresetRadioButton).length > 0) { - const presetsForm = event.target.closest('form'); - presetsForm.submit(); - } else { - // No selected presets. Display an error message to user. - getString('presetnotselected', 'mod_data').then((str) => { - return Notification.addNotification({ - type: 'error', - message: str - }); - }).catch(Notification.exception); - } + // Initialize the "Use preset" button properly. + disableUsePresetButton(); + + radioButton.forEach((elem) => { + elem.addEventListener('change', function(event) { + event.preventDefault(); + // Enable the "Use preset" button when any of the radio buttons in the presets list is checked. + disableUsePresetButton(); + }); }); + +}; + +/** + * Decide whether to disable or not the "Use preset" button. + * When there is no preset selected, the button should be displayed disabled; otherwise, it will appear enabled as a primary button. + * + * @method + * @private + */ +const disableUsePresetButton = () => { + let selectPresetButton = document.querySelector(selectors.selectPresetButton); + const selectedRadioButton = document.querySelectorAll(selectors.selectedPresetRadioButton); + + if (selectedRadioButton.length > 0) { + // There is one preset selected, so the button should be enabled. + selectPresetButton.removeAttribute('disabled'); + selectPresetButton.classList.remove('btn-secondary'); + selectPresetButton.classList.add('btn-primary'); + } else { + // There is no any preset selected, so the button should be disabled. + selectPresetButton.setAttribute('disabled', true); + selectPresetButton.classList.remove('btn-primary'); + selectPresetButton.classList.add('btn-secondary'); + } }; From 599aca679b25a7dd9a0e910ded4d0533a11c7ebf Mon Sep 17 00:00:00 2001 From: Sara Arjona Date: Thu, 7 Jul 2022 17:16:33 +0200 Subject: [PATCH 7/7] MDL-75148 mod_data: Improve presets list page - Add a new help text. - Add captions to table columns. - Move action icons to action menu, and fix code to display the remove action only for presets created by users. - CSS improvements to fit the prototype. In order to achive the previous points, the renderer has been improved, to return the data and let the mustache to print it properly (instead of returning the formatted HTML table). --- mod/data/classes/output/presets.php | 57 ++++++++++------ mod/data/lang/en/data.php | 1 + mod/data/styles.css | 4 ++ mod/data/templates/presets.mustache | 55 +++++++++++++-- mod/data/tests/behat/data_presets.feature | 81 +++++++++++++++++++++++ 5 files changed, 174 insertions(+), 24 deletions(-) create mode 100644 mod/data/tests/behat/data_presets.feature diff --git a/mod/data/classes/output/presets.php b/mod/data/classes/output/presets.php index 2e0cf4de5cd..7c1c6618b74 100644 --- a/mod/data/classes/output/presets.php +++ b/mod/data/classes/output/presets.php @@ -16,6 +16,8 @@ namespace mod_data\output; +use action_menu; +use action_menu_link_secondary; use moodle_url; use templatable; use renderable; @@ -64,54 +66,69 @@ class presets implements templatable, renderable { */ public function export_for_template(\renderer_base $output): array { + $presets = $this->get_presets(); return [ 'd' => $this->id, 'formactionul' => $this->formactionurl->out(), - 'presetstable' => $this->get_presets_table(), + 'showmanage' => $this->manage, + 'presets' => $presets, ]; } /** - * Generates and returns the HTML for the presets table. + * Returns the presets list with the information required to display them. * - * @return string + * @return array Presets list. */ - private function get_presets_table(): string { - global $OUTPUT, $PAGE, $DB; - - $presetstable = new \html_table(); - $presetstable->align = ['center', 'left', 'left']; - $presetstable->size = ['1%', '90%', '1%']; + private function get_presets(): array { + global $OUTPUT, $PAGE; + $presets = []; foreach ($this->presets as $preset) { $presetname = $preset->name; if (!empty($preset->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; - $presetuser = $DB->get_record('user', array('id' => $preset->userid), 'id, ' . $namefields, MUST_EXIST); + $fields = 'id, ' . $namefields; + $presetuser = \core_user::get_user($preset->userid, $fields, MUST_EXIST); $username = fullname($presetuser, true); $presetname = "{$presetname} ({$username})"; } - $deleteaction = ''; + $actions = []; if ($this->manage) { - if (data_user_can_delete_preset($PAGE->context, $preset) && $preset->name != 'Image gallery') { + // 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}", 'action' => 'confirmdelete']); - $deleteaction = $OUTPUT->action_icon($deleteactionurl, - new \pix_icon('t/delete', get_string('delete'))); + + $actionmenu = new action_menu(); + $icon = $OUTPUT->pix_icon('i/menu', get_string('actions')); + $actionmenu->set_menu_trigger($icon, 'btn btn-icon d-flex align-items-center justify-content-center'); + $actionmenu->set_action_label(get_string('actions')); + $actionmenu->attributes['class'] .= ' presets-actions'; + + $actionmenu->add(new action_menu_link_secondary( + $deleteactionurl, + null, + get_string('delete'), + )); + $actions = $actionmenu->export_for_template($OUTPUT); } } - $presetstable->data[] = [ - \html_writer::tag('input', '', array('type' => 'radio', 'name' => 'fullname', - 'value' => "{$preset->userid}/{$preset->shortname}")), - $presetname, - $deleteaction, + $presets[] = [ + 'id' => $this->id, + 'name' => $preset->name, + 'shortname' => $preset->shortname, + 'fullname' => $presetname, + 'userid' => $preset->userid, + 'actions' => $actions, ]; } - return \html_writer::table($presetstable); + return $presets; } } diff --git a/mod/data/lang/en/data.php b/mod/data/lang/en/data.php index d2ce2a79dda..185a714f7eb 100644 --- a/mod/data/lang/en/data.php +++ b/mod/data/lang/en/data.php @@ -320,6 +320,7 @@ $string['portfolionotfile'] = 'Export to a portfolio rather than a file (csv and $string['presetinfo'] = 'Saving as a preset will publish this template. Other users may be able to use it in their databases.'; $string['presetnotselected'] = 'No preset has been selected.'; $string['presets'] = 'Presets'; +$string['presetshelp'] = 'Choose a preset to use as a starting point.'; $string['privacy:metadata:commentpurpose'] = 'Comments on database records'; $string['privacy:metadata:data_content'] = 'Represents one answer to one field in database activity module'; $string['privacy:metadata:data_content:fieldid'] = 'Field definition ID'; diff --git a/mod/data/styles.css b/mod/data/styles.css index 00494bd8e1b..53026c90bea 100644 --- a/mod/data/styles.css +++ b/mod/data/styles.css @@ -148,3 +148,7 @@ #page-mod-data-edit .datatagcontrol { padding-left: 10px; } + +.preset_action_menu .dropdown-toggle::after { + display: none; +} diff --git a/mod/data/templates/presets.mustache b/mod/data/templates/presets.mustache index 3d1958a2af7..06754453dfe 100644 --- a/mod/data/templates/presets.mustache +++ b/mod/data/templates/presets.mustache @@ -19,22 +19,69 @@ Context variables required for this template: * formactionul - The form action url. * d - The database id. - * presetstable - The HTML output of the table with presets + * presets - List of presets containing id, name, fullname, shortname and actions. Example context (json): { "formactionul": "http://www.example.com", "d": 1, - "presetstable": "
" + "presets": [ + { + "id": 1, + "name": "Image gallery", + "shortname": "imagegallery", + "fullname": "Image gallery", + "userid": 0, + "actions": [] + }, + { + "id": 2, + "name": "Preset saved manually", + "shortname": "Preset saved manually", + "fullname": "Preset saved manually (admin)", + "userid": 2, + "actions": [] + } + ] } }} +{{#str}}presetshelp, mod_data{{/str}} +
- {{{presetstable}}} - + + + + + + + + + + + {{#presets}} + + + + + + {{/presets}} + +
{{#str}} name {{/str}}{{#showmanage}}{{#str}} action {{/str}}{{/showmanage}}
+ + {{fullname}} + {{#actions}} +
+ {{>core/action_menu}} +
+ {{/actions}} +
+ +
+ {{#js}} require(['mod_data/selectpreset'], function(selectPreset) { selectPreset.init(); diff --git a/mod/data/tests/behat/data_presets.feature b/mod/data/tests/behat/data_presets.feature new file mode 100644 index 00000000000..d8f5f09965d --- /dev/null +++ b/mod/data/tests/behat/data_presets.feature @@ -0,0 +1,81 @@ +@mod @mod_data +Feature: Users can view and manage data presets + In order to use presets + As a user + I need to view, manage and use presets + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + And the following "activities" exist: + | 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 | + + @javascript + Scenario: Admins can delete saved presets + Given I am on the "Mountain landscapes" "data activity" page logged in as admin + 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 "Saved preset 1" + And I should see "Saved preset 2" + # Plugin presets can't be removed. + And I should not see "Actions" in the "Image gallery" "table_row" + # The admin should be able to delete saved presets. + But I open the action menu in "Saved preset 1" "table_row" + And I should see "Delete" + And I open the action menu in "Saved preset 2" "table_row" + And I should see "Delete" + + @javascript + Scenario: Teachers can see and use 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" + And I click on "Save as preset" "button" + And I set the field "Name" to "Saved preset by teacher1" + And I click on "Save" "button" in the "Save all fields and templates as preset" "dialogue" + 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 "Saved preset 1" + And I should see "Saved preset 2" + And I should see "Saved preset by teacher1" + # Plugin presets can't be removed. + And I should not see "Actions" in the "Image gallery" "table_row" + # Teachers should be able to delete their saved presets. + And I open the action menu in "Saved preset by teacher1" "table_row" + And I should see "Delete" + # Teachers can't delete the presets they haven't created. + And I should not see "Actions" in the "Saved preset 1" "table_row" + # The "Use preset" button should be enabled only when a preset is selected. + And the "Use preset" "button" should be disabled + And I click on "fullname" "radio" in the "Image gallery" "table_row" + And the "Use preset" "button" should be enabled + + @javascript + Scenario: Only users with the viewalluserpresets capability can see presets created by other users + Given the following "permission override" exists: + | role | editingteacher | + | capability | mod/data:viewalluserpresets | + | permission | Prohibit | + | contextlevel | System | + | reference | | + When I am on the "Mountain landscapes" "data activity" page logged in as teacher1 + And I follow "Presets" + Then I should see "Image gallery" + And I should not see "Saved preset 1" + And I should not see "Saved preset 2"