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
from StringIO import StringIO
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 .light_children import XBlockWithLightChildren
......@@ -69,6 +69,10 @@ class MentoringBlock(XBlockWithLightChildren):
display_submit = Boolean(help="Allow to submit current block?", default=True, 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)
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'
has_score = True
......@@ -86,7 +90,7 @@ class MentoringBlock(XBlockWithLightChildren):
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_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')
......@@ -145,11 +149,17 @@ class MentoringBlock(XBlockWithLightChildren):
})
self.completed = bool(completed)
if not self.completed and self.max_attempts > 0:
self.num_attempts += 1
return {
'submitResults': submit_results,
'completed': self.completed,
'attempted': self.attempted,
'message': message,
'max_attempts': self.max_attempts,
'num_attempts': self.num_attempts
}
def get_message_fragment(self, message_type):
......
......@@ -26,7 +26,7 @@
import logging
from .light_children import Integer, List, Scope
from .light_children import List, Scope
from .questionnaire import QuestionnaireAbstractBlock
from .utils import render_template
......@@ -43,17 +43,6 @@ class MRQBlock(QuestionnaireAbstractBlock):
An XBlock used to ask multiple-response questions
"""
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):
log.debug(u'Received MRQ submissions: "%s"', submissions)
......@@ -85,26 +74,14 @@ 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 = {
'submissions': submissions,
'completed': completed,
'choices': results,
'message': self.message,
'max_attempts': self.max_attempts,
'num_attempts': self.num_attempts,
'score': sum(1.0 for r in results if r['completed']) / len(results),
'submissions': submissions,
'completed': completed,
'choices': results,
'message': self.message,
'score': sum(1.0 for r in results if r['completed']) / len(results)
}
log.debug(u'MRQ submissions result: %s', result)
......
......@@ -61,3 +61,12 @@
.mentoring .progress .indicator .checkmark-incorrect {
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 @@
margin-right: 5px;
}
.mentoring .mrq-attempts {
display: inline-block;
vertical-align: baseline;
color: #777;
font-style: italic;
webkit-font-smoothing: antialiased;
}
.mentoring .mrq-attempts div {
display: inline-block;
.mentoring .show-answer {
display: none;
}
function MentoringBlock(runtime, element) {
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() {
var data = $('.progress', element).data();
$('.indicator', element).html(progressTemplate(data));
}
function renderAttempts() {
var data = $('.attempts', element).data();
$('.attempts', element).html(attemptsTemplate(data));
}
function renderDependency() {
var warning_dom = $('.missing-dependency', element),
data = warning_dom.data();
......@@ -31,24 +38,37 @@ function MentoringBlock(runtime, element) {
var input = submitResult[0],
result = submitResult[1],
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('attempted', results.attempted ? 'True' : 'False');
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_dom.append(results.message);
if (messages_dom.html().trim()) {
messages_dom.prepend('<div class="title1">Feedback</div>');
messages_dom.show();
}
validateXBlock();
}
function getChildren(element) {
var children_dom = $('.xblock-light-child', element),
children = [];
if (!_.isUndefined(children))
return children;
var children_dom = $('.xblock-light-child', element);
children = [];
$.each(children_dom, function(index, child_dom) {
var child_type = $(child_dom).attr('data-type'),
......@@ -99,13 +119,15 @@ function MentoringBlock(runtime, element) {
callIfExists(child, 'init', options);
});
validateXBlock();
if (submit_dom.length) {
renderProgress();
}
renderAttempts();
renderDependency();
validateXBlock();
}
function handleRefreshResults(results) {
......@@ -121,21 +143,26 @@ function MentoringBlock(runtime, element) {
// validate all children
function validateXBlock() {
var submit_dom = $(element).find('.submit .input-main');
var children_are_valid = true;
var data = {};
var is_valid = true;
var data = $('.attempts', element).data();
var children = getChildren(element);
for (var i = 0; i < children.length; i++) {
var child = children[i];
if (child.name !== undefined) {
var child_validation = callIfExists(child, 'validate');
if (_.isBoolean(child_validation)) {
children_are_valid = children_are_valid && child_validation
if ((data.max_attempts > 0) && (data.num_attempts >= data.max_attempts)) {
is_valid = false;
}
else {
for (var i = 0; i < children.length; i++) {
var child = children[i];
if (child.name !== undefined) {
var child_validation = callIfExists(child, 'validate');
if (_.isBoolean(child_validation)) {
is_valid = is_valid && child_validation;
}
}
}
}
if (!children_are_valid) {
if (!is_valid) {
submit_dom.attr('disabled','disabled');
}
else {
......
......@@ -91,23 +91,11 @@ function MCQBlock(runtime, element) {
}
function MRQBlock(runtime, element) {
var mrqAttemptsTemplate = _.template($('#xblock-mrq-attempts').html());
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() {
this.renderAttempts();
var answerButton = $('button', element);
answerButton.on('click', _.bind(this.toggleAnswers, this));
this.showAnswerButton();
},
submit: function() {
......@@ -125,7 +113,7 @@ function MRQBlock(runtime, element) {
return checkedValues;
},
handleSubmit: function(result) {
handleSubmit: function(result, options) {
var messageView = MessageView(element);
if (result.message) {
......@@ -149,8 +137,8 @@ function MRQBlock(runtime, element) {
});
choiceResultDOM.removeClass('incorrect icon-exclamation correct icon-ok');
/* show hint if checked or max_attempts is disabled */
if (result.completed || choiceInputDOM.prop('checked') || result.max_attempts <= 0) {
/* show hint if checked or max_attempts is disabled */
if (result.completed || choiceInputDOM.prop('checked') || options.max_attempts <= 0) {
if (choice.completed) {
choiceResultDOM.addClass('correct icon-ok');
} else if (!choice.completed) {
......@@ -170,10 +158,21 @@ function MRQBlock(runtime, element) {
});
});
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);
this.renderAttempts();
if (is_enabled && _.isArray(this.answers)) {
button.show();
}
else {
button.hide();
}
},
toggleAnswers: function() {
......
......@@ -7,6 +7,7 @@
{{c.body_html|safe}}
{% endfor %}
{% if self.display_submit %}
<div class="attempts" data-max_attempts="{{ self.max_attempts }}" data-num_attempts="{{ self.num_attempts }}"></div>
<div class="submit">
<input type="button" class="input-main" value="Submit"></input>
<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 @@
<div class="choice-message"></div>
</div>
</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