1 |
efrain |
1 |
{"version":3,"file":"edittree_weights.min.js","sources":["../src/edittree_weights.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 <http://www.gnu.org/licenses/>.\n\n/**\n * This module provides functionality for managing weight calculations and adjustments for grade items.\n *\n * @module core_grades/edittree_weight\n * @copyright 2023 Shamim Rezaie <shamim@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {getString} from 'core/str';\nimport {prefetchStrings} from 'core/prefetch';\n\n/**\n * Selectors.\n *\n * @type {Object}\n */\nconst selectors = {\n weightOverrideCheckbox: 'input[type=\"checkbox\"][name^=\"weightoverride_\"]',\n weightOverrideInput: 'input[type=\"text\"][name^=\"weight_\"]',\n aggregationForCategory: category => `[data-aggregationforcategory='${category}']`,\n childrenByCategory: category => `tr[data-parent-category=\"${category}\"]`,\n categoryByIdentifier: identifier => `tr.category[data-category=\"${identifier}\"]`,\n};\n\n/**\n * An object representing grading-related constants.\n * The same as what's defined in lib/grade/constants.php.\n *\n * @type {Object}\n * @property {Object} aggregation Aggregation settings.\n * @property {number} aggregation.sum Aggregation method: sum.\n * @property {Object} type Grade type settings.\n * @property {number} type.none Grade type: none.\n * @property {number} type.value Grade type: value.\n * @property {number} type.scale Grade type: scale.\n */\nconst grade = {\n aggregation: {\n sum: 13,\n },\n};\n\n/**\n * The character used as the decimal separator for number formatting.\n *\n * @type {string}\n */\nlet decimalSeparator;\n\n/**\n * This setting indicates if we should use algorithm prior to MDL-49257 fix for calculating extra credit weights.\n * Even though the old algorithm has bugs in it, we need to preserve existing grades.\n *\n * @type {boolean}\n */\nlet oldExtraCreditCalculation;\n\n/**\n * Recalculates the natural weights for grade items within a given category.\n *\n * @param {HTMLElement} categoryElement The DOM element representing the category.\n */\n// Suppress 'complexity' linting rule to keep this function as close to grade_category::auto_update_weights.\n// eslint-disable-next-line complexity\nconst recalculateNaturalWeights = (categoryElement) => {\n const childElements = document.querySelectorAll(selectors.childrenByCategory(categoryElement.dataset.category));\n\n // Calculate the sum of the grademax's of all the items within this category.\n let totalGradeMax = 0;\n\n // Out of 100, how much weight has been manually overridden by a user?\n let totalOverriddenWeight = 0;\n let totalOverriddenGradeMax = 0;\n\n // Has every assessment in this category been overridden?\n let automaticGradeItemsPresent = false;\n // Does the grade item require normalising?\n let requiresNormalising = false;\n\n // Is there an error in the weight calculations?\n let erroneous = false;\n\n // This array keeps track of the id and weight of every grade item that has been overridden.\n const overrideArray = {};\n\n for (const childElement of childElements) {\n const weightInput = childElement.querySelector(selectors.weightOverrideInput);\n const weightCheckbox = childElement.querySelector(selectors.weightOverrideCheckbox);\n\n // There are cases where a grade item should be excluded from calculations:\n // - If the item's grade type is 'text' or 'none'.\n // - If the grade item is an outcome item and the settings are set to not aggregate outcome items.\n // - If the item's grade type is 'scale' and the settings are set to ignore scales in aggregations.\n // All these cases are already taken care of in the backend, and no 'weight' input element is rendered on the page\n // if a grade item should not have a weight.\n if (!weightInput) {\n continue;\n }\n\n const itemWeight = parseWeight(weightInput.value);\n const itemAggregationCoefficient = parseInt(childElement.dataset.aggregationcoef);\n const itemGradeMax = parseFloat(childElement.dataset.grademax);\n\n // Record the ID and the weight for this grade item.\n overrideArray[childElement.dataset.itemid] = {\n extraCredit: itemAggregationCoefficient,\n weight: itemWeight,\n weightOverride: weightCheckbox.checked,\n };\n // If this item has had its weight overridden then set the flag to true, but\n // only if all previous items were also overridden. Note that extra credit items\n // are counted as overridden grade items.\n if (!weightCheckbox.checked && itemAggregationCoefficient === 0) {\n automaticGradeItemsPresent = true;\n }\n\n if (itemAggregationCoefficient > 0) {\n // An extra credit grade item doesn't contribute to totalOverriddenGradeMax.\n continue;\n } else if (weightCheckbox.checked && itemWeight <= 0) {\n // An overridden item that defines a weight of 0 does not contribute to totalOverriddenGradeMax.\n continue;\n }\n\n totalGradeMax += itemGradeMax;\n if (weightCheckbox.checked) {\n totalOverriddenWeight += itemWeight;\n totalOverriddenGradeMax += itemGradeMax;\n }\n }\n\n // Initialise this variable (used to keep track of the weight override total).\n let normaliseTotal = 0;\n // Keep a record of how much the override total is to see if it is above 100. If it is then we need to set the\n // other weights to zero and normalise the others.\n let overriddenTotal = 0;\n // Total up all the weights.\n for (const gradeItemDetail of Object.values(overrideArray)) {\n // Exclude grade items with extra credit or negative weights (which will be set to zero later).\n if (!gradeItemDetail.extraCredit && gradeItemDetail.weight > 0) {\n normaliseTotal += gradeItemDetail.weight;\n }\n // The overridden total includes items that are marked as overridden, not extra credit, and have a positive weight.\n if (gradeItemDetail.weightOverride && !gradeItemDetail.extraCredit && gradeItemDetail.weight > 0) {\n // Add overridden weights up to see if they are greater than 1.\n overriddenTotal += gradeItemDetail.weight;\n }\n }\n if (overriddenTotal > 100) {\n // Make sure that this category of weights gets normalised.\n requiresNormalising = true;\n // The normalised weights are only the overridden weights, so we just use the total of those.\n normaliseTotal = overriddenTotal;\n }\n\n const totalNonOverriddenGradeMax = totalGradeMax - totalOverriddenGradeMax;\n\n for (const childElement of childElements) {\n const weightInput = childElement.querySelector(selectors.weightOverrideInput);\n const weightCheckbox = childElement.querySelector(selectors.weightOverrideCheckbox);\n const itemAggregationCoefficient = parseInt(childElement.dataset.aggregationcoef);\n const itemGradeMax = parseFloat(childElement.dataset.grademax);\n\n if (!weightInput) {\n continue;\n } else if (!oldExtraCreditCalculation && itemAggregationCoefficient > 0 && weightCheckbox.checked) {\n // For an item with extra credit ignore other weights and overrides but do not change anything at all\n // if its weight was already overridden.\n continue;\n }\n\n // Remove any error messages and classes.\n weightInput.classList.remove('is-invalid');\n const errorArea = weightInput.closest('td').querySelector('.invalid-feedback');\n errorArea.textContent = '';\n\n if (!oldExtraCreditCalculation && itemAggregationCoefficient > 0 && !weightCheckbox.checked) {\n // For an item with extra credit ignore other weights and overrides.\n weightInput.value = totalGradeMax ? formatFloat(itemGradeMax * 100 / totalGradeMax) : 0;\n } else if (!weightCheckbox.checked) {\n // Calculations with a grade maximum of zero will cause problems. Just set the weight to zero.\n if (totalOverriddenWeight >= 100 || totalNonOverriddenGradeMax === 0 || itemGradeMax === 0) {\n // There is no more weight to distribute.\n weightInput.value = formatFloat(0);\n } else {\n // Calculate this item's weight as a percentage of the non-overridden total grade maxes\n // then convert it to a proportion of the available non-overridden weight.\n weightInput.value = formatFloat((itemGradeMax / totalNonOverriddenGradeMax) * (100 - totalOverriddenWeight));\n }\n } else if ((!automaticGradeItemsPresent && normaliseTotal !== 100) || requiresNormalising ||\n overrideArray[childElement.dataset.itemid].weight < 0) {\n if (overrideArray[childElement.dataset.itemid].weight < 0) {\n weightInput.value = formatFloat(0);\n }\n\n // Zero is a special case. If the total is zero then we need to set the weight of the parent category to zero.\n if (normaliseTotal !== 0) {\n erroneous = true;\n const error = normaliseTotal > 100 ? 'erroroverweight' : 'errorunderweight';\n // eslint-disable-next-line promise/always-return,promise/catch-or-return\n getString(error, 'core_grades').then((errorString) => {\n errorArea.textContent = errorString;\n });\n weightInput.classList.add('is-invalid');\n }\n }\n }\n\n if (!erroneous) {\n const categoryGradeMax = parseFloat(categoryElement.dataset.grademax);\n if (categoryGradeMax !== totalGradeMax) {\n // The category grade max is not the same as the total grade max, so we need to update the category grade max.\n categoryElement.dataset.grademax = totalGradeMax;\n const relatedCategoryAggregationRow = document.querySelector(\n selectors.aggregationForCategory(categoryElement.dataset.category)\n );\n relatedCategoryAggregationRow.querySelector('.column-range').innerHTML = formatFloat(totalGradeMax, 2, 2);\n\n const parentCategory = document.querySelector(selectors.categoryByIdentifier(categoryElement.dataset.parentCategory));\n if (parentCategory && (parseInt(parentCategory.dataset.aggregation) === grade.aggregation.sum)) {\n recalculateNaturalWeights(parentCategory);\n }\n }\n }\n};\n\n/**\n * Formats a floating-point number as a string with the specified number of decimal places.\n * Unnecessary trailing zeros are removed up to the specified minimum number of decimal places.\n *\n * @param {number} number The float value to be formatted.\n * @param {number} [decimalPoints=3] The number of decimal places to use.\n * @param {number} [minDecimals=1] The minimum number of decimal places to use.\n * @returns {string} The formatted weight value with the specified decimal places.\n */\nconst formatFloat = (number, decimalPoints = 3, minDecimals = 1) => {\n return number.toFixed(decimalPoints)\n .replace(new RegExp(`0{0,${decimalPoints - minDecimals}}$`), '')\n .replace('.', decimalSeparator);\n};\n\n/**\n * Parses a weight string and returns a normalized float value.\n *\n * @param {string} weightString The weight as a string, possibly with localized formatting.\n * @returns {number} The parsed weight as a float. If parsing fails, returns 0.\n */\nconst parseWeight = (weightString) => {\n const normalizedWeightString = weightString.replace(decimalSeparator, '.');\n return isNaN(Number(normalizedWeightString)) ? 0 : parseFloat(normalizedWeightString || 0);\n};\n\n/**\n * Initializes the weight management module with optional configuration.\n *\n * @param {string} decSep The character used as the decimal separator for number formatting.\n * @param {boolean} oldCalculation A flag indicating whether to use the old (pre MDL-49257) extra credit calculation.\n */\nexport const init = (decSep, oldCalculation) => {\n decimalSeparator = decSep;\n oldExtraCreditCalculation = oldCalculation;\n prefetchStrings('core_grades', ['erroroverweight', 'errorunderweight']);\n\n document.addEventListener('change', e => {\n // Update the weights of all grade items in the category when the weight of any grade item in the category is changed.\n if (e.target.matches(selectors.weightOverrideInput) || e.target.matches(selectors.weightOverrideCheckbox)) {\n // The following is named gradeItemRow, but it may also be a row that's representing a grade category.\n // It's ok because it serves as the categories associated grade item in our calculations.\n const gradeItemRow = e.target.closest('tr');\n const categoryElement = document.querySelector(selectors.categoryByIdentifier(gradeItemRow.dataset.parentCategory));\n\n // This is only required if we are using natural weights.\n if (parseInt(categoryElement.dataset.aggregation) === grade.aggregation.sum) {\n const weightElement = gradeItemRow.querySelector(selectors.weightOverrideInput);\n weightElement.value = formatFloat(Math.max(0, parseWeight(weightElement.value)));\n recalculateNaturalWeights(categoryElement);\n }\n }\n });\n\n document.addEventListener('submit', e => {\n // If the form is being submitted, then we need to ensure that the weight input fields are all set to\n // a valid value.\n if (e.target.matches('#gradetreeform')) {\n const firstInvalidWeightInput = e.target.querySelector('input.is-invalid');\n if (firstInvalidWeightInput) {\n const firstFocusableInvalidWeightInput = e.target.querySelector('input.is-invalid:enabled');\n if (firstFocusableInvalidWeightInput) {\n firstFocusableInvalidWeightInput.focus();\n } else {\n firstInvalidWeightInput.scrollIntoView({block: 'center'});\n }\n e.preventDefault();\n }\n }\n });\n};\n"],"names":["selectors","category","identifier","grade","sum","decimalSeparator","oldExtraCreditCalculation","recalculateNaturalWeights","categoryElement","childElements","document","querySelectorAll","dataset","totalGradeMax","totalOverriddenWeight","totalOverriddenGradeMax","automaticGradeItemsPresent","requiresNormalising","erroneous","overrideArray","childElement","weightInput","querySelector","weightCheckbox","itemWeight","parseWeight","value","itemAggregationCoefficient","parseInt","aggregationcoef","itemGradeMax","parseFloat","grademax","itemid","extraCredit","weight","weightOverride","checked","normaliseTotal","overriddenTotal","gradeItemDetail","Object","values","totalNonOverriddenGradeMax","classList","remove","errorArea","closest","textContent","formatFloat","error","then","errorString","add","innerHTML","parentCategory","aggregation","number","decimalPoints","minDecimals","toFixed","replace","RegExp","weightString","normalizedWeightString","isNaN","Number","decSep","oldCalculation","addEventListener","e","target","matches","gradeItemRow","weightElement","Math","max","firstInvalidWeightInput","firstFocusableInvalidWeightInput","focus","scrollIntoView","block","preventDefault"],"mappings":"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"}
|