From 31c87f015bb5f34d4db36906eb02a56e69eb67e1 Mon Sep 17 00:00:00 2001 From: jsconan Date: Mon, 15 Apr 2024 09:57:08 +0200 Subject: [PATCH 1/7] refactor: apply ES6, using shorthand and arrow functions when suitable --- .../creator/helpers/categorySelector.js | 124 ++++++++---------- 1 file changed, 56 insertions(+), 68 deletions(-) diff --git a/views/js/controller/creator/helpers/categorySelector.js b/views/js/controller/creator/helpers/categorySelector.js index d2f0c0d16..5aed830eb 100644 --- a/views/js/controller/creator/helpers/categorySelector.js +++ b/views/js/controller/creator/helpers/categorySelector.js @@ -13,7 +13,7 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * - * Copyright (c) 2017-2023 (original work) Open Assessment Technologies SA; + * Copyright (c) 2017-2024 (original work) Open Assessment Technologies SA; */ /** * This helper manages the category selection UI: @@ -35,58 +35,50 @@ define([ ], function ($, _, __, eventifier, tooltip, templates, featureVisibility) { 'use strict'; - let allPresets = [], - allQtiCategoriesPresets = []; + let allPresets = []; + let allQtiCategoriesPresets = []; function categorySelectorFactory($container) { const $presetsContainer = $container.find('.category-presets'); const $customCategoriesSelect = $container.find('[name=category-custom]'); - /** - * Read the form state from the DOM and trigger an event with the result, so the listeners can update the item/section model - * @fires categorySelector#category-change - */ - function updateCategories() { - const presetSelected = $container - .find('.category-preset input:checked') - .toArray() - .map(function (categoryEl) { - return categoryEl.value; - }), - presetIndeterminate = $container - .find('.category-preset input:indeterminate') - .toArray() - .map(function (categoryEl) { - return categoryEl.value; - }), - customSelected = $customCategoriesSelect - .siblings('.select2-container') - .find('.select2-search-choice') - .not('.partial') - .toArray() - .map(function (categoryEl) { - return categoryEl.textContent && categoryEl.textContent.trim(); - }), - customIndeterminate = $customCategoriesSelect - .siblings('.select2-container') - .find('.select2-search-choice.partial') - .toArray() - .map(function (categoryEl) { - return categoryEl.textContent && categoryEl.textContent.trim(); - }); - - const selectedCategories = presetSelected.concat(customSelected); - const indeterminatedCategories = presetIndeterminate.concat(customIndeterminate); - + const categorySelector = { /** - * @event categorySelector#category-change - * @param {String[]} allCategories - * @param {String[]} indeterminate + * Read the form state from the DOM and trigger an event with the result, so the listeners can update the item/section model + * @fires categorySelector#category-change */ - this.trigger('category-change', selectedCategories, indeterminatedCategories); - } + updateCategories() { + const presetSelected = $container + .find('.category-preset input:checked') + .toArray() + .map(categoryEl => categoryEl.value), + presetIndeterminate = $container + .find('.category-preset input:indeterminate') + .toArray() + .map(categoryEl => categoryEl.value), + customSelected = $customCategoriesSelect + .siblings('.select2-container') + .find('.select2-search-choice') + .not('.partial') + .toArray() + .map(categoryEl => categoryEl.textContent && categoryEl.textContent.trim()), + customIndeterminate = $customCategoriesSelect + .siblings('.select2-container') + .find('.select2-search-choice.partial') + .toArray() + .map(categoryEl => categoryEl.textContent && categoryEl.textContent.trim()); + + const selectedCategories = presetSelected.concat(customSelected); + const indeterminatedCategories = presetIndeterminate.concat(customIndeterminate); + + /** + * @event categorySelector#category-change + * @param {String[]} allCategories + * @param {String[]} indeterminate + */ + this.trigger('category-change', selectedCategories, indeterminatedCategories); + }, - const categorySelector = { /** * Create the category selection form * @@ -94,24 +86,21 @@ define([ * contains all the categories applied to at least one item of the section. * @param {string} [level] one of the values `testPart`, `section` or `itemRef` */ - createForm: function createForm(currentCategories, level) { - const self = this, - presetsTpl = templates.properties.categorypresets, - customCategories = _.difference(currentCategories, allQtiCategoriesPresets); + createForm(currentCategories, level) { + const presetsTpl = templates.properties.categorypresets; + const customCategories = _.difference(currentCategories, allQtiCategoriesPresets); const filteredPresets = featureVisibility.filterVisiblePresets(allPresets, level); // add preset checkboxes $presetsContainer.append(presetsTpl({ presetGroups: filteredPresets })); - $presetsContainer.on('click', function (e) { + $presetsContainer.on('click', e => { const $preset = $(e.target).closest('.category-preset'); if ($preset.length) { const $checkbox = $preset.find('input'); $checkbox.prop('indeterminate', false); - _.defer(function () { - updateCategories.call(self); - }); + _.defer(() => this.updateCategories()); } }); @@ -127,9 +116,7 @@ define([ }, maximumInputLength: 32 }) - .on('change', function () { - updateCategories.call(self); - }); + .on('change', () => this.updateCategories()); // enable help tooltips tooltip.lookup($container); @@ -140,7 +127,7 @@ define([ * @param {String[]} selected - categories associated with an item, or with all the items of the same section * @param {String[]} [indeterminate] - categories in an indeterminate state at a section level */ - updateFormState: function updateFormState(selected, indeterminate) { + updateFormState(selected, indeterminate) { indeterminate = indeterminate || []; const customCategories = _.difference(selected.concat(indeterminate), allQtiCategoriesPresets); @@ -148,16 +135,16 @@ define([ // Preset categories const $presetsCheckboxes = $container.find('.category-preset input'); - $presetsCheckboxes.each(function () { - const category = this.value; + $presetsCheckboxes.each((idx, input) => { + const category = input.value; - this.indeterminate = false; - this.checked = false; + input.indeterminate = false; + input.checked = false; if (indeterminate.indexOf(category) !== -1) { - this.indeterminate = true; + input.indeterminate = true; } else if (selected.indexOf(category) !== -1) { - this.checked = true; + input.checked = true; } }); @@ -168,8 +155,8 @@ define([ $customCategoriesSelect .siblings('.select2-container') .find('.select2-search-choice') - .each(function () { - const $li = $(this); + .each((idx, li) => { + const $li = $(li); const content = $li.find('div').text(); if (indeterminate.indexOf(content) !== -1) { $li.addClass('partial'); @@ -193,8 +180,9 @@ define([ * { * id: 'nextPartWarning', * label: 'Next Part Warning', - * qtiCategory : 'x-tao-option-nextPartWarning', - * description : 'Displays a warning before the user finishes a part' + * qtiCategory: 'x-tao-option-nextPartWarning', + * description: 'Displays a warning before the user finishes a part' + * ... * }, * ... * ] @@ -214,7 +202,7 @@ define([ * @returns {String[]} */ function extractCategoriesFromPresets() { - return allPresets.reduce(function (prev, current) { + return allPresets.reduce((prev, current) => { const groupIds = _.map(current.presets, 'qtiCategory'); return prev.concat(groupIds); }, []); From f798bdd7f86501efce8dc1f282ee007f8ce8e5bf Mon Sep 17 00:00:00 2001 From: jsconan Date: Mon, 15 Apr 2024 10:56:26 +0200 Subject: [PATCH 2/7] refactor: set return types --- models/classes/TestCategoryPreset.php | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/models/classes/TestCategoryPreset.php b/models/classes/TestCategoryPreset.php index ed914c7fc..4d8ccf73d 100644 --- a/models/classes/TestCategoryPreset.php +++ b/models/classes/TestCategoryPreset.php @@ -15,7 +15,7 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * - * Copyright (c) 2017 (original work) Open Assessment Technologies SA; + * Copyright (c) 2017-2024 (original work) Open Assessment Technologies SA; * */ @@ -106,37 +106,37 @@ public function __construct($id, $label, $qtiCategory, $data) } } - public function getId() + public function getId(): string { return $this->id; } - public function getLabel() + public function getLabel(): string { return $this->label; } - public function getQtiCategory() + public function getQtiCategory(): string { return $this->qtiCategory; } - public function getDescription() + public function getDescription(): string { return $this->description; } - public function getOrder() + public function getOrder(): int { return $this->order; } - public function getPluginId() + public function getPluginId(): string { return $this->pluginId; } - public function getFeatureFlag() + public function getFeatureFlag(): string { return (string) $this->featureFlag; } @@ -144,7 +144,7 @@ public function getFeatureFlag() /** * @see JsonSerializable::jsonSerialize */ - public function jsonSerialize() + public function jsonSerialize(): array { return $this->toArray(); } @@ -153,7 +153,7 @@ public function jsonSerialize() * Convenient method to convert the members to an assoc array * @return array the data */ - public function toArray() + public function toArray(): array { return [ 'id' => $this->id, @@ -172,7 +172,7 @@ public function toArray() * @return TestCategoryPreset the new instance * @throws common_exception_InconsistentData */ - public static function fromArray(array $data) + public static function fromArray(array $data): TestCategoryPreset { if (!isset($data['id']) || !isset($data['label']) || !isset($data['qtiCategory'])) { throw new common_exception_InconsistentData( From 78bb31bcf1c82c76e0535107de92c6eeba0ab2f7 Mon Sep 17 00:00:00 2001 From: jsconan Date: Mon, 15 Apr 2024 11:58:42 +0200 Subject: [PATCH 3/7] feat: allow multiple categories to represent a QTI category preset in the model --- models/classes/TestCategoryPreset.php | 32 ++++++++++++++++++++------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/models/classes/TestCategoryPreset.php b/models/classes/TestCategoryPreset.php index 4d8ccf73d..7bc504d83 100644 --- a/models/classes/TestCategoryPreset.php +++ b/models/classes/TestCategoryPreset.php @@ -46,6 +46,11 @@ class TestCategoryPreset implements JsonSerializable */ private $qtiCategory; + /** + * @var string[] $altCategories - the other possible qti categories that would activate the preset + */ + private $altCategories; + /** * @var string $description - what is the category purpose */ @@ -92,6 +97,8 @@ public function __construct($id, $label, $qtiCategory, $data) $this->label = (string) $label; $this->qtiCategory = (string) $qtiCategory; + $this->altCategories = []; + if (isset($data['description'])) { $this->description = (string) $data['description']; } @@ -104,6 +111,9 @@ public function __construct($id, $label, $qtiCategory, $data) if (isset($data['featureFlag'])) { $this->featureFlag = (string) $data['featureFlag']; } + if (isset($data['altCategories'])) { + $this->altCategories = array_map('strval', $data['altCategories']); + } } public function getId(): string @@ -121,7 +131,12 @@ public function getQtiCategory(): string return $this->qtiCategory; } - public function getDescription(): string + public function getAltCategory(): array + { + return $this->altCategories; + } + + public function getDescription(): ?string { return $this->description; } @@ -156,13 +171,14 @@ public function jsonSerialize(): array public function toArray(): array { return [ - 'id' => $this->id, - 'label' => $this->label, - 'qtiCategory' => $this->qtiCategory, - 'description' => $this->description, - 'order' => $this->order, - 'pluginId' => $this->pluginId, - 'featureFlag' => $this->featureFlag + 'id' => $this->id, + 'label' => $this->label, + 'qtiCategory' => $this->qtiCategory, + 'altCategories' => $this->altCategories, + 'description' => $this->description, + 'order' => $this->order, + 'pluginId' => $this->pluginId, + 'featureFlag' => $this->featureFlag ]; } From 9ba9ebdcb24492d9b17ec4d5a3d0f611f25670cc Mon Sep 17 00:00:00 2001 From: jsconan Date: Tue, 16 Apr 2024 09:53:49 +0200 Subject: [PATCH 4/7] feat: allow multiple categories to represent a QTI category preset, keep only the main one when updating --- .../creator/helpers/categorySelector.js | 50 ++++++++++--------- .../creator/templates/category-presets.tpl | 2 +- .../creator/helpers/categorySelector/test.js | 25 +++++++--- 3 files changed, 47 insertions(+), 30 deletions(-) diff --git a/views/js/controller/creator/helpers/categorySelector.js b/views/js/controller/creator/helpers/categorySelector.js index 5aed830eb..fc8536030 100644 --- a/views/js/controller/creator/helpers/categorySelector.js +++ b/views/js/controller/creator/helpers/categorySelector.js @@ -37,6 +37,7 @@ define([ let allPresets = []; let allQtiCategoriesPresets = []; + let categoryToPreset = new Map(); function categorySelectorFactory($container) { const $presetsContainer = $container.find('.category-presets'); @@ -136,16 +137,21 @@ define([ const $presetsCheckboxes = $container.find('.category-preset input'); $presetsCheckboxes.each((idx, input) => { - const category = input.value; - - input.indeterminate = false; - input.checked = false; - - if (indeterminate.indexOf(category) !== -1) { - input.indeterminate = true; - } else if (selected.indexOf(category) !== -1) { - input.checked = true; + const qtiCategory = input.value; + if (!categoryToPreset.has(qtiCategory)) { + // Unlikely to happen, but better safe than sorry... + input.indeterminate = indeterminate.includes(qtiCategory); + input.checked = selected.includes(qtiCategory); + return; } + // Check if one category declared for the preset is selected. + // Usually, only one exists, but it may happen that alternatives are present. + // In any case, only the main declared category (qtiCategory) will be saved. + // The concept is as follows: read all, write one. + const preset = categoryToPreset.get(qtiCategory); + const hasCategory = category => preset.categories.includes(category); + input.indeterminate = indeterminate.some(hasCategory); + input.checked = selected.some(hasCategory); }); // Custom categories @@ -181,6 +187,7 @@ define([ * id: 'nextPartWarning', * label: 'Next Part Warning', * qtiCategory: 'x-tao-option-nextPartWarning', + * altCategories: [x-tao-option-nextPartWarningMessage] * description: 'Displays a warning before the user finishes a part' * ... * }, @@ -191,22 +198,19 @@ define([ * ] */ categorySelectorFactory.setPresets = function setPresets(presets) { - if (_.isArray(presets)) { - allPresets = presets; - allQtiCategoriesPresets = extractCategoriesFromPresets(); + if (Array.isArray(presets)) { + allPresets = Array.from(presets); + categoryToPreset = new Map(); + allQtiCategoriesPresets = allPresets.reduce((allCategories, group) => { + return group.presets.reduce((all, preset) => { + const categories = [preset.qtiCategory].concat(preset.altCategories || []); + categories.forEach(category => categoryToPreset.set(category, preset)); + preset.categories = categories; + return all.concat(categories); + }, allCategories); + }, []); } }; - /** - * Extract the qtiCategory property of all presets of all groups - * @returns {String[]} - */ - function extractCategoriesFromPresets() { - return allPresets.reduce((prev, current) => { - const groupIds = _.map(current.presets, 'qtiCategory'); - return prev.concat(groupIds); - }, []); - } - return categorySelectorFactory; }); diff --git a/views/js/controller/creator/templates/category-presets.tpl b/views/js/controller/creator/templates/category-presets.tpl index 2625421fc..6888f4dd7 100644 --- a/views/js/controller/creator/templates/category-presets.tpl +++ b/views/js/controller/creator/templates/category-presets.tpl @@ -3,7 +3,7 @@
{{#each presets}} -
+