Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
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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"}