diff --git a/js/js/adapt-xapi.js b/js/js/adapt-xapi.js deleted file mode 100644 index 6eed94a..0000000 --- a/js/js/adapt-xapi.js +++ /dev/null @@ -1,173 +0,0 @@ -define([ - 'core/js/adapt', - './offlineStorage', - './errorNotificationModel', - './launchModel', - './statementModel', - './stateModel', - 'libraries/xapiwrapper.min', - 'libraries/url-polyfill', - 'libraries/fetch-polyfill', - 'libraries/promise-polyfill.min' -], function(Adapt, OfflineStorage, ErrorNotificationModel, LaunchModel, StatementModel, StateModel) { - - const xAPI = Backbone.Controller.extend({ - - _isInitialized: false, - _config: null, - _activityId: null, - _restoredLanguage: null, - _currentLanguage: null, - errorNotificationModel: null, - launchModel: null, - statementModel: null, - stateModel: null, - - initialize: function() { - this.listenToOnce(Adapt, 'offlineStorage:prepare', this.onPrepareOfflineStorage); - }, - - initializeErrorNotification: function() { - const config = this._config._errors; - - this.errorNotificationModel = new ErrorNotificationModel(config); - }, - - initializeLaunch: function() { - this.listenToOnce(Adapt, { - 'xapi:launchInitialized': this.onLaunchInitialized, - 'xapi:launchFailed': this.onLaunchFailed - }); - - this.launchModel = new LaunchModel(); - }, - - initializeState: function() { - this.listenTo(Adapt, 'xapi:stateLoaded', this.onStateLoaded); - - const config = { - activityId: this.getActivityId(), - registration: this.launchModel.get('registration'), - actor: this.launchModel.get('actor') - }; - - this.stateModel = new StateModel(config, { - wrapper: this.launchModel.getWrapper(), - _tracking: this._config._tracking - }); - }, - - initializeStatement: function() { - const config = { - activityId: this.getActivityId(), - registration: this.launchModel.get('registration'), - revision: this._config._revision || null, - actor: this.launchModel.get('actor'), - contextActivities: this.launchModel.get('contextActivities') - }; - - this.statementModel = new StatementModel(config, { - wrapper: this.launchModel.getWrapper(), - _tracking: this._config._tracking - }); - }, - - getActivityId: function() { - if (this._activityId) return this._activityId; - - const lrs = this.launchModel.getWrapper().lrs; - // if using cmi5 the activityId MUST come from the query string for "cmi.defined" statements - let activityId = lrs.activityId || lrs.activity_id || this._config._activityId; - - // @todo: should activityId be derived from URL? Would suggest not as the domain may not be controlled by the author/vendor - if (!activityId) Adapt.trigger('xapi:activityIdError'); - - // remove trailing slash if included - activityId = activityId.replace(/\/?$/, ''); - - return activityId; - }, - - // @todo: offlineStorage conflict with adapt-contrib-spoor - onPrepareOfflineStorage: function() { - this._config = Adapt.config.get('_xapi'); - - if (this._config && this._config._isEnabled) { - Adapt.wait.begin(); - - Adapt.offlineStorage.initialize(OfflineStorage); - - this.initializeErrorNotification(); - this.initializeLaunch(); - } - }, - - onLaunchInitialized: function() { - this._activityId = this.getActivityId(); - - if (!this._activityId) { - this.onLaunchFailed(); - - return; - } - - this.listenToOnce(Adapt, { - 'offlineStorage:ready': this.onOfflineStorageReady, - 'app:dataLoaded': this.onDataLoaded, - 'adapt:initialize': this.onAdaptInitialize - }); - - this.listenTo(Adapt, { - 'app:languageChanged': this.onLanguageChanged - }); - - this.initializeState(); - this.initializeStatement(); - }, - - onLaunchFailed: function() { - Adapt.wait.end(); - - Adapt.offlineStorage.setReadyStatus(); - }, - - onOfflineStorageReady: function() { - this._restoredLanguage = Adapt.offlineStorage.get('lang'); - }, - - onLanguageChanged: function(lang) { - const languageConfig = Adapt.config.get('_languagePicker'); - - if (languageConfig && languageConfig._isEnabled && this._restoredLanguage !== lang && this._currentLanguage !== lang) { - // only reset if language has changed since the course was started - not neccessary before - const resetState = this._isInitialized && !languageConfig._restoreStateOnLanguageChange; - - // @todo: only send when via a user selection? If `"_showOnCourseLoad": false`, this will still be triggered - Adapt.trigger('xapi:languageChanged', lang, resetState); - } - - this._restoredLanguage = null; - this._currentLanguage = lang; - }, - - onStateLoaded: function() { - Adapt.wait.end(); - - Adapt.offlineStorage.setReadyStatus(); - }, - - onDataLoaded: function() { - const globals = Adapt.course.get('_globals'); - if (!globals._learnerInfo) globals._learnerInfo = {}; - globals._learnerInfo = Adapt.offlineStorage.get('learnerinfo'); - }, - - onAdaptInitialize: function() { - this._isInitialized = true; - } - - }); - - return new xAPI(); - -}); diff --git a/js/js/errorNotificationModel.js b/js/js/errorNotificationModel.js deleted file mode 100644 index bd856c1..0000000 --- a/js/js/errorNotificationModel.js +++ /dev/null @@ -1,115 +0,0 @@ -define([ - 'core/js/adapt' -], function(Adapt) { - - const LAUNCH_ERROR_ID = 'launch-error'; - const ACTIVITYID_ERROR_ID = 'activityId-error'; - const LRS_ERROR_ID = 'lrs-error'; - - const ErrorNotificationModel = Backbone.Model.extend({ - - _isReady: false, - _isNotifyOpen: false, - _isDeferredLoadingError: false, - _currentNotifyId: null, - - initialize: function() { - this.listenToOnce(Adapt, { - 'app:dataLoaded': this.onDataLoaded - }); - - this.listenTo(Adapt, { - 'xapi:launchError': this.onShowLaunchError, - 'xapi:activityIdError': this.onShowActivityIdError, - 'xapi:lrsError': this.onShowLRSError, - 'notify:closed': this.onNotifyClosed - }); - }, - - _showNotification: function(config, id) { - if (this._isReady) { - if (!this._isNotifyOpen) { - Adapt.log.error(config.title); - - const notifyConfig = this._getNotifyConfig(config, id); - - Adapt.trigger('notify:popup', notifyConfig); - - this._isNotifyOpen = true; - this._currentNotifyId = id; - - } else if (this._currentNotifyId !== id) { - this.listenToOnce(Adapt, 'notify:closed', _.partial(this._showNotification, config, id)); - } - } else { - this._isDeferredLoadingError = true; - - this.listenToOnce(Adapt, 'app:dataLoaded', _.partial(this._showNotification, config, id)); - } - }, - - _getNotifyConfig: function(config, id) { - const notifyConfig = { - title: config.title, - body: config.body, - _classes: 'xAPIError ' + id + ' ' + config._classes, - _isxAPIError: true - }; - - let isCancellable = true; - - if (config.hasOwnProperty('_isCancellable')) { - isCancellable = config._isCancellable; - notifyConfig._isCancellable = isCancellable; - notifyConfig._closeOnShadowClick = !isCancellable; - } - - return notifyConfig; - }, - - /** - * Can't show notify until data has loaded due to `import_globals` in template - */ - onDataLoaded: function() { - this._isReady = true; - - if (this._isDeferredLoadingError) { - Adapt.wait.begin(); - - $('.loading').hide(); - } - }, - - onShowLaunchError: function() { - this._showNotification(this.get('_launch'), LAUNCH_ERROR_ID); - }, - - onShowActivityIdError: function() { - this._showNotification(this.get('_activityId'), ACTIVITYID_ERROR_ID); - }, - - onShowLRSError: function() { - this._showNotification(this.get('_lrs'), LRS_ERROR_ID); - }, - - onNotifyClosed: function(notify) { - if (!notify.model.get('_isxAPIError')) return; - - if (this._isDeferredLoadingError) { - Adapt.wait.end(); - - this._isDeferredLoadingError = false; - - // cancel other errors if launch failed and user dismissed, as it won't track regardless - this.stopListening(); - } - - this._isNotifyOpen = false; - this._currentNotifyId = null; - } - - }); - - return ErrorNotificationModel; - -}); diff --git a/js/js/launchModel.js b/js/js/launchModel.js deleted file mode 100644 index 1e64a89..0000000 --- a/js/js/launchModel.js +++ /dev/null @@ -1,129 +0,0 @@ -define([ - 'core/js/adapt' -], function(Adapt) { - - const LaunchModel = Backbone.Model.extend({ - - defaults: { - registration: null, - actor: null, - contextActivities: { - grouping: [] - } - }, - - _xAPIWrapper: null, - _retryCount: 0, - _retryLimit: 1, - - initialize: function() { - this.initializeLaunch(); - }, - - initializeLaunch: function() { - const lrs = ADL.XAPIWrapper.lrs; - - /** - * can auth be sent through in a different process, e.g. OAuth? - * lrs.endpoint && lrs.auth have defaults in the ADL xAPIWrapper, so can't assume their existence means they are the correct credentials - errors will be handled when communicating with the LRS - */ - if (lrs.endpoint && lrs.auth && lrs.actor) { - this._xAPIWrapper = ADL.XAPIWrapper; - - // add trailing slash if missing in endpoint - lrs.endpoint = lrs.endpoint.replace(/\/?$/, '/'); - - // @todo: capture grouping URL params - unsure what data this actually contains based on specs - unlike contextActivities for ADL Launch - const launchData = { - registration: lrs.registration || null, - actor: JSON.parse(lrs.actor)/*, - 'contextActivities': launchdata.contextActivities */ - }; - - this.set(launchData); - - this.triggerLaunchInitialized(); - } else { - ADL.launch(_.bind(this.onADLLaunchAttempt, this), false); - } - }, - - getWrapper: function() { - return this._xAPIWrapper; - }, - - showErrorNotification: function() { - Adapt.trigger('xapi:launchError'); - }, - - triggerLaunchInitialized: function() { - _.defer(function() { - Adapt.trigger('xapi:launchInitialized'); - }); - }, - - onADLLaunchAttempt: function(err, launchdata, wrapper) { - /* - 200 = OK - 400 = launch already initialized - 404 = launch removed - */ - if (!err) { - this._xAPIWrapper = wrapper; - - // can ADL launch include registration? - const launchData = { - registration: launchdata.registration || null, - actor: launchdata.actor - }; - - const contextActivities = launchdata.contextActivities; - if (!(_.isEmpty(contextActivities))) launchData.contextActivities = contextActivities; - - this.set(launchData); - - // store launch server details should browser be reloaded and launch server session still initialized - sessionStorage.setItem('lrs', JSON.stringify(wrapper.lrs)); - sessionStorage.setItem('launchData', JSON.stringify(launchData)); - - this.triggerLaunchInitialized(); - } else if (performance.navigation.type === 1) { - this.onReload(); - } else if (this._retryCount < this._retryLimit) { - this._retryCount++; - - this.initializeLaunch(); - } else { - this.onLaunchFail(); - } - }, - - // if launch session expired, will the next request to the launch server produce an error notification for the user? - onReload: function() { - const lrs = JSON.parse(sessionStorage.getItem('lrs')); - const launchData = JSON.parse(sessionStorage.getItem('launchData')); - - if (!lrs || !launchData) { - this.onLaunchFail(); - return; - } - - this._xAPIWrapper = ADL.XAPIWrapper; - this._xAPIWrapper.changeConfig(lrs); - - this.set(launchData); - - this.triggerLaunchInitialized(); - }, - - onLaunchFail: function() { - Adapt.trigger('xapi:launchFailed'); - - this.showErrorNotification(); - } - - }); - - return LaunchModel; - -}); diff --git a/js/js/offlineStorage.js b/js/js/offlineStorage.js deleted file mode 100644 index 6838d0f..0000000 --- a/js/js/offlineStorage.js +++ /dev/null @@ -1,53 +0,0 @@ -define([ - 'core/js/adapt' -], function(Adapt) { - - const OfflineStorage = { - - // will be set to StateModel once ready - store values until then - model: new Backbone.Model(), - - get: function(name) { - switch (name.toLowerCase()) { - case 'student':// for backwards-compatibility. learnerInfo is preferred and will give more information - return this.model.get('actor').name; - case 'learnerinfo': - return this._getActorData(); - default: - return this.model.get(name); - } - }, - - set: function(name, value) { - this.model.set(name, value); - }, - - _getActorData: function() { - const actor = this.model.get('actor'); - const id = this._getIdFromActor(actor); - - // I don't think we should make any judgement on name format for firstname or lastname, as there is no standard for this in xAPI - // if actor.name not provided, use id IFI - return { - id, - name: actor.name || id - }; - }, - - _getIdFromActor: function(actor) { - let id = actor.openid; - if (id) return id; - - id = actor.account && actor.account.name; - if (id) return id; - - id = actor.mbox || actor.mbox_sha1sum; - - return id; - } - - }; - - return OfflineStorage; - -}); diff --git a/js/js/stateModel.js b/js/js/stateModel.js deleted file mode 100644 index 3a3cff4..0000000 --- a/js/js/stateModel.js +++ /dev/null @@ -1,455 +0,0 @@ -define([ - 'core/js/adapt', - './offlineStorage', - 'libraries/async.min' -], function(Adapt, OfflineStorage, Async) { - - const COMPONENTS_KEY = 'components'; - const DURATIONS_KEY = 'durations'; - - const StateModel = Backbone.Model.extend({ - - defaults: { - activityId: null, - actor: null, - registration: null, - components: [], - durations: [] - }, - - _tracking: { - _storeQuestionResponses: true - }, - - xAPIWrapper: null, - _isInitialized: false, - _isLoaded: false, - _isRestored: false, - _queues: {}, - - initialize: function(attributes, options) { - this.listenTo(Adapt, { - 'adapt:initialize': this.onAdaptInitialize, - 'xapi:languageChanged': this.onLanguageChanged, - 'xapi:stateReset': this.onStateReset - }); - - this.xAPIWrapper = options.wrapper; - - _.extend(this._tracking, options._tracking); - - this.setOfflineStorageModel(); - - this.load(); - }, - - setOfflineStorageModel: function() { - const attributes = OfflineStorage.model.attributes; - - for (const key in attributes) { - this.set(key, attributes[key]); - this.save(key); - } - - OfflineStorage.model = this; - }, - - setupListeners: function() { - this.setupModelListeners(); - - // don't create new listeners for those which are still valid from initial course load - if (this._isInitialized) return; - - this.listenTo(Adapt, { - 'xapi:durationsChange': this.onDurationChange, - // ideally core would trigger `state.change` for each model so we don't have to return early for non-component types - 'state:change': this.onTrackableStateChange - }); - }, - - setupModelListeners: function() { - this.listenTo(Adapt.course, { - 'change:_totalDuration': this.onDurationChange - }); - - this.listenTo(Adapt.contentObjects, { - 'change:_totalDuration': this.onDurationChange - }); - }, - - removeModelListeners: function() { - this.stopListening(Adapt.course, { - 'change:_totalDuration': this.onDurationChange - }); - - this.stopListening(Adapt.contentObjects, { - 'change:_totalDuration': this.onDurationChange - }); - }, - - showErrorNotification: function() { - Adapt.trigger('xapi:lrsError'); - }, - - load: function() { - const scope = this; - - this._getStates(function(err, data) { - if (err) { - scope.showErrorNotification(); - } else { - const states = data; - - Async.each(states, function(id, callback) { - scope._fetchState(id, function(err, data) { - if (err) { - scope.showErrorNotification(); - } else { - // all data is now saved and retrieved as JSON, so no need for try/catch anymore - scope.set(id, data); - } - - callback(); - }); - }, function(err) { - if (err) { - scope.showErrorNotification(); - } else { - scope._isLoaded = true; - - Adapt.trigger('xapi:stateLoaded'); - - scope.listenTo(Adapt, 'app:dataReady', scope.onDataReady); - } - }); - } - }); - }, - - reset: function() { - const scope = this; - - this._getStates(function(err, data) { - if (err) { - scope.showErrorNotification(); - } else { - Adapt.wait.begin(); - - const states = data; - - Async.each(states, function(id, callback) { - scope.delete(id, callback); - }, function(err) { - if (err) scope.showErrorNotification(); - - const data = {}; - data[COMPONENTS_KEY] = []; - data[DURATIONS_KEY] = []; - scope.set(data, { silent: true }); - - Adapt.wait.end(); - }); - } - }); - }, - - restore: function() { - this._restoreComponentsData(); - this._restoreDurationsData(); - - this._isRestored = true; - - Adapt.trigger('xapi:stateReady'); - }, - - set: function(id, value) { - Backbone.Model.prototype.set.apply(this, arguments); - - // @todo: save every time the value changes, or only on specific events? - if (this._isLoaded) { - if (Adapt.terminate) { - this.save(id); - } else { - const queue = this._getQueueById(id); - queue.push(id); - } - } - }, - - save: function(id, callback) { - const scope = this; - const state = this.get(id); - const data = JSON.stringify(state); - - // ensure any data being set is completed before restoring following languageChange - if (!this._isRestored) Adapt.wait.begin(); - - fetch(this._getStateURL(id), { - keepalive: Adapt.terminate || false, - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - Authorization: this.xAPIWrapper.lrs.auth, - 'X-Experience-API-Version': this.xAPIWrapper.xapiVersion - }, - body: data - }).then(function(response) { - // if (response) Adapt.log.debug(response); - - if (!response.ok) throw Error(response.statusText); - - if (callback) callback(); - - if (!scope._isRestored) Adapt.wait.end(); - - return response; - }).catch(function(error) { - scope.showErrorNotification(); - - if (callback) callback(); - - if (!scope._isRestored) Adapt.wait.end(); - }); - }, - - delete: function(id, callback) { - this.unset(id, { silent: true }); - - const scope = this; - - fetch(this._getStateURL(id), { - method: 'DELETE', - headers: { - Authorization: this.xAPIWrapper.lrs.auth, - 'X-Experience-API-Version': this.xAPIWrapper.xapiVersion - } - }).then(function(response) { - if (!response.ok) throw Error(response.statusText); - - if (callback) callback(); - - return response; - }).catch(function(error) { - scope.showErrorNotification(); - - if (callback) callback(); - }); - }, - - _getStateURL: function(stateId) { - const activityId = this.get('activityId'); - const agent = this.get('actor'); - const registration = this.get('registration'); - let url = this.xAPIWrapper.lrs.endpoint + 'activities/state?activityId=' + encodeURIComponent(activityId) + '&agent=' + encodeURIComponent(JSON.stringify(agent)); - - if (registration) url += '®istration=' + encodeURIComponent(registration); - if (stateId) url += '&stateId=' + encodeURIComponent(stateId); - - return url; - }, - - _fetchState: function(stateId, callback) { - const scope = this; - - fetch(this._getStateURL(stateId), { - // cache: "no-cache", - method: 'GET', - headers: { - 'Content-Type': 'application/json', - Authorization: this.xAPIWrapper.lrs.auth, - 'X-Experience-API-Version': this.xAPIWrapper.xapiVersion, - 'Cache-Control': 'no-cache', - Pragma: 'no-cache' - } - }).then(function(response) { - if (!response.ok) throw Error(response.statusText); - - return response.json(); - }).then(function(data) { - // if (data) Adapt.log.debug(data); - - if (callback) callback(null, data); - }).catch(function(error) { - scope.showErrorNotification(); - - if (callback) callback(); - }); - }, - - _getStates: function(callback) { - const scope = this; - - Adapt.wait.begin(); - - this._fetchState(null, function(err, data) { - if (err) { - scope.showErrorNotification(); - - if (callback) callback(err, null); - } else { - if (callback) callback(null, data); - } - - Adapt.wait.end(); - }); - }, - - _getQueueById: function(id) { - let queue = this._queues[id]; - - if (!queue) { - queue = this._queues[id] = Async.queue(_.bind(function(id, callback) { - this.save(id, callback); - }, this), 1); - - queue.drain = function() { - Adapt.log.debug('State API queue cleared for ' + id); - }; - } - - return queue; - }, - - _restoreComponentsData: function() { - this._restoreDataForState(this.get(COMPONENTS_KEY), Adapt.components.models); - }, - - _restoreDurationsData: function() { - const models = [Adapt.course].concat(Adapt.contentObjects.models); - - this._restoreDataForState(this.get(DURATIONS_KEY), models); - }, - - _restoreDataForState: function(state, models) { - if (state.length > 0) { - state.forEach(function(data) { - const model = models.filter(function(model) { - return model.get('_id') === data._id; - })[0]; - - // account for models being removed in content without xAPI activityId or registration being changed - if (model) { - const restoreData = _.omit(data, '_id'); - - model.set(restoreData); - } - }); - } - }, - - _setComponentsData: function(model, data) { - const stateId = COMPONENTS_KEY; - const state = this.get(stateId); - const modelId = model.get('_id'); - const modelIndex = this._getStateModelIndexFor(state, modelId); - - // responses won't properly be restored until https://github.com/adaptlearning/adapt_framework/issues/2522 is resolved - if (model.get('_isQuestionType') && !this._tracking._storeQuestionResponses) { - delete data._isInteractionComplete; - delete data._userAnswer; - delete data._isSubmitted; - delete data._score; - delete data._isCorrect; - delete data._attemptsLeft; - } - - (modelIndex === null) ? state.push(data) : state[modelIndex] = data; - - this.set(stateId, state); - }, - - _setDurationsData: function(model) { - const stateId = DURATIONS_KEY; - const state = this.get(stateId); - const modelId = model.get('_id'); - const modelIndex = this._getStateModelIndexFor(state, modelId); - - const data = { - _id: modelId, - _totalDuration: model.get('_totalDuration') - }; - - (modelIndex === null) ? state.push(data) : state[modelIndex] = data; - - this.set(stateId, state); - }, - - _getStateModelIndexFor: function(state, modelId) { - for (let i = 0, l = state.length; i < l; i++) { - const stateModel = state[i]; - if (stateModel._id === modelId) return i; - } - - return null; - }, - - onDataReady: function() { - Adapt.wait.queue(_.bind(function() { - this.restore(); - }, this)); - }, - - onAdaptInitialize: function() { - this.setupListeners(); - - this._isInitialized = true; - }, - - onDurationChange: function(model) { - this._setDurationsData(model); - }, - - onTrackableStateChange: function(model, state) { - if (model.get('_type') !== 'component') return; - - // don't actually need state._isCorrect and state._score for questions, but save trackable state as provided - this._setComponentsData(model, state); - }, - - onStateReset: function() { - this.reset(); - }, - - // @todo: resetting could go against cmi5 spec, if course was previously completed - can't send multiple "cmi.defined" statements for some verbs - onLanguageChanged: function(lang, isStateReset) { - if (this._isInitialized) this.removeModelListeners(); - - this._isRestored = false; - - if (!isStateReset) return; - - const scope = this; - - this._getStates(function(err, data) { - if (err) { - scope.showErrorNotification(); - } else { - Adapt.wait.begin(); - - const states = data; - - const statesToReset = states.filter(function(id) { - return id !== 'lang'; - }); - - Async.each(statesToReset, function(id, callback) { - scope.delete(id, callback); - }, function(err) { - if (err) scope.showErrorNotification(); - - const data = {}; - data[COMPONENTS_KEY] = []; - data[DURATIONS_KEY] = []; - scope.set(data, { silent: true }); - - Adapt.wait.end(); - }); - } - }); - } - - }); - - return StateModel; - -}); diff --git a/js/js/statementModel.js b/js/js/statementModel.js deleted file mode 100644 index ba61974..0000000 --- a/js/js/statementModel.js +++ /dev/null @@ -1,438 +0,0 @@ -define([ - 'core/js/adapt', - 'core/js/enums/completionStateEnum', - './statements/initializedStatementModel', - './statements/terminatedStatementModel', - './statements/preferredLanguageStatementModel', - './statements/completedStatementModel', - './statements/experiencedStatementModel', - './statements/mcqStatementModel', - './statements/sliderStatementModel', - './statements/textInputStatementModel', - './statements/matchingStatementModel', - './statements/assessmentStatementModel', - './statements/resourceItemStatementModel', - './statements/favouriteStatementModel', - './statements/unfavouriteStatementModel' -], function(Adapt, COMPLETION_STATE, InitializedStatementModel, TerminatedStatementModel, PreferredLanguageStatementModel, CompletedStatementModel, ExperiencedStatementModel, McqStatementModel, SliderStatementModel, TextInputStatementModel, MatchingStatementModel, AssessmentStatementModel, ResourceItemStatementModel, FavouriteStatementModel, UnfavouriteStatementModel) { - - const StatementModel = Backbone.Model.extend({ - - _tracking: { - _questionInteractions: true, - _assessmentsCompletion: false, - _assessmentCompletion: true - }, - - xAPIWrapper: null, - _isInitialized: false, - _hasLanguageChanged: false, - _courseSessionStartTime: null, - _currentPageModel: null, - _terminate: false, - - initialize: function(attributes, options) { - this.listenTo(Adapt, { - 'adapt:initialize': this.onAdaptInitialize, - 'xapi:languageChanged': this.onLanguageChanged - }); - - this.xAPIWrapper = options.wrapper; - - _.extend(this._tracking, options._tracking); - - // this.loadRecipe(); - }, - - loadRecipe: function() { - - }, - - setupListeners: function() { - this.setupModelListeners(); - - // don't create new listeners for those which are still valid from initial course load - if (this._isInitialized) return; - - this._onVisibilityChange = _.bind(this.onVisibilityChange, this); - $(document).on('visibilitychange', this._onVisibilityChange); - - this._onWindowUnload = _.bind(this.onWindowUnload, this); - $(window).on('beforeunload unload', this._onWindowUnload); - - this.listenTo(Adapt, { - 'pageView:ready': this.onPageViewReady, - 'router:location': this.onRouterLocation, - 'resources:itemClicked': this.onResourceClicked, - 'tracking:complete': this.onTrackingComplete - }); - - if (this._tracking._questionInteractions) { - this.listenTo(Adapt, { - 'questionView:recordInteraction': this.onQuestionInteraction - }); - } - - // @todo: if only 1 Adapt.assessment._assessments, override so we never record both statements - leave to config.json for now? - if (this._tracking._assessmentsCompletion) { - this.listenTo(Adapt, { - 'assessments:complete': this.onAssessmentsComplete - }); - } - - if (this._tracking._assessmentCompletion) { - this.listenTo(Adapt, { - 'assessment:complete': this.onAssessmentComplete - }); - } - }, - - setupModelListeners: function() { - this.listenTo(Adapt.contentObjects, { - 'change:_isComplete': this.onContentObjectComplete - }); - - this.listenTo(Adapt.components, { - 'change:_isComplete': this.onComponentComplete - }); - }, - - removeModelListeners: function() { - this.stopListening(Adapt.contentObjects, { - 'change:_isComplete': this.onContentObjectComplete - }); - - this.stopListening(Adapt.components, { - 'change:_isComplete': this.onComponentComplete - }); - }, - - showErrorNotification: function() { - Adapt.trigger('xapi:lrsError'); - }, - - sendInitialized: function() { - const config = this.attributes; - const statementModel = new InitializedStatementModel(config); - const statement = statementModel.getData(Adapt.course); - - this.send(statement); - }, - - sendTerminated: function() { - const model = Adapt.course; - - this.setModelDuration(model); - - const config = this.attributes; - const statementModel = new TerminatedStatementModel(config); - const statement = statementModel.getData(model); - - this.send(statement); - }, - - sendPreferredLanguage: function() { - const config = this.attributes; - const statementModel = new PreferredLanguageStatementModel(config); - const statement = statementModel.getData(Adapt.course, Adapt.config.get('_activeLanguage')); - - this.send(statement); - }, - - sendCompleted: function(model) { - const modelType = model.get('_type'); - if (modelType === 'course' || modelType === 'page') this.setModelDuration(model); - - const config = this.attributes; - const statementModel = new CompletedStatementModel(config); - const statement = statementModel.getData(model); - - this.send(statement); - }, - - sendExperienced: function(model) { - this.setModelDuration(model); - - const config = this.attributes; - const statementModel = new ExperiencedStatementModel(config); - const statement = statementModel.getData(model); - - this.send(statement); - - model.unset('_sessionStartTime', { silent: true }); - model.unset('_sessionDuration', { silent: true }); - }, - - sendQuestionAnswered: function(model) { - const config = this.attributes; - const questionType = model.get('_component'); - let statementClass; - - // better solution than this factory type pattern? - switch (questionType) { - case 'mcq': - case 'gmcq': - statementClass = McqStatementModel; - break; - case 'slider': - statementClass = SliderStatementModel; - break; - case 'textinput': - statementClass = TextInputStatementModel; - break; - case 'matching': - statementClass = MatchingStatementModel; - break; - } - - const statementModel = new statementClass(config); - const statement = statementModel.getData(model); - - this.send(statement); - }, - - sendAssessmentCompleted: function(model, state) { - const config = this.attributes; - const statementModel = new AssessmentStatementModel(config); - const statement = statementModel.getData(model, state); - - this.send(statement); - }, - - sendResourceExperienced: function(model) { - const config = this.attributes; - const statementModel = new ResourceItemStatementModel(config); - const statement = statementModel.getData(model); - - this.send(statement); - }, - - sendFavourite: function(model) { - const config = this.attributes; - const statementModel = new FavouriteStatementModel(config); - const statement = statementModel.getData(model); - - this.send(statement); - }, - - sendUnfavourite: function(model) { - const config = this.attributes; - const statementModel = new UnfavouriteStatementModel(config); - const statement = statementModel.getData(model); - - this.send(statement); - }, - - /* - * @todo: Add Fetch API into xAPIWrapper - https://github.com/adlnet/xAPIWrapper/issues/166 - */ - send: function(statement) { - const lrs = this.xAPIWrapper.lrs; - const url = lrs.endpoint + 'statements'; - const data = JSON.stringify(statement); - const scope = this; - - fetch(url, { - keepalive: this._terminate, - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: lrs.auth, - 'X-Experience-API-Version': this.xAPIWrapper.xapiVersion - }, - body: data - }).then(function(response) { - Adapt.log.debug('[' + statement.id + ']: ' + response.status + ' - ' + response.statusText); - - if (!response.ok) throw Error(response.statusText); - - return response; - }).catch(function(error) { - scope.showErrorNotification(); - }); - }, - - setModelSessionStartTime: function(model, restoredTime) { - const time = restoredTime || new Date().getTime(); - - model.set('_sessionStartTime', time); - - // capture start time for course session as models are reloaded on a language change - if (model.get('_type') === 'course') this._courseSessionStartTime = time; - }, - - setModelDuration: function(model) { - const elapsedTime = new Date().getTime() - model.get('_sessionStartTime'); - - // reset `_sessionStartTime` to prevent cumulative additions via multiple calls to this method within the same session - mostly affects course model - this.setModelSessionStartTime(model); - - model.set({ - _sessionDuration: (model.get('_sessionDuration') || 0) + elapsedTime, - _totalDuration: (model.get('_totalDuration') || 0) + elapsedTime - }); - }, - - onLanguageChanged: function(lang, isStateReset) { - this._hasLanguageChanged = true; - - if (this._isInitialized) { - this.removeModelListeners(); - - if (this._currentPageModel) { - // @todo: ideally this would fire before the Adapt collections have reset - not possible in earlier frameworks but might be possible in later by `listenTo('Adapt.data', 'loading')` which fires before reset - // send experienced statement to ensure statement is sent before preferred language - this.sendExperienced(this._currentPageModel); - - // due to models reloading `_currentPageModel` is not part of Adapt.contentObjects so the stateModel is not picking up the durations change - Adapt.trigger('xapi:durationsChange', this._currentPageModel); - - // reset to bypass call in `onRouterLocation` so experienced statement is not sent - this._currentPageModel = null; - } - - // restore course session start time - if (!isStateReset) this.setModelSessionStartTime(Adapt.course, this._courseSessionStartTime); - - // send statement if language has changed since the course was started - call in `onAdaptInitialize` is only used initially to ensure correct execution order of statements - this.sendPreferredLanguage(); - } - - this.set('lang', lang); - - // reset course session start time if the state has been reset - if (isStateReset) this.setModelSessionStartTime(Adapt.course); - }, - - onAdaptInitialize: function() { - if (!this._isInitialized) { - this.setModelSessionStartTime(Adapt.course); - - this.sendInitialized(); - - // only called on initial launch if the course contains a language picker - call in `onLanguageChanged` is used for subsequent changes within the current browser session - if (this._hasLanguageChanged) { - this.sendPreferredLanguage(); - - this._hasLanguageChanged = false; - } - } - - this.setupListeners(); - - this._isInitialized = true; - }, - - onPageViewReady: function(view) { - const model = view.model; - - // store model so we have a reference to existing model following a language change - this._currentPageModel = model; - - this.setModelSessionStartTime(model); - }, - - onRouterLocation: function() { - const previousId = Adapt.location._previousId; - - // bypass if no page model or no previous location - if (!this._currentPageModel || !previousId) return; - - const model = Adapt.findById(previousId); - - if (model && model.get('_type') === 'page') { - // only record experienced statements for pages - this.sendExperienced(model); - } - - this._currentPageModel = null; - }, - - onContentObjectComplete: function(model) { - // since Adapt 5.5 the course model is treated as a contentObject - ignore as this is already handled by `onTrackingComplete` - if (model.get('_type') === 'course') return; - - // @todo: if page contains an assessment which can be reset but the page completes regardless of pass/fail, the `_totalDuration` will increase cumulatively for each attempt - should we reset the duration when reset? - if (model.get('_isComplete') && !model.get('_isOptional')) { - this.sendCompleted(model); - } - }, - - onComponentComplete: function(model) { - if (model.get('_isComplete') && model.get('_recordCompletion')) { - this.sendCompleted(model); - } - }, - - onAssessmentsComplete: function(state, model) { - // defer as triggered before last question triggers questionView:recordInteraction - _.defer(_.bind(this.sendAssessmentCompleted, this), model, state); - }, - - onAssessmentComplete: function(state) { - // create model based on Adapt.course._assessment, otherwise use Adapt.course as base - let model; - const assessmentConfig = Adapt.course.get('_assessment'); - - if (assessmentConfig && assessmentConfig._id && assessmentConfig.title) { - model = new Backbone.Model(assessmentConfig); - } else { - model = Adapt.course; - } - - _.defer(_.bind(this.sendAssessmentCompleted, this), model, state); - }, - - onTrackingComplete: function(completionData) { - this.sendCompleted(Adapt.course); - - // no need to use completionData.assessment due to assessment:complete listener, which isn't restricted to only firing on tracking:complete - }, - - onQuestionInteraction: function(view) { - this.sendQuestionAnswered(view.model); - }, - - onResourceClicked: function(data) { - const model = new Backbone.Model(); - - model.set({ - _id: (data.type === 'document') ? data.filename : '?link=' + data._link, - title: data.title, - description: data.description, - url: (data.type === 'document') ? data.filename : data._link - }); - - this.sendResourceExperienced(model); - }, - - onVisibilityChange: function() { - // set durations to ensure State loss is minimised for durations data, if terminate didn't fire - if (document.visibilityState === 'hidden' && !this._terminate) { - if (this._currentPageModel) this.setModelDuration(this._currentPageModel); - - this.setModelDuration(Adapt.course); - } - }, - - onWindowUnload: function() { - $(window).off('beforeunload unload', this._onWindowUnload); - - if (!this._terminate) { - Adapt.terminate = this._terminate = true; - - const model = Adapt.findById(Adapt.location._currentId); - - if (model && model.get('_type') !== 'course') { - this.sendExperienced(model); - } - - this.sendTerminated(); - } - } - - }); - - return StatementModel; - -}); diff --git a/js/js/statements/abstractStatementModel.js b/js/js/statements/abstractStatementModel.js deleted file mode 100644 index fb412d2..0000000 --- a/js/js/statements/abstractStatementModel.js +++ /dev/null @@ -1,176 +0,0 @@ -define([ - 'core/js/adapt', - '../utils' -], function(Adapt, Utils) { - - var AbstractStatementModel = Backbone.Model.extend({ - - defaults: { - recipeLang: 'en', - lang: 'en', - activityId: null, - registration: null, - revision: null, - actor: null, - contextActivities: { - grouping: [] - } - }, - - getData: function(model, state) { - const statement = new ADL.XAPIStatement(); - statement.id = ADL.ruuid(); - statement.actor = new ADL.XAPIStatement.Agent(this.get('actor')); - statement.verb = this.getVerb(model); - statement.object = this.getObject(model); - statement.context = this.getContext(model, state); - statement.timestamp = Utils.getTimestamp(); - - return statement; - }, - - getVerb: function(model) { - // intentionally empty to be overriden by subclass - }, - - getActivityType: function(model) { - // intentionally empty to be overriden by subclass - }, - - getObject: function(model) { - const object = new ADL.XAPIStatement.Activity(this.getUniqueIri(model)); - - const definition = { - type: this.getActivityType(model), - name: this.getName(model) - }; - - const extensions = this.getObjectExtensions(model); - - if (!(_.isEmpty(extensions))) definition.extensions = extensions; - - object.definition = definition; - - return object; - }, - - getObjectExtensions: function(model) { - const extensions = {}; - const type = model.get('_type'); - - if (type) extensions['https://adaptlearning.org/xapi/extension/model'] = type; - - return extensions; - }, - - getContext: function(model, state) { - const context = { - contextActivities: this.getContextActivities(model), - extensions: this.getContextExtensions(model, state), - language: this.get('lang') - }; - - const registration = this.get('registration'); - if (registration) context.registration = registration; - - const revision = this.get('revision'); - if (revision) context.revision = revision; - - return context; - }, - - getContextActivities: function(model) { - const contextActivities = _.clone(this.get('contextActivities')); - contextActivities.grouping = this.getContextActivitiesGrouping(model); - - return contextActivities; - }, - - getContextActivitiesGrouping: function(model) { - const grouping = this.get('contextActivities').grouping.slice(); - - grouping.push(this.getCourseContextActivity()); - - const modelType = model.get('_type'); - - if (modelType && modelType !== 'course') { - grouping.push.apply(grouping, this.getContentObjectsContextActivities(model)); - } - - return grouping; - }, - - getCourseContextActivity: function() { - const object = AbstractStatementModel.prototype.getObject.call(this, Adapt.course); - object.definition.type = ADL.activityTypes.course; - - return object; - }, - - getContentObjectsContextActivities: function(model) { - const contentObjects = model.getAncestorModels(true).filter(function(model) { - const modelType = model.get('_type'); - const isContentObject = modelType === 'menu' || modelType === 'page'; - - if (isContentObject) return model; - }); - - contentObjects.reverse(); - - const activities = []; - - contentObjects.forEach(function(model) { - activities.push(this.getContentObjectContextActivity(model)); - }, this); - - return activities; - }, - - getContentObjectContextActivity: function(model) { - const modelType = model.get('_type'); - const isContentObject = modelType === 'menu' || modelType === 'page'; - const contentObject = (isContentObject) ? model : model.findAncestor('contentObjects'); - const object = AbstractStatementModel.prototype.getObject.call(this, contentObject); - object.definition.type = ADL.activityTypes.module; - - return object; - }, - - getContextExtensions: function(model, state) { - const buildConfig = Adapt.build; - const frameworkVersion = (buildConfig) ? buildConfig.get('package').version : '<3.0.0'; - - const extensions = { - 'https://adaptlearning.org/xapi/extension/framework': 'Adapt', - 'https://adaptlearning.org/xapi/extension/framework_version': frameworkVersion - }; - - return extensions; - }, - - getName: function(model) { - const name = {}; - name[this.get('lang')] = model.get('title') || model.get('displayTitle'); - - return name; - }, - - getUniqueIri: function(model) { - let iri = this.get('activityId'); - - if (model && model.get('_type') !== 'course') { - iri += '/' + model.get('_id'); - } - - return iri; - }, - - getISO8601Duration: function(milliseconds) { - return Utils.getISO8601Duration(milliseconds); - } - - }); - - return AbstractStatementModel; - -}); diff --git a/js/js/statements/assessmentStatementModel.js b/js/js/statements/assessmentStatementModel.js deleted file mode 100644 index 6591b25..0000000 --- a/js/js/statements/assessmentStatementModel.js +++ /dev/null @@ -1,56 +0,0 @@ -define([ - './abstractStatementModel' -], function(AbstractStatementModel) { - - const AssessmentStatementModel = AbstractStatementModel.extend({ - - getData: function(model, state) { - const statement = AbstractStatementModel.prototype.getData.apply(this, arguments); - statement.verb = this.getVerb(state); - statement.result = this.getResult(state); - - return statement; - }, - - getVerb: function(state) { - // return if using Backbone.Model from AbstractStatementModel - if (state.attributes) return; - - const isPass = state.isPass; - // var verb = (isPass) ? ADL.verbs.passed : ADL.verbs.failed; - const verbType = (isPass) ? 'passed' : 'failed'; - - const verb = { - id: 'http://adlnet.gov/expapi/verbs/' + verbType, - display: {} - }; - - verb.display[this.get('recipeLang')] = verbType; - - return verb; - }, - - getActivityType: function(model) { - return ADL.activityTypes.assessment; - }, - - getResult: function(state) { - const result = { - score: { - raw: state.score, - min: 0, - max: state.maxScore, - scaled: state.scoreAsPercent / 100 - }/*, - success: state.isPass, - completion: state.isComplete */ - }; - - return result; - } - - }); - - return AssessmentStatementModel; - -}); diff --git a/js/js/statements/completedStatementModel.js b/js/js/statements/completedStatementModel.js deleted file mode 100644 index f73887e..0000000 --- a/js/js/statements/completedStatementModel.js +++ /dev/null @@ -1,54 +0,0 @@ -define([ - './abstractStatementModel' -], function(AbstractStatementModel) { - - const CompletedStatementModel = AbstractStatementModel.extend({ - - getData: function(model) { - const statement = AbstractStatementModel.prototype.getData.apply(this, arguments); - - const modelType = model.get('_type'); - if (modelType === 'course' || modelType === 'page') statement.result = this.getResult(model); - - return statement; - }, - - getVerb: function(model) { - // return ADL.verbs.completed; - - const verb = { - id: 'http://adlnet.gov/expapi/verbs/completed', - display: {} - }; - - verb.display[this.get('recipeLang')] = 'completed'; - - return verb; - }, - - getActivityType: function(model) { - const modelType = model.get('_type'); - - switch (modelType) { - case 'course': - return ADL.activityTypes.course; - case 'page': - return ADL.activityTypes.module; - case 'component': - return ADL.activityTypes.interaction; - } - }, - - getResult: function(model) { - const result = { - duration: this.getISO8601Duration(model.get('_totalDuration')) - }; - - return result; - } - - }); - - return CompletedStatementModel; - -}); diff --git a/js/js/statements/experiencedStatementModel.js b/js/js/statements/experiencedStatementModel.js deleted file mode 100644 index 6af192f..0000000 --- a/js/js/statements/experiencedStatementModel.js +++ /dev/null @@ -1,43 +0,0 @@ -define([ - './abstractStatementModel' -], function(AbstractStatementModel) { - - const ExperiencedStatementModel = AbstractStatementModel.extend({ - - getData: function(model) { - const statement = AbstractStatementModel.prototype.getData.apply(this, arguments); - statement.result = this.getResult(model); - - return statement; - }, - - getVerb: function(model) { - // return ADL.verbs.experienced; - - const verb = { - id: 'http://adlnet.gov/expapi/verbs/experienced', - display: {} - }; - - verb.display[this.get('recipeLang')] = 'experienced'; - - return verb; - }, - - getActivityType: function(model) { - return ADL.activityTypes.module; - }, - - getResult: function(model) { - const result = { - duration: this.getISO8601Duration(model.get('_sessionDuration')) - }; - - return result; - } - - }); - - return ExperiencedStatementModel; - -}); diff --git a/js/js/statements/favouriteStatementModel.js b/js/js/statements/favouriteStatementModel.js deleted file mode 100644 index e0c7db4..0000000 --- a/js/js/statements/favouriteStatementModel.js +++ /dev/null @@ -1,26 +0,0 @@ -define([ - './abstractStatementModel' -], function(AbstractStatementModel) { - - const FavouriteStatementModel = AbstractStatementModel.extend({ - - getVerb: function(model) { - const verb = { - id: 'http://activitystrea.ms/schema/1.0/favorite', - display: {} - }; - - verb.display[this.get('recipeLang')] = 'favorite'; - - return verb; - }, - - getActivityType: function(model) { - return ADL.activityTypes.module; - } - - }); - - return FavouriteStatementModel; - -}); diff --git a/js/js/statements/initializedStatementModel.js b/js/js/statements/initializedStatementModel.js deleted file mode 100644 index 7f60c8b..0000000 --- a/js/js/statements/initializedStatementModel.js +++ /dev/null @@ -1,40 +0,0 @@ -define([ - './abstractStatementModel' -], function(AbstractStatementModel) { - - const InitializedStatementModel = AbstractStatementModel.extend({ - - getVerb: function(model) { - // return ADL.verbs.initialized; - - const verb = { - id: 'http://adlnet.gov/expapi/verbs/initialized', - display: {} - }; - - verb.display[this.get('recipeLang')] = 'initialized'; - - return verb; - }, - - getActivityType: function(model) { - return ADL.activityTypes.course; - }, - - getContextExtensions: function(model, state) { - const extensions = AbstractStatementModel.prototype.getContextExtensions.apply(this, arguments); - - _.extend(extensions, { - 'http://id.tincanapi.com/extension/browser-info': { - 'user-agent-header': navigator.userAgent.toString() - } - }); - - return extensions; - } - - }); - - return InitializedStatementModel; - -}); diff --git a/js/js/statements/matchingStatementModel.js b/js/js/statements/matchingStatementModel.js deleted file mode 100644 index 3d7d4b6..0000000 --- a/js/js/statements/matchingStatementModel.js +++ /dev/null @@ -1,52 +0,0 @@ -define([ - './questionStatementModel' -], function(QuestionStatementModel) { - - const ITEM_DELIMETER = '[.]'; - const PAIR_DELIMETER = '[,]'; - - const MatchingStatementModel = QuestionStatementModel.extend({ - - /* - getInteractionObject: function(model) { - var interactionObject = model.getInteractionObject(); - - var definition = { - source: this.getSource(interactionObject.source), - target: this.getTarget(interactionObject.target), - correctResponsesPattern: interactionObject.correctResponsesPattern - }; - - return definition; - }, - - getSource: function(sources) { - sources.forEach(function(source) { - var description = {}; - description[this.get('lang')] = source.description; - source.description = description; - }, this); - - return sources; - }, - - getTarget: function(targets) { - targets.forEach(function(target) { - var description = {}; - description[this.get('lang')] = target.description; - target.description = description; - }, this); - - return targets; - }, - */ - - getResponse: function(model) { - return model.getResponse().replace(/\./g, ITEM_DELIMETER).replace(/,|#/g, PAIR_DELIMETER); - } - - }); - - return MatchingStatementModel; - -}); diff --git a/js/js/statements/mcqStatementModel.js b/js/js/statements/mcqStatementModel.js deleted file mode 100644 index 93912cb..0000000 --- a/js/js/statements/mcqStatementModel.js +++ /dev/null @@ -1,40 +0,0 @@ -define([ - './questionStatementModel' -], function(QuestionStatementModel) { - - const DELIMETER = '[,]'; - - const McqStatementModel = QuestionStatementModel.extend({ - - /* - getInteractionObject: function(model) { - var interactionObject = model.getInteractionObject(); - - var definition = { - choices: this.getChoices(interactionObject.choices), - correctResponsesPattern: interactionObject.correctResponsesPattern - }; - - return definition; - }, - - getChoices: function(choices) { - choices.forEach(function(choice) { - var description = {}; - description[this.get('lang')] = choice.description; - choice.description = description; - }, this); - - return choices; - }, - */ - - getResponse: function(model) { - return model.getResponse().replace(/,|#/g, DELIMETER); - } - - }); - - return McqStatementModel; - -}); diff --git a/js/js/statements/preferredLanguageStatementModel.js b/js/js/statements/preferredLanguageStatementModel.js deleted file mode 100644 index 6e82fb1..0000000 --- a/js/js/statements/preferredLanguageStatementModel.js +++ /dev/null @@ -1,31 +0,0 @@ -define([ - './preferredStatementModel' -], function(PreferredStatementModel) { - - const PreferredLanguageStatementModel = PreferredStatementModel.extend({ - - getData: function(model, lang) { - const statement = PreferredStatementModel.prototype.getData.apply(this, arguments); - - statement.result = this.getResult(model, lang); - - return statement; - }, - - getActivityType: function(model) { - return ADL.activityTypes.course; - }, - - getResult: function(model, lang) { - const result = { - response: lang - }; - - return result; - } - - }); - - return PreferredLanguageStatementModel; - -}); diff --git a/js/js/statements/preferredStatementModel.js b/js/js/statements/preferredStatementModel.js deleted file mode 100644 index bdd09e3..0000000 --- a/js/js/statements/preferredStatementModel.js +++ /dev/null @@ -1,24 +0,0 @@ -define([ - './abstractStatementModel' -], function(AbstractStatementModel) { - - const PreferredStatementModel = AbstractStatementModel.extend({ - - getVerb: function(model) { - // return ADL.verbs.preferred; - - const verb = { - id: 'http://adlnet.gov/expapi/verbs/preferred', - display: {} - }; - - verb.display[this.get('recipeLang')] = 'preferred'; - - return verb; - } - - }); - - return PreferredStatementModel; - -}); diff --git a/js/js/statements/questionStatementModel.js b/js/js/statements/questionStatementModel.js deleted file mode 100644 index dff231b..0000000 --- a/js/js/statements/questionStatementModel.js +++ /dev/null @@ -1,136 +0,0 @@ -define([ - './abstractStatementModel' -], function(AbstractStatementModel) { - - const QuestionStatementModel = AbstractStatementModel.extend({ - - getData: function(model) { - const statement = AbstractStatementModel.prototype.getData.apply(this, arguments); - statement.result = this.getResult(model); - - return statement; - }, - - getVerb: function(model) { - // return ADL.verbs.answered; - - const verb = { - id: 'http://adlnet.gov/expapi/verbs/answered', - display: {} - }; - - verb.display[this.get('recipeLang')] = 'answered'; - - return verb; - }, - - getActivityType: function(model) { - return 'http://adlnet.gov/expapi/activities/cmi.interaction'; - }, - - getObject: function(model) { - const object = AbstractStatementModel.prototype.getObject.apply(this, arguments); - - const definition = { - description: this.getDescription(model), - interactionType: model.getResponseType() - }; - - _.extend(definition, this.getInteractionObject(model)); - _.extend(object.definition, definition); - - return object; - }, - - getDescription: function(model) { - const description = {}; - description[this.get('lang')] = model.get('body'); - - return description; - }, - - /* - getInteractionActivities: function(model) { - var activities = { - interactionType: model.getResponseType() - }; - - _.extend(activities, this.getInteractionObject(model)); - - return activities; - }, - */ - - getInteractionObject: function(model) { - const interactionObject = model.getInteractionObject(); - - for (const key in interactionObject) { - const interactionActivity = interactionObject[key]; - - interactionActivity.forEach(function(activity) { - if (activity.hasOwnProperty('description')) { - const description = {}; - description[this.get('lang')] = activity.description; - activity.description = description; - } - }, this); - } - - return interactionObject; - }, - - getObjectExtensions: function(model) { - const extensions = AbstractStatementModel.prototype.getObjectExtensions.apply(this, arguments); - - _.extend(extensions, { - 'https://adaptlearning.org/xapi/extension/component': model.get('_component') - }); - - return extensions; - }, - - getContextActivities: function(model) { - const contextActivities = AbstractStatementModel.prototype.getContextActivities.apply(this, arguments); - - if (model.get('_isPartOfAssessment')) { - contextActivities.parent = [ - this.getAssessmentContextActivity(model) - ]; - } - - return contextActivities; - }, - - getAssessmentContextActivity: function(model) { - const assessment = model.findAncestor('articles'); - const object = AbstractStatementModel.prototype.getObject.call(this, assessment); - object.definition.type = ADL.activityTypes.assessment; - - return object; - }, - - getResult: function(model) { - const result = { - score: { - raw: model.get('_score') || 0/*, - min: 0, - max: model.get('_maxScore'), - scaled: model.get('_scoreAsPercent') / 100 */ - }, - success: model.get('_isCorrect'), - completion: model.get('_isComplete'), - response: this.getResponse(model) - }; - - return result; - }, - - getResponse: function(model) { - return model.getResponse(); - } - - }); - - return QuestionStatementModel; - -}); diff --git a/js/js/statements/resourceItemStatementModel.js b/js/js/statements/resourceItemStatementModel.js deleted file mode 100644 index fc452ed..0000000 --- a/js/js/statements/resourceItemStatementModel.js +++ /dev/null @@ -1,53 +0,0 @@ -define([ - './abstractStatementModel' -], function(AbstractStatementModel) { - - const ResourceItemStatementModel = AbstractStatementModel.extend({ - - getVerb: function(model) { - // return ADL.verbs.experienced; - - const verb = { - id: 'http://adlnet.gov/expapi/verbs/experienced', - display: {} - }; - - verb.display[this.get('recipeLang')] = 'experienced'; - - return verb; - }, - - getActivityType: function(model) { - return 'http://id.tincanapi.com/activitytype/resource'; - }, - - getName: function(model) { - const name = {}; - name[this.get('lang')] = model.get('title'); - - return name; - }, - - getObject: function(model) { - const object = AbstractStatementModel.prototype.getObject.apply(this, arguments); - - _.extend(object.definition, { - description: this.getDescription(model), - moreInfo: model.get('url') - }); - - return object; - }, - - getDescription: function(model) { - const description = {}; - description[this.get('lang')] = model.get('description'); - - return description; - } - - }); - - return ResourceItemStatementModel; - -}); diff --git a/js/js/statements/sliderStatementModel.js b/js/js/statements/sliderStatementModel.js deleted file mode 100644 index 084d70a..0000000 --- a/js/js/statements/sliderStatementModel.js +++ /dev/null @@ -1,36 +0,0 @@ -define([ - './questionStatementModel' -], function(QuestionStatementModel) { - - const DELIMETER = '[:]'; - - const SliderStatementModel = QuestionStatementModel.extend({ - - getInteractionObject: function(model) { - const definition = { - correctResponsesPattern: this.getCorrectResponsesPattern(model) - }; - - return definition; - }, - - getCorrectResponsesPattern: function(model) { - const correctAnswer = model.get('_correctAnswer'); - if (correctAnswer) return [correctAnswer]; - - const correctRange = model.get('_correctRange'); - if (correctRange) { - const bottom = correctRange._bottom || ''; - const top = correctRange._top || ''; - - return [ - bottom + DELIMETER + top - ]; - } - } - - }); - - return SliderStatementModel; - -}); diff --git a/js/js/statements/terminatedStatementModel.js b/js/js/statements/terminatedStatementModel.js deleted file mode 100644 index 80adcd7..0000000 --- a/js/js/statements/terminatedStatementModel.js +++ /dev/null @@ -1,43 +0,0 @@ -define([ - './abstractStatementModel' -], function(AbstractStatementModel) { - - const TerminatedStatementModel = AbstractStatementModel.extend({ - - getData: function(model) { - const statement = AbstractStatementModel.prototype.getData.apply(this, arguments); - statement.result = this.getResult(model); - - return statement; - }, - - getVerb: function(model) { - // return ADL.verbs.terminated; - - const verb = { - id: 'http://adlnet.gov/expapi/verbs/terminated', - display: {} - }; - - verb.display[this.get('recipeLang')] = 'terminated'; - - return verb; - }, - - getActivityType: function(model) { - return ADL.activityTypes.course; - }, - - getResult: function(model) { - const result = { - duration: this.getISO8601Duration(model.get('_sessionDuration')) - }; - - return result; - } - - }); - - return TerminatedStatementModel; - -}); diff --git a/js/js/statements/textInputStatementModel.js b/js/js/statements/textInputStatementModel.js deleted file mode 100644 index dc03c91..0000000 --- a/js/js/statements/textInputStatementModel.js +++ /dev/null @@ -1,39 +0,0 @@ -define([ - './questionStatementModel' -], function(QuestionStatementModel) { - - const TextInputStatementModel = QuestionStatementModel.extend({ - - getInteractionObject: function(model) { - const correctResponsesPattern = this.getCorrectResponsesPattern(model); - if (correctResponsesPattern === null) return {}; - - const definition = { - correctResponsesPattern - }; - - return definition; - }, - - getCorrectResponsesPattern: function(model) { - let correctAnswers = model.get('_answers'); - - // use same assumption as component that generic answers supersede specific answers - if (!correctAnswers) { - const items = model.get('_items'); - - // Exclude correctResponsesPattern if using specific answers when there is more than one input? - // 'Where the criteria for a question are complex and correct responses cannot be exhaustively listed, Learning Record Providers are discouraged from using the "correctResponsesPattern" property.' - if (items > 1) return null; - - correctAnswers = _.pluck(items, '_answers'); - } - - return _.flatten(correctAnswers); - } - - }); - - return TextInputStatementModel; - -}); diff --git a/js/js/statements/unfavouriteStatementModel.js b/js/js/statements/unfavouriteStatementModel.js deleted file mode 100644 index 561ee1b..0000000 --- a/js/js/statements/unfavouriteStatementModel.js +++ /dev/null @@ -1,26 +0,0 @@ -define([ - './abstractStatementModel' -], function(AbstractStatementModel) { - - const UnfavouriteStatementModel = AbstractStatementModel.extend({ - - getVerb: function(model) { - const verb = { - id: 'http://activitystrea.ms/schema/1.0/unfavorite', - display: {} - }; - - verb.display[this.get('recipeLang')] = 'unfavorite'; - - return verb; - }, - - getActivityType: function(model) { - return ADL.activityTypes.module; - } - - }); - - return UnfavouriteStatementModel; - -}); diff --git a/js/js/utils.js b/js/js/utils.js deleted file mode 100644 index 15a83eb..0000000 --- a/js/js/utils.js +++ /dev/null @@ -1,66 +0,0 @@ -define(function() { - - const Utils = { - - getISO8601Duration: function(milliseconds) { - const centiseconds = Math.round(milliseconds / 10); - const hours = parseInt(centiseconds / 360000, 10); - const minutes = parseInt((centiseconds % 360000) / 6000, 10); - const seconds = ((centiseconds % 360000) % 6000) / 100; - - let durationString = 'PT'; - if (hours > 0) durationString += hours + 'H'; - if (minutes > 0) durationString += minutes + 'M'; - durationString += seconds + 'S'; - - return durationString; - }, - - getTimestamp: function() { - const date = new Date(); - const ISODate = this.getISODate(date); - const ISOTime = this.getISOTime(date); - const ISOOffset = this.getISOOffset(date); - - return ISODate + 'T' + ISOTime + ISOOffset; - }, - - getISODate: function(date) { - const year = date.getFullYear(); - const month = this.padZeros(date.getMonth() + 1); - const monthDay = this.padZeros(date.getDate()); - - return year + '-' + month + '-' + monthDay; - }, - - getISOTime: function(date) { - const hours = this.padZeros(date.getHours()); - const minutes = this.padZeros(date.getMinutes()); - const seconds = this.padZeros(date.getSeconds()); - const milliseconds = this.padZeros(date.getMilliseconds()); - - return hours + ':' + minutes + ':' + seconds + '.' + milliseconds; - }, - - getISOOffset: function(date) { - const offset = date.getTimezoneOffset(); - - if (offset === 0) return 'Z'; - - const absOffset = Math.abs(offset); - const offsetHours = this.padZeros(Math.floor(absOffset / 60)); - const offsetMinutes = this.padZeros(Math.floor(absOffset % 60)); - const offsetSign = offset > 0 ? '-' : '+'; - - return offsetSign + offsetHours + ':' + offsetMinutes; - }, - - padZeros: function(num) { - return num < 10 ? '0' + num : num.toString(); - } - - }; - - return Utils; - -}); diff --git a/js/statementModel.js b/js/statementModel.js index 263bd71..ba61974 100644 --- a/js/statementModel.js +++ b/js/statementModel.js @@ -397,10 +397,10 @@ define([ const model = new Backbone.Model(); model.set({ - _id: (data.type === 'document') ? data.filename : '?' + data.href, + _id: (data.type === 'document') ? data.filename : '?link=' + data._link, title: data.title, description: data.description, - url: (data.type === 'document') ? data.filename : data.href + url: (data.type === 'document') ? data.filename : data._link }); this.sendResourceExperienced(model);