Skip to content

Commit

Permalink
Merge pull request #120 from adaptlearning/enhancement-967
Browse files Browse the repository at this point in the history
enhancement-967: divided into model and view
  • Loading branch information
moloko authored Mar 6, 2017
2 parents afc8132 + f7fc963 commit 098c2cd
Show file tree
Hide file tree
Showing 3 changed files with 374 additions and 342 deletions.
351 changes: 9 additions & 342 deletions js/adapt-contrib-mcq.js
Original file line number Diff line number Diff line change
@@ -1,345 +1,12 @@
define(function(require) {
var QuestionView = require('coreViews/questionView');
var Adapt = require('coreJS/adapt');

var Mcq = QuestionView.extend({

events: {
'focus .mcq-item input':'onItemFocus',
'blur .mcq-item input':'onItemBlur',
'change .mcq-item input':'onItemSelected',
'keyup .mcq-item input':'onKeyPress'
},

resetQuestionOnRevisit: function() {
this.setAllItemsEnabled(true);
this.resetQuestion();
},

setupQuestion: function() {
// if only one answer is selectable, we should display radio buttons not checkboxes
this.model.set("_isRadio", (this.model.get("_selectable") == 1) );

this.model.set('_selectedItems', []);

this.setupQuestionItemIndexes();

this.setupRandomisation();

this.restoreUserAnswers();
},

setupQuestionItemIndexes: function() {
var items = this.model.get("_items");
if (items && items.length > 0) {
for (var i = 0, l = items.length; i < l; i++) {
if (items[i]._index === undefined) items[i]._index = i;
}
}
},

setupRandomisation: function() {
if (this.model.get('_isRandom') && this.model.get('_isEnabled')) {
this.model.set("_items", _.shuffle(this.model.get("_items")));
}
},

restoreUserAnswers: function() {
if (!this.model.get("_isSubmitted")) return;

var selectedItems = [];
var items = this.model.get("_items");
var userAnswer = this.model.get("_userAnswer");
_.each(items, function(item, index) {
item._isSelected = userAnswer[item._index];
if (item._isSelected) {
selectedItems.push(item)
}
});

this.model.set("_selectedItems", selectedItems);

this.setQuestionAsSubmitted();
this.markQuestion();
this.setScore();
this.showMarking();
this.setupFeedback();
},

disableQuestion: function() {
this.setAllItemsEnabled(false);
},

enableQuestion: function() {
this.setAllItemsEnabled(true);
},

setAllItemsEnabled: function(isEnabled) {
_.each(this.model.get('_items'), function(item, index){
var $itemLabel = this.$('label').eq(index);
var $itemInput = this.$('input').eq(index);

if (isEnabled) {
$itemLabel.removeClass('disabled');
$itemInput.prop('disabled', false);
} else {
$itemLabel.addClass('disabled');
$itemInput.prop('disabled', true);
}
}, this);
},

onQuestionRendered: function() {
this.setReadyStatus();
},

onKeyPress: function(event) {
if (event.which === 13) { //<ENTER> keypress
this.onItemSelected(event);
}
},

onItemFocus: function(event) {
if(this.model.get('_isEnabled') && !this.model.get('_isSubmitted')){
$("label[for='"+$(event.currentTarget).attr('id')+"']").addClass('highlighted');
}
},

onItemBlur: function(event) {
$("label[for='"+$(event.currentTarget).attr('id')+"']").removeClass('highlighted');
},

onItemSelected: function(event) {
if(this.model.get('_isEnabled') && !this.model.get('_isSubmitted')){
var selectedItemObject = this.model.get('_items')[$(event.currentTarget).parent('.component-item').index()];
this.toggleItemSelected(selectedItemObject, event);
}
},

toggleItemSelected:function(item, clickEvent) {
var selectedItems = this.model.get('_selectedItems');
var itemIndex = _.indexOf(this.model.get('_items'), item),
$itemLabel = this.$('label').eq(itemIndex),
$itemInput = this.$('input').eq(itemIndex),
selected = !$itemLabel.hasClass('selected');

if(selected) {
if(this.model.get('_selectable') === 1){
this.$('label').removeClass('selected');
this.$('input').prop('checked', false);
this.deselectAllItems();
selectedItems[0] = item;
} else if(selectedItems.length < this.model.get('_selectable')) {
selectedItems.push(item);
} else {
clickEvent.preventDefault();
return;
}
$itemLabel.addClass('selected');
$itemLabel.a11y_selected(true);
} else {
selectedItems.splice(_.indexOf(selectedItems, item), 1);
$itemLabel.removeClass('selected');
$itemLabel.a11y_selected(false);
}
$itemInput.prop('checked', selected);
item._isSelected = selected;
this.model.set('_selectedItems', selectedItems);
},

// check if the user is allowed to submit the question
canSubmit: function() {
var count = 0;

_.each(this.model.get('_items'), function(item) {
if (item._isSelected) {
count++;
}
}, this);

return (count > 0) ? true : false;

},

// Blank method to add functionality for when the user cannot submit
// Could be used for a popup or explanation dialog/hint
onCannotSubmit: function() {},

// This is important for returning or showing the users answer
// This should preserve the state of the users answers
storeUserAnswer: function() {
var userAnswer = [];

var items = this.model.get('_items').slice(0);
items.sort(function(a, b) {
return a._index - b._index;
});

_.each(items, function(item, index) {
userAnswer.push(item._isSelected);
}, this);
this.model.set('_userAnswer', userAnswer);
},

isCorrect: function() {

var numberOfRequiredAnswers = 0;
var numberOfCorrectAnswers = 0;
var numberOfIncorrectAnswers = 0;

_.each(this.model.get('_items'), function(item, index) {

var itemSelected = (item._isSelected || false);

if (item._shouldBeSelected) {
numberOfRequiredAnswers ++;

if (itemSelected) {
numberOfCorrectAnswers ++;

item._isCorrect = true;

this.model.set('_isAtLeastOneCorrectSelection', true);
}

} else if (!item._shouldBeSelected && itemSelected) {
numberOfIncorrectAnswers ++;
}

}, this);

this.model.set('_numberOfCorrectAnswers', numberOfCorrectAnswers);
this.model.set('_numberOfRequiredAnswers', numberOfRequiredAnswers);

// Check if correct answers matches correct items and there are no incorrect selections
var answeredCorrectly = (numberOfCorrectAnswers === numberOfRequiredAnswers) && (numberOfIncorrectAnswers === 0);
return answeredCorrectly;
},

// Sets the score based upon the questionWeight
// Can be overwritten if the question needs to set the score in a different way
setScore: function() {
var questionWeight = this.model.get("_questionWeight");
var answeredCorrectly = this.model.get('_isCorrect');
var score = answeredCorrectly ? questionWeight : 0;
this.model.set('_score', score);
},

setupFeedback: function() {

if (this.model.get('_isCorrect')) {
this.setupCorrectFeedback();
} else if (this.isPartlyCorrect()) {
this.setupPartlyCorrectFeedback();
} else {
// apply individual item feedback
if((this.model.get('_selectable') === 1) && this.model.get('_selectedItems')[0].feedback) {
this.setupIndividualFeedback(this.model.get('_selectedItems')[0]);
return;
} else {
this.setupIncorrectFeedback();
}
}
},

setupIndividualFeedback: function(selectedItem) {
this.model.set({
feedbackTitle: this.model.get('title'),
feedbackMessage: selectedItem.feedback
});
},

// This is important and should give the user feedback on how they answered the question
// Normally done through ticks and crosses by adding classes
showMarking: function() {
if (!this.model.get('_canShowMarking')) return;

_.each(this.model.get('_items'), function(item, i) {
var $item = this.$('.component-item').eq(i);
$item.removeClass('correct incorrect').addClass(item._isCorrect ? 'correct' : 'incorrect');
}, this);
},

isPartlyCorrect: function() {
return this.model.get('_isAtLeastOneCorrectSelection');
},

resetUserAnswer: function() {
this.model.set({_userAnswer: []});
},

// Used by the question view to reset the look and feel of the component.
resetQuestion: function() {

this.deselectAllItems();
this.resetItems();
},

deselectAllItems: function() {
this.$el.a11y_selected(false);
_.each(this.model.get('_items'), function(item) {
item._isSelected = false;
}, this);
},

resetItems: function() {
this.$('.component-item label').removeClass('selected');
this.$('.component-item').removeClass('correct incorrect');
this.$('input').prop('checked', false);
this.model.set({
_selectedItems: [],
_isAtLeastOneCorrectSelection: false
});
},

showCorrectAnswer: function() {
_.each(this.model.get('_items'), function(item, index) {
this.setOptionSelected(index, item._shouldBeSelected);
}, this);
},

setOptionSelected:function(index, selected) {
var $itemLabel = this.$('label').eq(index);
var $itemInput = this.$('input').eq(index);
if (selected) {
$itemLabel.addClass('selected');
$itemInput.prop('checked', true);
} else {
$itemLabel.removeClass('selected');
$itemInput.prop('checked', false);
}
},

hideCorrectAnswer: function() {
_.each(this.model.get('_items'), function(item, index) {
this.setOptionSelected(index, this.model.get('_userAnswer')[item._index]);
}, this);
},

/**
* used by adapt-contrib-spoor to get the user's answers in the format required by the cmi.interactions.n.student_response data field
* returns the user's answers as a string in the format "1,5,2"
*/
getResponse:function() {
var selected = _.where(this.model.get('_items'), {'_isSelected':true});
var selectedIndexes = _.pluck(selected, '_index');
// indexes are 0-based, we need them to be 1-based for cmi.interactions
for (var i = 0, count = selectedIndexes.length; i < count; i++) {
selectedIndexes[i]++;
}
return selectedIndexes.join(',');
},

/**
* used by adapt-contrib-spoor to get the type of this question in the format required by the cmi.interactions.n.type data field
*/
getResponseType:function() {
return "choice";
}

define([
'core/js/adapt',
'./mcqView',
'./mcqModel'
], function(Adapt, McqView, McqModel) {

return Adapt.register("mcq", {
view: McqView,
model: McqModel
});

Adapt.register("mcq", Mcq);

return Mcq;
});
Loading

0 comments on commit 098c2cd

Please sign in to comment.