Skip to content

Commit

Permalink
Merge pull request #2589 from oat-sa/fix/MS-2872/set-external-for-score
Browse files Browse the repository at this point in the history
fix: create rules if score is external
  • Loading branch information
tikhanovichA authored Nov 1, 2024
2 parents 6d16146 + 4666b92 commit aa88a9d
Show file tree
Hide file tree
Showing 14 changed files with 179 additions and 29 deletions.
2 changes: 1 addition & 1 deletion model/Export/QTIPackedItemExporter.php
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ public function exportManifest($options = [], $exportResult = [])
//@todo add support of multi language packages
$rdfItem = $this->getItem();
$qtiItem = $qtiItemService->getDataItemByRdfItem($rdfItem);

$qtiItem->validateOutcomes();
if (!is_null($qtiItem)) {
// -- Prepare data transfer to the imsmanifest.tpl template.
$qtiItemData = [];
Expand Down
1 change: 1 addition & 0 deletions model/QtiJsonItemCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ protected function deployQtiItem(

try {
$qtiItem = $this->createQtiItem($item, $language);
$qtiItem->validateOutcomes();
$resolver = new ItemMediaResolver($item, $language);
$publicLangDirectory = $publicDirectory->getDirectory($language);

Expand Down
20 changes: 20 additions & 0 deletions model/qti/Item.php
Original file line number Diff line number Diff line change
Expand Up @@ -739,4 +739,24 @@ public function toForm()

return $returnValue;
}

public function validateOutcomes()
{
// Validate during publishing and exporting
$isExternalScore = false;
$isExternalSomeOutcome = false;
foreach ($this->getOutcomes() as $outcome) {
$externalScored = $outcome->getAttributeValue('externalScored');
if ($outcome->getIdentifier() === 'SCORE' && $externalScored && $externalScored !== 'none') {
$isExternalScore = true;
}
if ($outcome->getIdentifier() !== 'SCORE' && $externalScored && $externalScored !== 'none') {
$isExternalSomeOutcome = true;
}
}

if ($isExternalScore && $isExternalSomeOutcome) {
throw new \Exception('ExternalScored attribute is not allowed for multiple outcomes in item');
}
}
}
2 changes: 1 addition & 1 deletion views/js/loader/qtiLoader.min.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion views/js/loader/taoQtiItem.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion views/js/loader/taoQtiItem.min.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion views/js/loader/taoQtiItemRunner.es5.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion views/js/loader/taoQtiItemRunner.es5.min.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion views/js/loader/taoQtiItemRunner.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion views/js/loader/taoQtiItemRunner.min.js.map

Large diffs are not rendered by default.

158 changes: 144 additions & 14 deletions views/js/qtiCreator/plugins/panel/outcomeEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,14 @@ define([
function renderListing(item, $outcomeEditorPanel) {
const readOnlyRpVariables = getRpUsedVariables(item);
const scoreMaxScoreVisible = features.isVisible('taoQtiItem/creator/interaction/response/outcomeDeclarations/scoreMaxScore');
const scoreExternalScored = _.get(_.find(item.outcomes, function (outcome) {
return outcome.attributes && outcome.attributes.identifier === 'SCORE';
}), 'attributes.externalScored', externalScoredOptions.none);

const outcomesData = _.map(item.outcomes, function (outcome) {
const readonly = readOnlyRpVariables.indexOf(outcome.id()) >= 0;

const id = outcome.id();
let externalScoredDisabled = outcome.attr('externalScoredDisabled');
const externalScored = {
none: { label: __('None'), selected: !outcome.attr('externalScored') },
human: { label: __('Human'), selected: outcome.attr('externalScored') === externalScoredOptions.human },
Expand All @@ -108,11 +112,38 @@ define([
selected: outcome.attr('externalScored') === externalScoredOptions.externalMachine
}
};
function setExternalScoredToNone() {
externalScored.none.selected = true;
externalScored.human.selected = false;
externalScored.externalMachine.selected = false;
externalScoredDisabled = 1;
outcome.removeAttr('externalScored');
}

function hasExternalScoredOutcome(outcomes) {
return _.some(outcomes, function (outcome) {
return outcome.attributes &&
outcome.attributes.identifier !== 'SCORE' &&
outcome.attributes.externalScored &&
outcome.attributes.externalScored !== externalScoredOptions.none
});
}

function shouldSetExternalScoredToNone() {
if (id !== 'SCORE') {
return scoreExternalScored && scoreExternalScored !== externalScoredOptions.none;
}
return hasExternalScoredOutcome(item.outcomes);
}

if (shouldSetExternalScoredToNone()) {
setExternalScoredToNone();
}

return {
serial: outcome.serial,
identifier: outcome.id(),
hidden: (outcome.id() === 'SCORE' || outcome.id() === 'MAXSCORE') && !scoreMaxScoreVisible,
identifier: id,
hidden: (id === 'SCORE' || id === 'MAXSCORE') && !scoreMaxScoreVisible,
interpretation: outcome.attr('interpretation'),
longInterpretation: outcome.attr('longInterpretation'),
externalScored: externalScored,
Expand All @@ -122,7 +153,8 @@ define([
? __('Cannot delete a variable currently used in response processing')
: __('Delete'),
titleEdit: readonly ? __('Cannot edit a variable currently used in response processing') : __('Edit'),
readonly: readonly
readonly: readonly,
externalScoredDisabled: externalScoredDisabled || 0
};
});

Expand Down Expand Up @@ -167,6 +199,66 @@ define([
}
};

function hasAllNotExternalScored(outcomes) {
return _.every(outcomes, function (outcome) {
return outcome.attributes &&
outcome.attributes.externalScored === externalScoredOptions.none
});
}

function setMinumumMaximumValue(outcomeElement, outcomeValueContainer, min, max) {
outcomeElement.attr('normalMaximum', max);
outcomeValueContainer.find('[name="normalMaximum"]').val(max);
outcomeElement.attr('normalMinimum', min);
outcomeValueContainer.find('[name="normalMinimum"]').val(min);
}


function updateExternalScored(responsePanel, serial, disableCondition, shouldDisable) {
// Iterate over each outcome container
responsePanel.find('.outcome-container').each(function () {
const currentSerial = $(this).data('serial');
if (currentSerial === serial) {
return; // Skip the current serial
}

const currentOutcomeElement = Element.getElementBySerial(currentSerial);
const id = $(this).find(".identifier").val();

// Check if disabling condition is met
if (disableCondition(id)) {
$(this).find("select[name='externalScored']").attr('disabled', shouldDisable);
if (shouldDisable) {
currentOutcomeElement.removeAttr('externalScored');
}
}
});
}

function shouldDisableScoreBasedOnOtherVariables(responsePanel, serial) {
let shouldDisable = false;

// Iterate over each element in responsePanel and check if any value != none
responsePanel.find('.outcome-container').each(function () {
const currentSerial = $(this).data('serial');

// Skip the element with the same serial, as we're focusing on other elements
if (currentSerial === serial) {
return;
}

const currentValue = $(this).find("select[name='externalScored']").val();

// If any element has a value other than 'none', disable SCORE
if (currentValue !== externalScoredOptions.none) {
shouldDisable = true;
return false; // Exit the loop early
}
});

return shouldDisable;
}

/**
* Disposes tooltips
*
Expand Down Expand Up @@ -207,18 +299,19 @@ define([
const $incrementerContainer = $outcomeContainer.find(".incrementer");
const $identifierLabel = $labelContainer.find('.label');
const $identifierInput = $labelContainer.find('.identifier');
const $outcomeValueContainer = $outcomeContainer.find('div.minimum-maximum');
const isScoreOutcome = outcomeElement.attributes.identifier === 'SCORE';
let isScoringTraitValidationEnabled =
outcomeElement.attr('externalScored') === externalScoredOptions.human;
if (
isScoreOutcome &&
!externalScoredValidOptions.includes(
outcomeElement.attr("externalScored")
)
) {
$incrementerContainer.incrementer("disable");
$incrementerContainer.incrementer("disable");
setMinumumMaximumValue(outcomeElement, $outcomeValueContainer, 0, 0);
} else {
$incrementerContainer.incrementer("enable");
$incrementerContainer.incrementer("enable");
}

$outcomeContainer.addClass('editing');
Expand All @@ -229,8 +322,6 @@ define([
$identifierInput.val('');
$identifierInput.val(outcomeElement.id());

const $outcomeValueContainer = $outcomeContainer.find('div.minimum-maximum');

const showScoringTraitWarningOnInvalidValue = () => {
if (
!isValidScoringTrait(outcomeElement.attr('normalMinimum')) ||
Expand All @@ -250,6 +341,10 @@ define([
showScoringTraitWarningOnInvalidValue();
}

if (hasAllNotExternalScored(item.outcome)) {
$outcomeContainer.find("select[name='externalScored']").attr('disabled', false);
}

//attach form change callbacks
formElement.setChangeCallbacks(
$outcomeContainer,
Expand Down Expand Up @@ -283,10 +378,12 @@ define([
if (isScoreOutcome && value !== externalScoredOptions.none) {
$incrementerContainer.incrementer("enable");
} else if (isScoreOutcome) {
outcome.attr('normalMaximum', 0);
$outcomeValueContainer.find('[name="normalMaximum"]').val(0);
outcome.attr('normalMinimum', 0);
$outcomeValueContainer.find('[name="normalMinimum"]').val(0);
setMinumumMaximumValue(outcome, $outcomeValueContainer, 0, 0);
$incrementerContainer.incrementer("disable");
} else if (value !== externalScoredOptions.none) {
$incrementerContainer.incrementer("enable");
} else {
setMinumumMaximumValue(outcome, $outcomeValueContainer, 0, 0);
$incrementerContainer.incrementer("disable");
}

Expand All @@ -310,6 +407,30 @@ define([
} else {
outcome.attr('externalScored', value);
}
if (isScoreOutcome && value !== externalScoredOptions.none) {
// Disable other outcomes if condition is met
updateExternalScored($responsePanel, serial, function (id) {
return value !== externalScoredOptions.none;
}, true);
} else if (isScoreOutcome && value === externalScoredOptions.none) {
// Disable other outcomes if condition is met
updateExternalScored($responsePanel, serial, function (id) {
return id !== 'SCORE';
}, false);
} else if (value !== externalScoredOptions.none) {
// Disable SCORE if condition is met
updateExternalScored($responsePanel, serial, function (id) {
return id === 'SCORE';
}, true);
} else {
// Check the states of other outcomes before enabling SCORE
const shouldDisable = shouldDisableScoreBasedOnOtherVariables($responsePanel, serial);

// Enable or disable SCORE based on the result
updateExternalScored($responsePanel, serial, function (id) {
return id === 'SCORE';
}, shouldDisable); // Disable SCORE if any other outcomes is not 'none'
}
}
},
formElement.getMinMaxAttributeCallbacks(
Expand All @@ -329,7 +450,7 @@ define([
value = Math.round(value);

outcome.attr(attr, value);
$outcomeValueContainer.find(`[name="${attr}"]`).val(value);
$outcomeValueContainer.find(`[name = "${attr}"]`).val(value);

if (attr === 'normalMinimum' && outcome.attr('normalMaximum') < value) {
outcome.attr('normalMaximum', value);
Expand All @@ -355,6 +476,15 @@ define([
.on(`click${_ns}`, '.deletable [data-role="delete"]', function () {
//delete the outcome
const $outcomeContainer = $(this).closest('.outcome-container');
const serial = $outcomeContainer.data('serial');
// Check the states of other outcomes before enabling SCORE
const shouldDisable = shouldDisableScoreBasedOnOtherVariables($responsePanel, serial);

// Enable or disable SCORE based on the result
updateExternalScored($responsePanel, serial, function (id) {
return id === 'SCORE';
}, shouldDisable); // Disable SCORE if any other outcomes is not 'none'

$outcomeContainer.remove();
item.remove('outcomes', $outcomeContainer.data('serial'));
})
Expand Down
2 changes: 1 addition & 1 deletion views/js/qtiCreator/tpl/outcomeEditor/listing.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
<label for="externalScored" class="has-icon">{{__ "External Scored"}}</label>
<span class="icon-help tooltipstered" data-tooltip="~ .tooltip-content:first" data-tooltip-theme="info"></span>
<div class="tooltip-content">{{__ "Select if you want the outcome declaration to be processed by an external system or human scorer. This is typically the case for items asking candidates to write an essay."}}</div>
<select name="externalScored" class="select2" data-has-search="false">
<select name="externalScored" class="select2" data-has-search="false" {{#if externalScoredDisabled}} disabled="disabled" {{/if}}>
{{#each externalScored}}
<option value="{{@key}}" {{#if selected}}selected="selected"{{/if}}>{{label}}</option>
{{/each}}
Expand Down
9 changes: 4 additions & 5 deletions views/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion views/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@
},
"dependencies": {
"@oat-sa/tao-item-runner": "^1.0.0",
"@oat-sa/tao-item-runner-qti": "^2.5.0"
"@oat-sa/tao-item-runner-qti": "^2.5.1"
}
}

0 comments on commit aa88a9d

Please sign in to comment.