Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New: Added branching by attempt and correctness #50

Merged
merged 14 commits into from
Nov 21, 2023
Merged
29 changes: 23 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,33 @@ The attributes listed below are properly formatted as JSON in [*example.json*](h

>**\_incorrect** (string): When the mandatory questions contained are all incorrect and complete, this is the id of the next content block.

>**\_hasAttemptBands** (boolean): If set to `true`, turns on the **\_attemptBands** behaviour, allowing branching to happen across both attempts and correctness.

>**\_useQuestionAttempts** (boolean): If set to `true`, **\_hasAttemptBands** will branch according to the previous completed attempts at this question.

>**\_attemptBands** (object array): Multiple items may be created. Each item represents the branching options for the appropriate range of attempts. **\_attemptBands** contains values for **\_attempts**, **\_correct**, **\_partlyCorrect** and **\_incorrect**.

>>**\_attempts** (number): This numeric value represents the start of the range. The range continues to the next highest **\_attempts** of another band.

>>**\_correct** (string): When the mandatory questions contained are all correct and complete, this is the id of the next content block.

>>**\_partlyCorrect** (string): When the mandatory questions contained are partly correct and complete, this is the id of the next content block.

>>**\_incorrect** (string): When the mandatory questions contained are all incorrect and complete, this is the id of the next content block.

## Notes

* All blocks that are part of a branching sequence need to have a `_branching` object, even if it's empty. For instance, a block can simply use `"_branching": {}` if it should conditionally be shown but does not create any branches of its own.

## Limitations

* This extension will not work with legacy versions of trickle <=4.
* This extension will not work with legacy versions of assessment <=4.
* Spoor [`_shouldStoreAttempts`](https://github.com/adaptlearning/adapt-contrib-spoor#_shouldstoreattempts-boolean) should be set to true to retain the user selections across sessions

----------------------------
**Version number:** 0.1.0 <a href="https://community.adaptlearning.org/" target="_blank"><img src="https://github.com/adaptlearning/documentation/blob/master/04_wiki_assets/plug-ins/images/adapt-logo-mrgn-lft.jpg" alt="adapt learning logo" align="right"></a>
**Framework versions:** 5.7+
**Author / maintainer:** Adapt Core Team with [contributors](https://github.com/adaptlearning/adapt-contrib-trickle/graphs/contributors)
**Accessibility support:** WAI AA
**RTL support:** Yes
**Cross-platform coverage:** Chrome, Chrome for Android, Firefox (ESR + latest version), Edge, IE11, Safari 12+13 for macOS/iOS/iPadOS, Opera
**Framework versions:** 5.7+<br/>
**Author / maintainer:** Adapt Core Team with [contributors](https://github.com/adaptlearning/adapt-contrib-trickle/graphs/contributors)<br/>
**Accessibility support:** WAI AA<br/>
**RTL support:** Yes<br/>
**Cross-platform coverage:** Chrome, Chrome for Android, Firefox (ESR + latest version), Edge, Safari for macOS/iOS/iPadOS, Opera<br/>
37 changes: 36 additions & 1 deletion example.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
}

// articles.json

"_branching": {
"_isEnabled": true,
"_onChildren": true,
Expand All @@ -13,11 +12,47 @@
},

// blocks.json
"_branching": {
"_containerId": "",
"__comment": "leave _containerId blank to use the article _id",
"_correct": "b-255",
"_partlyCorrect": "b-260",
"_incorrect": "b-270"
}

// blocks.json - When using attempt-based branching
"_branching": {
"_containerId": "",
"__comment": "leave _containerId blank to use the article _id",
"_hasAttemptBands": true,
"_useQuestionAttempts": false,
"_attemptBands": [
{
"__comment": "2nd attempt",
"_attempts": 2,
"_correct": "b-255",
"_partlyCorrect": "b-260",
"_incorrect": "b-270"
},
{
"__comment": "3rd - 4th attempt",
"_attempts": 3,
"_correct": "b-255",
"_partlyCorrect": "b-260",
"_incorrect": "b-270"
},
{
"__comment": "5th+ attempt",
"_attempts": 5,
"_correct": "b-255",
"_partlyCorrect": "b-260",
"_incorrect": "b-270"
}
],
"__comment": "correct at any other attempt",
"_correct": "b-255",
"__comment": "partlyCorrect at any other attempt",
"_partlyCorrect": "b-260",
"__comment": "incorrect at any other attempt",
"_incorrect": "b-270"
}
8 changes: 3 additions & 5 deletions js/BranchingSet.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import Adapt from 'core/js/adapt';
import data from 'core/js/data';
import ComponentModel from 'core/js/models/componentModel';
import {
getCorrectness
} from './correctness';
getNextId
} from './branchingAlgorithm';
import offlineStorage from 'core/js/offlineStorage';
import logging from 'core/js/logging';

Expand Down Expand Up @@ -142,9 +142,7 @@ export default class BranchingSet {
const lastChildConfig = lastChildModel.get('_branching');
if (!lastChildConfig) return true;

// Branch from the last model's correctness, if configured
const correctness = getCorrectness(lastChildModel);
const nextId = lastChildConfig._force || lastChildConfig[`_${correctness}`];
const nextId = getNextId(lastChildModel);
if (!nextId) return true;

function findNextModel(nextId) {
Expand Down
2 changes: 1 addition & 1 deletion js/adapt-contrib-branching.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,8 @@ class Branching extends Backbone.Controller {

onComplete(model, value) {
if (!value) return;
this.continueAfterBranchChild(model);
this.saveBranchQuestionAttemptHistory(model);
this.continueAfterBranchChild(model);
}

continueAfterBranchChild(model) {
Expand Down
60 changes: 60 additions & 0 deletions js/branchingAlgorithm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import QuestionModel from 'core/js/models/questionModel';
import data from 'core/js/data';

export function getCorrectness(model) {
const questionModels = model.getAllDescendantModels().concat([model]).filter(model => model instanceof QuestionModel);
const numberOfCorrect = (questionModels.filter(child => (child.isCorrect()) && !child.get('_isOptional'))).length;
const numberOfPartlyCorrect = (questionModels.filter(child => (child.isPartlyCorrect()) && !child.get('_isOptional'))).length;
const isCorrect = questionModels.every(child => child.isCorrect() || child.get('_isOptional'));
const isPartlyCorrect = (numberOfCorrect > 0) || (numberOfPartlyCorrect > 0);
const correctnessState = isCorrect ?
'correct' :
isPartlyCorrect ?
'partlyCorrect' :
'incorrect';
return correctnessState;
}

export function getAttemptsTaken(model) {
const useQuestionAttempts = Boolean(model.get('_branching')?._useQuestionAttempts);
const questionModels = model.getAllDescendantModels().concat([model]).filter(model => model instanceof QuestionModel);
if (useQuestionAttempts) {
function getOriginalModelAttemptsTaken(questionModel) {
const originalModel = data.findById(questionModel.get('_branchOriginalModelId'));
const attemptObjects = originalModel.getAttemptObjects();
return attemptObjects.length;
}
const attemptsTaken = questionModels.reduce((sum, questionModel) => sum + getOriginalModelAttemptsTaken(questionModel), 0);
return attemptsTaken;
}
const attemptsPossible = questionModels.reduce((sum, questionModel) => sum + questionModel.get('_attempts'), 0);
const attemptsLeft = questionModels.reduce((sum, questionModel) => sum + (questionModel.get('_attemptsLeft') ?? questionModel.get('_attempts')), 0);
const attemptsTaken = (attemptsPossible - attemptsLeft);
return attemptsTaken;
}

export function getNextId(model) {
const config = model.get('_branching');
if (config._force) return config._force;
const correctness = getCorrectness(model);
const hasAttemptBands = Boolean(config?._hasAttemptBands ?? false);
oliverfoster marked this conversation as resolved.
Show resolved Hide resolved
if (!hasAttemptBands) return config[`_${correctness}`];
const attemptsTaken = getAttemptsTaken(model);
const attemptBands = config._attemptBands || [];
attemptBands.sort((a, b) => a._attempts - b._attempts);
const attemptBand = attemptBands
.slice(0)
.reverse()
.reduce((found, band) => {
if (found) return found;
if (band._attempts > attemptsTaken) return null;
return band;
}, null);
const hasAttemptBand = Boolean(attemptBand);
if (hasAttemptBands && hasAttemptBand) {
// Branch from last model's attemptBand correctness, if configured
return attemptBand[`_${correctness}`];
}
// Branch from the last model's correctness, if configured
return config[`_${correctness}`];
}
14 changes: 0 additions & 14 deletions js/correctness.js

This file was deleted.

59 changes: 59 additions & 0 deletions properties.schema
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,65 @@
"title": "Incorrect target",
"inputType": "Text",
"help": "Next block to render if all questions in this block are answered incorrectly"
},
"_hasAttemptBands": {
"type": "boolean",
"required": true,
"default": false,
"title": "Has attempt bands",
"inputType": "Checkbox",
"validators": [],
"help": "Controls whether this block will branch according to correctness and attempt or just correctness"
},
"_useQuestionAttempts": {
"type": "boolean",
"required": true,
"default": false,
"title": "Use question attempts",
"inputType": "Checkbox",
"validators": [],
"help": "Controls whether this block will branch according previous completed attempts at this question"
},
"_attemptBands": {
"type": "array",
"required": true,
"title": "Attempt bands",
"items": {
"type": "object",
"required": true,
"properties": {
"_attempts": {
"type": "number",
"required": false,
"default": 1,
"title": "Minimum attempts",
"inputType": "Number",
"validators": ["number"],
"help": "Enter a value representing the attempts for the start of the range."
},
"_correct": {
"type": "string",
"default": "",
"title": "Correct target",
"inputType": "Text",
"help": "Next block to render if all questions in this block are answered correctly"
},
"_partlyCorrect": {
"type": "string",
"default": "",
"title": "Partly correct target",
"inputType": "Text",
"help": "Next block to render if all questions in this block are answered partly correctly"
},
"_incorrect": {
"type": "string",
"default": "",
"title": "Incorrect target",
"inputType": "Text",
"help": "Next block to render if all questions in this block are answered incorrectly"
}
}
}
}
}
}
Expand Down
45 changes: 45 additions & 0 deletions schema/block.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,51 @@
"title": "Incorrect target",
"description": "Next block to render if all questions in this block are answered incorrectly",
"default": ""
},
"_hasAttemptBands": {
"type": "boolean",
"title": "Has attempt bands",
"description": "Controls whether this block will branch according to correctness and attempt or just correctness",
"default": false
},
"_useQuestionAttempts": {
"type": "boolean",
"title": "Use question attempts",
"description": "Controls whether this block will branch according previous completed attempts at this question",
"default": false
},
"_attemptBands": {
"type": "array",
"title": "Attempt bands",
"items": {
"type": "object",
"properties": {
"_attempts": {
"type": "number",
"title": "Minimum attempts",
"description": "Enter a value representing the attempts for the start of the range",
"default": 1
},
"_correct": {
"type": "string",
"title": "Correct target",
"description": "Next block to render if all questions in this block are answered correctly",
"default": ""
},
"_partlyCorrect": {
"type": "string",
"title": "Partly correct target",
"description": "Next block to render if all questions in this block are answered partly correctly",
"default": ""
},
"_incorrect": {
"type": "string",
"title": "Incorrect target",
"description": "Next block to render if all questions in this block are answered incorrectly",
"default": ""
}
}
}
}
}
}
Expand Down