Commit e1950513 by Tim Krones

Merge pull request #63 from open-craft/step-navigation

Step navigation and submission for new mentoring block
parents f839d587 6c0390d5
......@@ -9,3 +9,4 @@ Alan Boudreault <boudreault.alan@gmail.com>
Eugeny Kolpakov <eugeny@opencraft.com>
Braden MacDonald <braden@opencraft.com>
Jonathan Piacenti <jonathan@opencraft.com>
Tim Krones <tim@opencraft.com>
......@@ -834,6 +834,14 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes
scope=Scope.settings
)
# User state
active_step = Integer(
# Keep track of the student progress.
default=0,
scope=Scope.user_state,
enforce_type=True
)
editable_fields = ('display_name',)
@lazy
......@@ -854,27 +862,31 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes
def student_view(self, context):
fragment = Fragment()
child_content = u""
children_contents = []
for child_id in self.children:
child = self.runtime.get_block(child_id)
if child is None: # child should not be None but it can happen due to bugs or permission issues
child_content += u"<p>[{}]</p>".format(self._(u"Error: Unable to load child component."))
child_content = u"<p>[{}]</p>".format(self._(u"Error: Unable to load child component."))
elif not isinstance(child, MentoringMessageBlock):
child_fragment = self._render_child_fragment(child, context, view='mentoring_view')
fragment.add_frag_resources(child_fragment)
child_content += child_fragment.content
child_content = child_fragment.content
children_contents.append(child_content)
fragment.add_content(loader.render_template('templates/html/mentoring.html', {
fragment.add_content(loader.render_template('templates/html/mentoring_with_steps.html', {
'self': self,
'title': self.display_name,
'show_title': self.show_title,
'child_content': child_content,
'children_contents': children_contents,
}))
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder.css'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/mentoring_with_steps.js'))
self.include_theme_files(fragment)
fragment.initialize_js('MentoringWithStepsBlock')
return fragment
@property
......@@ -897,6 +909,27 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes
NestedXBlockSpec(OnAssessmentReviewMentoringMessageShim, boilerplate='on-assessment-review'),
]
@XBlock.json_handler
def update_active_step(self, new_value, suffix=''):
if new_value < len(self.steps):
self.active_step = new_value
return {
'active_step': self.active_step
}
@XBlock.json_handler
def try_again(self, data, suffix=''):
self.active_step = 0
step_blocks = [self.runtime.get_block(child_id) for child_id in self.steps]
for step in step_blocks:
step.reset()
return {
'active_step': self.active_step
}
def author_edit_view(self, context):
"""
Add some HTML to the author view that allows authors to add child blocks.
......
function MentoringWithStepsBlock(runtime, element) {
var steps = runtime.children(element).filter(
function(c) { return c.element.className.indexOf('pb-mentoring-step') > -1; }
);
var activeStep = $('.mentoring', element).data('active-step');
var checkmark, submitDOM, nextDOM, tryAgainDOM, submitXHR;
function isLastStep() {
return (activeStep === steps.length-1);
}
function updateActiveStep(newValue) {
var handlerUrl = runtime.handlerUrl(element, 'update_active_step');
$.post(handlerUrl, JSON.stringify(newValue))
.success(function(response) {
activeStep = response.active_step;
});
}
function handleResults(response) {
// Update active step so next step is shown on page reload (even if user does not click "Next Step")
updateActiveStep(activeStep+1);
// Update UI
if (response.completed === 'correct') {
checkmark.addClass('checkmark-correct icon-ok fa-check');
} else if (response.completed === 'partial') {
checkmark.addClass('checkmark-partially-correct icon-ok fa-check');
} else {
checkmark.addClass('checkmark-incorrect icon-exclamation fa-exclamation');
}
submitDOM.attr('disabled', 'disabled');
nextDOM.removeAttr("disabled");
if (nextDOM.is(':visible')) { nextDOM.focus(); }
if (isLastStep()) {
tryAgainDOM.removeAttr('disabled');
tryAgainDOM.show();
}
}
function submit() {
// We do not handle submissions at this level, so just forward to "submit" method of active step
var step = steps[activeStep];
step.submit(handleResults);
}
function hideAllSteps() {
for (var i=0; i < steps.length; i++) {
$(steps[i].element).hide();
}
}
function cleanAll() {
checkmark.removeClass('checkmark-correct icon-ok fa-check');
checkmark.removeClass('checkmark-partially-correct icon-ok fa-check');
checkmark.removeClass('checkmark-incorrect icon-exclamation fa-exclamation');
hideAllSteps();
}
function updateDisplay() {
cleanAll();
showActiveStep();
nextDOM.attr('disabled', 'disabled');
validateXBlock();
}
function showActiveStep() {
var step = steps[activeStep];
$(step.element).show();
}
function onChange() {
// We do not allow users to modify answers belonging to a step after submitting them:
// Once an answer has been submitted ("Next Step" button is enabled),
// start ignoring changes to the answer.
if (nextDOM.attr('disabled')) {
validateXBlock();
}
}
function validateXBlock() {
var isValid = true;
var step = steps[activeStep];
if (step) {
isValid = step.validate();
}
if (!isValid) {
submitDOM.attr('disabled', 'disabled');
} else {
submitDOM.removeAttr('disabled');
}
if (isLastStep()) {
nextDOM.hide();
}
}
function initSteps(options) {
for (var i=0; i < steps.length; i++) {
var step = steps[i];
step.initChildren(options);
}
}
function handleTryAgain(result) {
activeStep = result.active_step;
updateDisplay();
tryAgainDOM.hide();
submitDOM.show();
if (! isLastStep()) {
nextDOM.show();
}
}
function tryAgain() {
var handlerUrl = runtime.handlerUrl(element, 'try_again');
if (submitXHR) {
submitXHR.abort();
}
submitXHR = $.post(handlerUrl, JSON.stringify({})).success(handleTryAgain);
}
function initXBlockView() {
checkmark = $('.assessment-checkmark', element);
submitDOM = $(element).find('.submit .input-main');
submitDOM.on('click', submit);
submitDOM.show();
nextDOM = $(element).find('.submit .input-next');
nextDOM.on('click', updateDisplay);
nextDOM.show();
tryAgainDOM = $(element).find('.submit .input-try-again');
tryAgainDOM.on('click', tryAgain);
var options = {
onChange: onChange
};
initSteps(options);
updateDisplay();
}
initXBlockView();
}
function MentoringStepBlock(runtime, element) {
var children = runtime.children(element);
var submitXHR;
function callIfExists(obj, fn) {
if (typeof obj !== 'undefined' && typeof obj[fn] == 'function') {
return obj[fn].apply(obj, Array.prototype.slice.call(arguments, 2));
} else {
return null;
}
}
return {
initChildren: function(options) {
for (var i=0; i < children.length; i++) {
var child = children[i];
callIfExists(child, 'init', options);
}
},
validate: function() {
var is_valid = true;
for (var i = 0; i < children.length; i++) {
var child = children[i];
if (child && child.name !== undefined) {
var child_validation = callIfExists(child, 'validate');
if (_.isBoolean(child_validation)) {
is_valid = is_valid && child_validation;
}
}
}
return is_valid;
},
submit: function(result_handler) {
var handler_name = 'submit';
var data = {};
for (var i = 0; i < children.length; i++) {
var child = children[i];
if (child && child.name !== undefined && typeof(child[handler_name]) !== "undefined") {
data[child.name.toString()] = child[handler_name]();
}
}
var handlerUrl = runtime.handlerUrl(element, handler_name);
if (submitXHR) {
submitXHR.abort();
}
submitXHR = $.post(handlerUrl, JSON.stringify(data))
.success(function(response) {
result_handler(response);
});
}
};
}
......@@ -18,12 +18,13 @@
# "AGPLv3". If not, see <http://www.gnu.org/licenses/>.
#
import logging
from lazy.lazy import lazy
from xblock.core import XBlock
from xblock.fields import String, Boolean, Scope
from xblock.fields import String, List, Scope
from xblock.fragment import Fragment
from xblockutils.helpers import child_isinstance
from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable import (
NestedXBlockSpec, StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin, XBlockWithPreviewMixin
......@@ -31,11 +32,12 @@ from xblockutils.studio_editable import (
from problem_builder.answer import AnswerBlock, AnswerRecapBlock
from problem_builder.mcq import MCQBlock, RatingBlock
from problem_builder.mixins import EnumerableChildMixin
from problem_builder.mixins import EnumerableChildMixin, StepParentMixin
from problem_builder.mrq import MRQBlock
from problem_builder.table import MentoringTableBlock
log = logging.getLogger(__name__)
loader = ResourceLoader(__name__)
......@@ -56,6 +58,12 @@ def _normalize_id(key):
return key
class Correctness(object):
CORRECT = 'correct'
PARTIAL = 'partial'
INCORRECT = 'incorrect'
class HtmlBlockShim(object):
CATEGORY = 'html'
STUDIO_LABEL = _(u"HTML")
......@@ -64,7 +72,7 @@ class HtmlBlockShim(object):
@XBlock.needs('i18n')
class MentoringStepBlock(
StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin, XBlockWithPreviewMixin,
EnumerableChildMixin, XBlock
EnumerableChildMixin, StepParentMixin, XBlock
):
"""
An XBlock for a step.
......@@ -73,7 +81,7 @@ class MentoringStepBlock(
STUDIO_LABEL = _(u"Mentoring Step")
CATEGORY = 'pb-mentoring-step'
# Fields:
# Settings
display_name = String(
display_name=_("Step Title"),
help=_('Leave blank to use sequential numbering'),
......@@ -81,6 +89,13 @@ class MentoringStepBlock(
scope=Scope.content
)
# User state
student_results = List(
# Store results of student choices.
default=[],
scope=Scope.user_state
)
editable_fields = ('display_name', 'show_title',)
@lazy
......@@ -104,13 +119,41 @@ class MentoringStepBlock(
AnswerRecapBlock, MentoringTableBlock,
]
@property
def steps(self):
""" Get the usage_ids of all of this XBlock's children that are "Questions" """
from mixins import QuestionMixin
return [
_normalize_id(child_id) for child_id in self.children if child_isinstance(self, child_id, QuestionMixin)
]
@XBlock.json_handler
def submit(self, submissions, suffix=''):
log.info(u'Received submissions: {}'.format(submissions))
# Submit child blocks (questions) and gather results
submit_results = []
for child in self.get_steps():
if child.name and child.name in submissions:
submission = submissions[child.name]
child_result = child.submit(submission)
submit_results.append([child.name, child_result])
child.save()
# Update results stored for this step
self.reset()
for result in submit_results:
self.student_results.append(result)
# Compute "answer status" for this step
if all(result[1]['status'] == 'correct' for result in submit_results):
completed = Correctness.CORRECT
elif all(result[1]['status'] == 'incorrect' for result in submit_results):
completed = Correctness.INCORRECT
else:
completed = Correctness.PARTIAL
return {
'message': 'Success!',
'completed': completed,
'results': submit_results,
}
def reset(self):
while self.student_results:
self.student_results.pop()
def author_edit_view(self, context):
"""
......@@ -160,4 +203,7 @@ class MentoringStepBlock(
'child_contents': child_contents,
}))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/step.js'))
fragment.initialize_js('MentoringStepBlock')
return fragment
{% load i18n %}
<div class="mentoring themed-xblock" data-active-step="{{ self.active_step }}">
{% if show_title and title %}
<div class="title">
<h2>{{ title }}</h2>
</div>
{% endif %}
<div class="assessment-question-block">
{% for child_content in children_contents %}
{{ child_content|safe }}
{% endfor %}
<div class="submit">
<span class="assessment-checkmark fa icon-2x"></span>
<input type="button" class="input-main" value="Submit" disabled="disabled" />
<input type="button" class="input-next" value="Next Step" disabled="disabled" />
<input type="button" class="input-try-again" value="Try again" disabled="disabled" />
</div>
</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