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

issue/25 Allow external blocks #31

Merged
merged 12 commits into from
Jun 9, 2022
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ The attributes listed below are properly formatted as JSON in [*example.json*](h

>**\_onChildren** (boolean): If set to `true`, usually on an article, its children will be used for the branching scenario.

>**\_containerId** (string): To add a block to a alternative branching set, add the branching id here. Leave this blank to use the current parent.

>**\_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.
Expand Down
2 changes: 1 addition & 1 deletion bower.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "adapt-contrib-branching",
"version": "0.2.0",
"framework": ">=5.7",
"framework": ">=5.19.1",
"homepage": "https://github.com/adaptlearning/adapt-contrib-branching",
"issues": "https://github.com/adaptlearning/adapt_framework/issues/new",
"extension" : "branching",
Expand Down
2 changes: 2 additions & 0 deletions example.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

"_branching": {
"_isEnabled": true,
"_containerId": "",
"__comment": "leave _containerId blank to use the article _id",
"_correct": "b-255",
"_partlyCorrect": "b-260",
"_incorrect": "b-270"
Expand Down
65 changes: 38 additions & 27 deletions js/BranchingSet.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import Adapt from 'core/js/adapt';
import data from 'core/js/data';
import ComponentModel from 'core/js/models/componentModel';
import {
getTrackingPosition,
findByTrackingPosition
} from './trackingPosition';
import {
getCorrectness
} from './correctness';
import offlineStorage from 'core/js/offlineStorage';

/** @typedef {import("core/js/models/adaptModel").default} AdaptModel */

Expand All @@ -30,14 +27,14 @@ export default class BranchingSet {
}

restore() {
const branching = Adapt.offlineStorage.get('b');
const branching = offlineStorage.get('b');
if (!branching) return;
const id = this.model.get('_id');
if (!branching[id]) return;
const trackingPositions = Adapt.offlineStorage.deserialize(branching[id]);
const trackingPositions = offlineStorage.deserialize(branching[id]);
trackingPositions.forEach((trackingPosition, index) => {
const isLast = (index === trackingPositions.length - 1);
const model = findByTrackingPosition(trackingPosition);
const model = data.findByTrackingPosition(trackingPosition);
this.addNextModel(model, false, true, isLast);
});
if (this.isAtEnd) {
Expand Down Expand Up @@ -90,7 +87,7 @@ export default class BranchingSet {
return brachingModels.find(model => model.get('_id') === nextId) || true;
}

const originalLastChildModel = Adapt.findById(lastChildModel.get('_branchOriginalModelId'));
const originalLastChildModel = data.findById(lastChildModel.get('_branchOriginalModelId'));
const nextModel = originalLastChildModel.findRelativeModel(nextId);
const wasModelAlreadyUsed = nextModel.get('_isAvailable');
if (wasModelAlreadyUsed) return true;
Expand All @@ -109,8 +106,12 @@ export default class BranchingSet {
const cloned = nextModel.deepClone((clone, model) => {
clone.set({
_id: `${model.get('_id')}_branching_${attemptIndex}`, // Replicable ids for bookmarking
_isAvailable: true
_isAvailable: true,
_isBranchClone: true
});
if (model === nextModel) {
clone.set('_parentId', this.model.get('_id'));
}
// Remove tracking ids as these will change depending on the branches
// Clone attempt states are stored on the original model in their order of occurance
if (clone.has('_trackingId')) {
Expand Down Expand Up @@ -147,36 +148,41 @@ export default class BranchingSet {
cloned.setCompletionStatus();
}
}
// Add the cloned model to the parent hierarchy
nextModel.getParent().getChildren().add(cloned);
if (shouldSave) {
this.saveNextModel(nextModel);
}
return cloned;
}

/**
* @param {AdaptModel} nextModel
*/
saveNextModel(nextModel) {
const branching = Adapt.offlineStorage.get('b') || {};
const branching = offlineStorage.get('b') || {};
const id = this.model.get('_id');
const trackingIds = (branching[id] && Adapt.offlineStorage.deserialize(branching[id])) || [];
trackingIds.push(getTrackingPosition(nextModel));
branching[id] = Adapt.offlineStorage.serialize(trackingIds);
Adapt.offlineStorage.set('b', branching);
const trackingIds = (branching[id] && offlineStorage.deserialize(branching[id])) || [];
trackingIds.push(nextModel.trackingPosition);
branching[id] = offlineStorage.serialize(trackingIds);
offlineStorage.set('b', branching);
}

get models() {
return this.model.getChildren().filter(model => {
if (model.get('_isAvailable')) return false;
const containerId = this.model.get('_id');
return data.filter(model => {
if (model.get('_isBranchClone')) return false;
const config = model.get('_branching');
return (config && config._isEnabled !== false);
if (!config || config._isEnabled === false) return false;
return (config._containerId === containerId);
});
}

get branchedModels() {
return this.model.getChildren().filter(model => {
if (!model.get('_isAvailable')) return false;
const containerId = this.model.get('_id');
return data.filter(model => {
if (!model.get('_isBranchClone')) return false;
const config = model.get('_branching');
return (config && config._isEnabled !== false);
if (!config || config._isEnabled === false) return false;
return (config._containerId === containerId);
});
}

Expand All @@ -192,13 +198,15 @@ export default class BranchingSet {
}

async reset({ removeViews = false } = {}) {
if (this._isInReset) return;
this._isInReset = true;
this.model.set('_requireCompletionOf', Number.POSITIVE_INFINITY);
const parentView = Adapt.findViewByModelId(this.model.get('_id'));
const parentView = data.findViewByModelId(this.model.get('_id'));
const childViews = parentView?.getChildViews();
const branchedModels = this.branchedModels;
branchedModels.forEach(model => {
if (Adapt.parentView && removeViews) {
const view = Adapt.findViewByModelId(model.get('_id'));
const view = data.findViewByModelId(model.get('_id'));
if (view) {
view.remove();
childViews.splice(childViews.findIndex(v => v === view), 1);
Expand All @@ -209,13 +217,16 @@ export default class BranchingSet {
});
this.model.getChildren().remove(branchedModels);
this.model.findDescendantModels('component').forEach(model => model.set('_attemptStates', []));
const branching = Adapt.offlineStorage.get('b') || {};
const branching = offlineStorage.get('b') || {};
const id = this.model.get('_id');
const trackingIds = [];
branching[id] = Adapt.offlineStorage.serialize(trackingIds);
Adapt.offlineStorage.set('b', branching);
branching[id] = offlineStorage.serialize(trackingIds);
offlineStorage.set('b', branching);
this.addFirstModel();
await Adapt.parentView?.addChildren();
Adapt.checkingCompletion();
this.model.checkCompletionStatusFor('_isComplete');
this._isInReset = false;
return true;
}

Expand Down
37 changes: 31 additions & 6 deletions js/adapt-contrib-branching.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import Backbone from 'backbone';
import Adapt from 'core/js/adapt';
import data from 'core/js/data';
import logging from 'core/js/logging';
import ComponentModel from 'core/js/models/componentModel';
import offlineStorage from 'core/js/offlineStorage';
import BranchingSet from './BranchingSet';

class Branching extends Backbone.Controller {
Expand All @@ -23,12 +25,23 @@ class Branching extends Backbone.Controller {
const config = Adapt.course.get('_branching');
if (!config || !config._isEnabled) return;
// Wait for all other app:dataReady handlers to finish
if (this._isAwaitingDataReady) return;
this._isAwaitingDataReady = true;
await data.whenReady();
this.warnForSpoorMisconfiguration();
this._isAwaitingDataReady = false;
this.setupBranchingModels();
this.setupEventListeners();
Adapt.trigger('branching:dataReady');
}

warnForSpoorMisconfiguration() {
const config = Adapt.config.get('_spoor');
const isMisConfigured = (config?._isEnabled && config?._tracking?._shouldStoreAttempts === false);
oliverfoster marked this conversation as resolved.
Show resolved Hide resolved
if (!isMisConfigured) return;
oliverfoster marked this conversation as resolved.
Show resolved Hide resolved
logging.error('Branching: Spoor is misconfigured. Branching requires _spoor._tracking._shouldStoreAttempts = true');
}

setupBranchingModels() {
this._rawSets.length = 0;
const containerModels = data.filter(model => {
Expand All @@ -45,24 +58,36 @@ class Branching extends Backbone.Controller {
return (config._onChildren === true);
});
containerModels.forEach(containerModel => {
const containerId = containerModel.get('_id');
containerModel.set({
// Allow containers to request children at render
_canRequestChild: true,
// Prevent default completion
_requireCompletionOf: Number.POSITIVE_INFINITY
});
const children = containerModel.getChildren();
const children = [
...containerModel.getChildren(),
...data.filter(model => model.get('_branching')?._containerId === containerId)
].filter(Boolean);
// Hide all branching container original children as only clones will be displayed
children.forEach(child => {
const config = child.get('_branching');
if (!config || !config._isEnabled) return;
child.set('_isBranchChild', true);
child.setOnChildren({ _isAvailable: false });
config._containerId = config._containerId || containerId;
// Make direct children unavailable
const isDirectChild = (child.getParent().get('_id') === containerId);
if (isDirectChild) child.setOnChildren({ _isAvailable: false });
child.set({
_isBranchChild: true,
_isBranchClone: false
});
const descendants = [child].concat(child.getAllDescendantModels(true));
// Link all branch questions to their original ids ready for
// cloning and to facilitate save + restore
descendants.forEach(descendant => {
descendant.set('_branchOriginalModelId', descendant.get('_id'));
// Stop original items saving their own attemptStates as attemptStates are used to save/restore branching
if (descendant.isTypeGroup('component')) descendant.set('_shouldStoreAttempts', false);
});
});
const set = new BranchingSet({ model: containerModel });
Expand Down Expand Up @@ -146,7 +171,7 @@ class Branching extends Backbone.Controller {

contineAfterBranchChild(model) {
oliverfoster marked this conversation as resolved.
Show resolved Hide resolved
this.checkIfIsEffectivelyComplete(model);
if (!model.get('_isBranchChild')) return;
if (!model.get('_isBranchChild') || !model.get('_isAvailable')) return;
this.continue();
}

Expand Down Expand Up @@ -183,9 +208,9 @@ class Branching extends Backbone.Controller {
if (!(model instanceof ComponentModel)) return;
const branchOriginModelId = model.get('_branchOriginalModelId');
if (!branchOriginModelId) return;
const originModel = Adapt.findById(branchOriginModelId);
const originModel = data.findById(branchOriginModelId);
originModel.addAttemptObject(model.getAttemptObject());
Adapt.offlineStorage.save();
offlineStorage.save();
}

}
Expand Down
45 changes: 0 additions & 45 deletions js/trackingPosition.js

This file was deleted.

7 changes: 7 additions & 0 deletions properties.schema
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@
"title": "Enable branching to/from this block",
"inputType": "Checkbox"
},
"_containerId": {
"type": "string",
"default": "",
"title": "Alternate branching container id",
"inputType": "Text",
"help": "Leave blank to use the current article"
},
"_correct": {
"type": "string",
"default": "",
Expand Down