Commit 072ab3ed by Sven Marnach

Add maximum number of attempts and Save button.

parent 29bb31c7
......@@ -78,6 +78,12 @@ class ActiveTableXBlock(StudioEditableXBlockMixin, XBlock):
scope=Scope.settings,
default=1.0,
)
max_attempts = Integer(
display_name='Maximum attempts',
help='Defines the number of times a student can try to answer this problem. If the value '
'is not set, infinite attempts are allowed.',
scope=Scope.settings,
)
editable_fields = [
'display_name',
......@@ -87,17 +93,35 @@ class ActiveTableXBlock(StudioEditableXBlockMixin, XBlock):
'row_heights',
'default_tolerance',
'max_score',
'max_attempts',
]
# Dictionary mapping cell ids to the student answers.
answers = Dict(scope=Scope.user_state)
# Number of correct answers.
num_correct_answers = Integer(scope=Scope.user_state)
# Dictionary mapping cell ids to Boolean values indicating whether the cell was answered
# correctly at the last check.
answers_correct = Dict(scope=Scope.user_state, default=None)
# The number of points awarded.
score = Float(scope=Scope.user_state)
# The number of attempts used.
attempts = Integer(scope=Scope.user_state, default=0)
has_score = True
@property
def num_correct_answers(self):
"""The number of correct answers during the last check."""
if self.answers_correct is None:
return None
return sum(self.answers_correct.itervalues())
@property
def num_total_answers(self):
"""The total number of answers during the last check."""
if self.answers_correct is None:
return None
return len(self.answers_correct)
def parse_fields(self):
"""Parse the user-provided fields into more processing-friendly structured data."""
if self.table_definition:
......@@ -130,16 +154,23 @@ class ActiveTableXBlock(StudioEditableXBlockMixin, XBlock):
cell.id = 'cell_{}_{}'.format(cell.index, row['index'])
if not cell.is_static:
self.response_cells[cell.id] = cell
cell.classes = 'active'
cell.value = self.answers.get(cell.id)
cell.height = height - 2
if isinstance(cell, NumericCell) and cell.abs_tolerance is None:
cell.set_tolerance(self.default_tolerance)
if cell.value is None:
cell.classes = 'active unchecked'
elif cell.check_response(cell.value):
cell.classes = 'active right-answer'
else:
cell.classes = 'active wrong-answer'
def get_status(self):
"""Status dictionary passed to the frontend code."""
return dict(
answers_correct=self.answers_correct,
num_correct_answers=self.num_correct_answers,
num_total_answers=self.num_total_answers,
score=self.score,
max_score=self.max_score,
attempts=self.attempts,
max_attempts=self.max_attempts,
)
def student_view(self, context=None):
"""Render the table."""
......@@ -153,6 +184,7 @@ class ActiveTableXBlock(StudioEditableXBlockMixin, XBlock):
head_height=self._row_heights[0] if self._row_heights else None,
thead=self.thead,
tbody=self.tbody,
max_attempts=self.max_attempts,
)
html = loader.render_template('templates/html/activetable.html', context)
......@@ -166,39 +198,45 @@ class ActiveTableXBlock(StudioEditableXBlockMixin, XBlock):
frag = Fragment(html)
frag.add_css(css)
frag.add_javascript(loader.load_unicode('static/js/src/activetable.js'))
frag.initialize_js('ActiveTableXBlock', dict(
num_correct_answers=self.num_correct_answers,
num_total_answers=len(self.answers) if self.answers is not None else None,
score=self.score,
max_score=self.max_score,
))
frag.initialize_js('ActiveTableXBlock', self.get_status())
return frag
@XBlock.json_handler
def check_answers(self, data, unused_suffix=''):
"""Check the answers given by the student.
This handler is called when the "Check" button is clicked.
"""
def check_and_save_answers(self, data):
"""Common implementation for the check and save handlers."""
if self.max_attempts and self.attempts >= self.max_attempts:
# The "Check" button is hidden when the maximum number of attempts has been reached, so
# we can only get here by manually crafted requests. We simply return the current
# status without rechecking or storing the answers in that case.
return self.get_status()
self.parse_fields()
self.postprocess_table()
correct = {
answers_correct = {
cell_id: self.response_cells[cell_id].check_response(value)
for cell_id, value in data.iteritems()
}
# Since the previous statement executed without error, the data is well-formed enough to be
# stored. We now know it's a dictionary and all the keys are valid cell ids.
self.answers = data
self.num_correct_answers = sum(correct.itervalues())
self.score = self.num_correct_answers * self.max_score / len(correct)
return answers_correct
@XBlock.json_handler
def check_answers(self, data, unused_suffix=''):
"""Check the answers given by the student.
This handler is called when the "Check" button is clicked.
"""
self.answers_correct = self.check_and_save_answers(data)
self.attempts += 1
self.score = self.num_correct_answers * self.max_score / len(self.answers_correct)
self.runtime.publish(self, 'grade', dict(value=self.score, max_value=self.max_score))
return dict(
correct=correct,
num_correct_answers=self.num_correct_answers,
num_total_answers=len(correct),
score=self.score,
max_score=self.max_score,
)
return self.get_status()
@XBlock.json_handler
def save_answers(self, data, unused_suffix=''):
"""Save the answers given by the student without checking them."""
self.check_and_save_answers(data)
self.answers_correct = None
return self.get_status()
def validate_field_data(self, validation, data):
"""Validate the data entered by the user.
......
......@@ -2,49 +2,84 @@
function ActiveTableXBlock(runtime, element, init_args) {
var checkHandlerUrl = runtime.handlerUrl(element, 'check_answers');
var saveHandlerUrl = runtime.handlerUrl(element, 'save_answers');
function updateStatus(data) {
function markResponseCells(data) {
if (data.answers_correct) {
$.each(data.answers_correct, function(cell_id, correct) {
var $cell = $('#' + cell_id, element);
$cell.removeClass('right-answer wrong-answer unchecked');
if (correct) {
$cell.addClass('right-answer');
$cell.prop('title', 'correct');
} else {
$cell.addClass('wrong-answer');
$cell.prop('title', 'incorrect');
}
});
} else {
$('td.active', element).removeClass('right-answer wrong-answer').addClass('unchecked');
}
}
function updateStatusMessage(data) {
var $status = $('.status', element);
var $status_message = $('.status-message', element);
if (data.num_total_answers == data.num_correct_answers) {
if (!data.answers_correct) {
$status.removeClass('incorrect correct');
$status.text('unanswered');
$status_message.text('');
}
else if (data.num_total_answers == data.num_correct_answers) {
$status.removeClass('incorrect').addClass('correct');
$status.text('correct');
$status_message.text('Great job! (' + data.score + '/' + data.max_score + ' points)');
$status_message.text('Great job! ');
} else {
$status.removeClass('correct').addClass('incorrect');
$status.text('incorrect');
$status_message.text(
'You have ' + data.num_correct_answers + ' out of ' + data.num_total_answers +
' cells correct. (' + data.score + '/' + data.max_score + ' points)'
' cells correct.'
);
}
}
function markResponseCells(data) {
$.each(data.correct, function(cell_id, correct) {
var $cell = $('#' + cell_id, element);
$cell.removeClass('right-answer wrong-answer unchecked');
if (correct) {
$cell.addClass('right-answer');
$cell.prop('title', 'correct');
function updateFeedback(data) {
var feedback_msg;
if (data.score === null) {
feedback_msg = '(' + data.max_score + ' points possible)';
} else {
$cell.addClass('wrong-answer');
$cell.prop('title', 'incorrect');
feedback_msg = '(' + data.score + '/' + data.max_score + ' points)';
}
});
updateStatus(data);
if (data.max_attempts) {
feedback_msg = 'You have used ' + data.attempts + ' of ' + data.max_attempts +
' submissions ' + feedback_msg;
if (data.attempts == data.max_attempts - 1) {
$('.action .check .check-label', element).text('Final check');
}
else if (data.attempts >= data.max_attempts) {
$('.action .check, .action .save', element).hide();
}
}
$('.submission-feedback', element).text(feedback_msg);
}
function updateStatus(data) {
markResponseCells(data);
updateStatusMessage(data);
updateFeedback(data);
}
function checkAnswers(e) {
answers = {};
function callHandler(url) {
var answers = {};
$('td.active', element).each(function() {
answers[this.id] = $('input', this).val();
});
$.ajax({
type: "POST",
url: checkHandlerUrl,
url: url,
data: JSON.stringify(answers),
success: markResponseCells,
success: updateStatus,
});
}
......@@ -55,6 +90,7 @@ function ActiveTableXBlock(runtime, element, init_args) {
}
$('#activetable-help-button', element).click(toggleHelp);
$('.action .check', element).click(checkAnswers);
if (init_args.num_total_answers) updateStatus(init_args);
$('.action .check', element).click(function (e) { callHandler(checkHandlerUrl); });
$('.action .save', element).click(function (e) { callHandler(saveHandlerUrl); });
updateStatus(init_args);
}
......@@ -110,8 +110,14 @@
.activetable_block .status-message {
margin: 0.5em 0 1.5em;
}
.activetable_block .action .check {
.activetable_block .action button {
height: 40px;
margin-right: 10px;
font-weight: 600;
text-transform: uppercase;
}
.activetable_block .submission-feedback {
display: inline-block;
font-style: italic;
margin-left: 10px;
}
......@@ -35,9 +35,13 @@
{% else %}
<p>This component isn't configured properly and can't be displayed.</p>
{% endif %}
<p class="status">unanswered</p>
<p class="status"></p>
<div class="status-message"></div>
<div class="action">
<button class="check"><span class="check-label">Check</span><span class="sr"> your answer</span></button>
{% if max_attempts %}
<button class="save">Save<span class="sr"> your answer</span></button>
<div class="submission-feedback"></div>
{% endif %}
</div>
</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