Commit 9a8c20db by Xavier Antoviaque

Merge pull request #12 from aboudreault/global-max-attempts

Global max attempts
parents cb899cb7 e4831015
...@@ -30,7 +30,7 @@ from lxml import etree ...@@ -30,7 +30,7 @@ from lxml import etree
from StringIO import StringIO from StringIO import StringIO
from xblock.core import XBlock from xblock.core import XBlock
from xblock.fields import Boolean, Scope, String, Float from xblock.fields import Boolean, Scope, String, Integer, Float
from xblock.fragment import Fragment from xblock.fragment import Fragment
from .light_children import XBlockWithLightChildren from .light_children import XBlockWithLightChildren
...@@ -69,6 +69,10 @@ class MentoringBlock(XBlockWithLightChildren): ...@@ -69,6 +69,10 @@ class MentoringBlock(XBlockWithLightChildren):
display_submit = Boolean(help="Allow to submit current block?", default=True, scope=Scope.content) display_submit = Boolean(help="Allow to submit current block?", default=True, scope=Scope.content)
xml_content = String(help="XML content", default='', scope=Scope.content) xml_content = String(help="XML content", default='', scope=Scope.content)
weight = Float(help="Defines the maximum total grade of the block.", default=0, scope=Scope.content) weight = Float(help="Defines the maximum total grade of the block.", default=0, scope=Scope.content)
num_attempts = Integer(help="Number of attempts a user has answered for this questions",
default=0, scope=Scope.user_state)
max_attempts = Integer(help="Number of max attempts for this questions", default=0,
scope=Scope.content)
icon_class = 'problem' icon_class = 'problem'
has_score = True has_score = True
...@@ -86,7 +90,7 @@ class MentoringBlock(XBlockWithLightChildren): ...@@ -86,7 +90,7 @@ class MentoringBlock(XBlockWithLightChildren):
self.runtime.local_resource_url(self, 'public/js/vendor/underscore-min.js')) self.runtime.local_resource_url(self, 'public/js/vendor/underscore-min.js'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/mentoring.js')) fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/mentoring.js'))
fragment.add_resource(load_resource('templates/html/mentoring_progress.html'), "text/html") fragment.add_resource(load_resource('templates/html/mentoring_progress.html'), "text/html")
fragment.add_resource(load_resource('templates/html/mrqblock_attempts.html'), "text/html") fragment.add_resource(load_resource('templates/html/mentoring_attempts.html'), "text/html")
fragment.initialize_js('MentoringBlock') fragment.initialize_js('MentoringBlock')
...@@ -145,11 +149,17 @@ class MentoringBlock(XBlockWithLightChildren): ...@@ -145,11 +149,17 @@ class MentoringBlock(XBlockWithLightChildren):
}) })
self.completed = bool(completed) self.completed = bool(completed)
if not self.completed and self.max_attempts > 0:
self.num_attempts += 1
return { return {
'submitResults': submit_results, 'submitResults': submit_results,
'completed': self.completed, 'completed': self.completed,
'attempted': self.attempted, 'attempted': self.attempted,
'message': message, 'message': message,
'max_attempts': self.max_attempts,
'num_attempts': self.num_attempts
} }
def get_message_fragment(self, message_type): def get_message_fragment(self, message_type):
......
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
import logging import logging
from .light_children import Integer, List, Scope from .light_children import List, Scope
from .questionnaire import QuestionnaireAbstractBlock from .questionnaire import QuestionnaireAbstractBlock
from .utils import render_template from .utils import render_template
...@@ -43,17 +43,6 @@ class MRQBlock(QuestionnaireAbstractBlock): ...@@ -43,17 +43,6 @@ class MRQBlock(QuestionnaireAbstractBlock):
An XBlock used to ask multiple-response questions An XBlock used to ask multiple-response questions
""" """
student_choices = List(help="Last submissions by the student", default=[], scope=Scope.user_state) student_choices = List(help="Last submissions by the student", default=[], scope=Scope.user_state)
max_attempts = Integer(help="Number of max attempts for this questions", default=0,
scope=Scope.content)
num_attempts = Integer(help="Number of attempts a user has answered for this questions",
default=0, scope=Scope.user_state)
# TODO REMOVE THIS, ONLY NEEDED FOR LIGHTCHILDREN
@classmethod
def get_fields_to_save(cls):
return [
'num_attempts'
]
def submit(self, submissions): def submit(self, submissions):
log.debug(u'Received MRQ submissions: "%s"', submissions) log.debug(u'Received MRQ submissions: "%s"', submissions)
...@@ -85,16 +74,6 @@ class MRQBlock(QuestionnaireAbstractBlock): ...@@ -85,16 +74,6 @@ class MRQBlock(QuestionnaireAbstractBlock):
}), }),
}) })
self.message = u'Your answer is correct!' if completed else u'Your answer is incorrect.'
# Do not increase the counter is the answer is correct
if not completed:
setattr(self, 'num_attempts', self.num_attempts + 1)
if self.max_attempts > 0 and self.num_attempts >= self.max_attempts:
completed = True
self.message += u' You have reached the maximum number of attempts for this question. ' \
u'Your next answers won''t be saved. You can check the answer(s) using the "Show Answer(s)" button.'
else:
self.student_choices = submissions self.student_choices = submissions
result = { result = {
...@@ -102,9 +81,7 @@ class MRQBlock(QuestionnaireAbstractBlock): ...@@ -102,9 +81,7 @@ class MRQBlock(QuestionnaireAbstractBlock):
'completed': completed, 'completed': completed,
'choices': results, 'choices': results,
'message': self.message, 'message': self.message,
'max_attempts': self.max_attempts, 'score': sum(1.0 for r in results if r['completed']) / len(results)
'num_attempts': self.num_attempts,
'score': sum(1.0 for r in results if r['completed']) / len(results),
} }
log.debug(u'MRQ submissions result: %s', result) log.debug(u'MRQ submissions result: %s', result)
......
...@@ -61,3 +61,12 @@ ...@@ -61,3 +61,12 @@
.mentoring .progress .indicator .checkmark-incorrect { .mentoring .progress .indicator .checkmark-incorrect {
color: #ff0000; color: #ff0000;
} }
.mentoring .attempts {
margin-top: 20px;
display: inline-block;
vertical-align: baseline;
color: #777;
font-style: italic;
webkit-font-smoothing: antialiased;
}
...@@ -82,14 +82,6 @@ ...@@ -82,14 +82,6 @@
margin-right: 5px; margin-right: 5px;
} }
.mentoring .mrq-attempts { .mentoring .show-answer {
display: inline-block; display: none;
vertical-align: baseline;
color: #777;
font-style: italic;
webkit-font-smoothing: antialiased;
}
.mentoring .mrq-attempts div {
display: inline-block;
} }
function MentoringBlock(runtime, element) { function MentoringBlock(runtime, element) {
var progressTemplate = _.template($('#xblock-progress-template').html()); var progressTemplate = _.template($('#xblock-progress-template').html());
var attemptsTemplate = _.template($('#xblock-attempts-template').html());
var children; // Keep track of children. A Child need a single object scope for its data.
function renderProgress() { function renderProgress() {
var data = $('.progress', element).data(); var data = $('.progress', element).data();
$('.indicator', element).html(progressTemplate(data)); $('.indicator', element).html(progressTemplate(data));
} }
function renderAttempts() {
var data = $('.attempts', element).data();
$('.attempts', element).html(attemptsTemplate(data));
}
function renderDependency() { function renderDependency() {
var warning_dom = $('.missing-dependency', element), var warning_dom = $('.missing-dependency', element),
data = warning_dom.data(); data = warning_dom.data();
...@@ -31,23 +38,36 @@ function MentoringBlock(runtime, element) { ...@@ -31,23 +38,36 @@ function MentoringBlock(runtime, element) {
var input = submitResult[0], var input = submitResult[0],
result = submitResult[1], result = submitResult[1],
child = getChildByName(element, input); child = getChildByName(element, input);
callIfExists(child, 'handleSubmit', result); var options = {
max_attempts: results.max_attempts,
num_attempts: results.num_attempts
}
callIfExists(child, 'handleSubmit', result, options);
}); });
$('.progress', element).data('completed', results.completed ? 'True' : 'False'); $('.progress', element).data('completed', results.completed ? 'True' : 'False');
$('.progress', element).data('attempted', results.attempted ? 'True' : 'False'); $('.progress', element).data('attempted', results.attempted ? 'True' : 'False');
renderProgress(); renderProgress();
$('.attempts', element).data('max_attempts', results.max_attempts);
$('.attempts', element).data('num_attempts', results.num_attempts);
renderAttempts();
// Messages should only be displayed upon hitting 'submit', not on page reload // Messages should only be displayed upon hitting 'submit', not on page reload
messages_dom.append(results.message); messages_dom.append(results.message);
if (messages_dom.html().trim()) { if (messages_dom.html().trim()) {
messages_dom.prepend('<div class="title1">Feedback</div>'); messages_dom.prepend('<div class="title1">Feedback</div>');
messages_dom.show(); messages_dom.show();
} }
validateXBlock();
} }
function getChildren(element) { function getChildren(element) {
var children_dom = $('.xblock-light-child', element), if (!_.isUndefined(children))
return children;
var children_dom = $('.xblock-light-child', element);
children = []; children = [];
$.each(children_dom, function(index, child_dom) { $.each(children_dom, function(index, child_dom) {
...@@ -99,13 +119,15 @@ function MentoringBlock(runtime, element) { ...@@ -99,13 +119,15 @@ function MentoringBlock(runtime, element) {
callIfExists(child, 'init', options); callIfExists(child, 'init', options);
}); });
validateXBlock();
if (submit_dom.length) { if (submit_dom.length) {
renderProgress(); renderProgress();
} }
renderAttempts();
renderDependency(); renderDependency();
validateXBlock();
} }
function handleRefreshResults(results) { function handleRefreshResults(results) {
...@@ -121,21 +143,26 @@ function MentoringBlock(runtime, element) { ...@@ -121,21 +143,26 @@ function MentoringBlock(runtime, element) {
// validate all children // validate all children
function validateXBlock() { function validateXBlock() {
var submit_dom = $(element).find('.submit .input-main'); var submit_dom = $(element).find('.submit .input-main');
var children_are_valid = true; var is_valid = true;
var data = {}; var data = $('.attempts', element).data();
var children = getChildren(element); var children = getChildren(element);
if ((data.max_attempts > 0) && (data.num_attempts >= data.max_attempts)) {
is_valid = false;
}
else {
for (var i = 0; i < children.length; i++) { for (var i = 0; i < children.length; i++) {
var child = children[i]; var child = children[i];
if (child.name !== undefined) { if (child.name !== undefined) {
var child_validation = callIfExists(child, 'validate'); var child_validation = callIfExists(child, 'validate');
if (_.isBoolean(child_validation)) { if (_.isBoolean(child_validation)) {
children_are_valid = children_are_valid && child_validation is_valid = is_valid && child_validation;
}
} }
} }
} }
if (!children_are_valid) { if (!is_valid) {
submit_dom.attr('disabled','disabled'); submit_dom.attr('disabled','disabled');
} }
else { else {
......
...@@ -91,23 +91,11 @@ function MCQBlock(runtime, element) { ...@@ -91,23 +91,11 @@ function MCQBlock(runtime, element) {
} }
function MRQBlock(runtime, element) { function MRQBlock(runtime, element) {
var mrqAttemptsTemplate = _.template($('#xblock-mrq-attempts').html());
return { return {
renderAttempts: function() {
var data = $('.mrq-attempts', element).data();
$('.mrq-attempts', element).html(mrqAttemptsTemplate(data));
// bind show answer button
var showAnswerButton = $('button', element);
if (showAnswerButton.length != 0) {
if (_.isUndefined(this.answers))
showAnswerButton.hide();
else
showAnswerButton.on('click', _.bind(this.toggleAnswers, this));
}
},
init: function() { init: function() {
this.renderAttempts(); var answerButton = $('button', element);
answerButton.on('click', _.bind(this.toggleAnswers, this));
this.showAnswerButton();
}, },
submit: function() { submit: function() {
...@@ -125,7 +113,7 @@ function MRQBlock(runtime, element) { ...@@ -125,7 +113,7 @@ function MRQBlock(runtime, element) {
return checkedValues; return checkedValues;
}, },
handleSubmit: function(result) { handleSubmit: function(result, options) {
var messageView = MessageView(element); var messageView = MessageView(element);
if (result.message) { if (result.message) {
...@@ -150,7 +138,7 @@ function MRQBlock(runtime, element) { ...@@ -150,7 +138,7 @@ function MRQBlock(runtime, element) {
choiceResultDOM.removeClass('incorrect icon-exclamation correct icon-ok'); choiceResultDOM.removeClass('incorrect icon-exclamation correct icon-ok');
/* show hint if checked or max_attempts is disabled */ /* show hint if checked or max_attempts is disabled */
if (result.completed || choiceInputDOM.prop('checked') || result.max_attempts <= 0) { if (result.completed || choiceInputDOM.prop('checked') || options.max_attempts <= 0) {
if (choice.completed) { if (choice.completed) {
choiceResultDOM.addClass('correct icon-ok'); choiceResultDOM.addClass('correct icon-ok');
} else if (!choice.completed) { } else if (!choice.completed) {
...@@ -170,10 +158,21 @@ function MRQBlock(runtime, element) { ...@@ -170,10 +158,21 @@ function MRQBlock(runtime, element) {
}); });
}); });
this.answers = answers; this.answers = answers;
this.showAnswerButton(options);
},
showAnswerButton: function(options) {
var button = $('.show-answer', element);
var is_enabled = options && (options.num_attempts >= options.max_attempts);
$('.mrq-attempts', element).data('num_attempts', result.num_attempts); if (is_enabled && _.isArray(this.answers)) {
this.renderAttempts(); button.show();
}
else {
button.hide();
}
}, },
toggleAnswers: function() { toggleAnswers: function() {
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
{{c.body_html|safe}} {{c.body_html|safe}}
{% endfor %} {% endfor %}
{% if self.display_submit %} {% if self.display_submit %}
<div class="attempts" data-max_attempts="{{ self.max_attempts }}" data-num_attempts="{{ self.num_attempts }}"></div>
<div class="submit"> <div class="submit">
<input type="button" class="input-main" value="Submit"></input> <input type="button" class="input-main" value="Submit"></input>
<span class="progress" data-completed="{{ self.completed }}" data-attempted="{{ self.attempted }}"> <span class="progress" data-completed="{{ self.completed }}" data-attempted="{{ self.attempted }}">
......
<script type="text/template" id="xblock-attempts-template">
<% if (_.isNumber(max_attempts) && max_attempts > 0) {{ %>
<div> You have used <%= _.min([num_attempts, max_attempts]) %> of <%= max_attempts %> attempts.</div>
<% }} %>
</script>
<script type="text/template" id="xblock-mrq-attempts">
<% if (_.isNumber(max_attempts) && max_attempts > 0) {{ %>
<% if (num_attempts >= max_attempts) {{ %>
<button class="show">
<span class="show-label">Show Answer(s)</span>
</button>
<% }} %>
<div> You have used <%= _.min([num_attempts, max_attempts]) %> of <%= max_attempts %> attempts for this question.</div>
<% }} %>
</script>
...@@ -21,4 +21,8 @@ ...@@ -21,4 +21,8 @@
<div class="choice-message"></div> <div class="choice-message"></div>
</div> </div>
</fieldset> </fieldset>
<div class="mrq-attempts" data-max_attempts="{{ self.max_attempts }}" data-num_attempts="{{ self.num_attempts }}"></div> <div class="show-answer">
<button class="show">
<span class="show-label">Show Answer(s)</span>
</button>
</div>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment