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> ...@@ -9,3 +9,4 @@ Alan Boudreault <boudreault.alan@gmail.com>
Eugeny Kolpakov <eugeny@opencraft.com> Eugeny Kolpakov <eugeny@opencraft.com>
Braden MacDonald <braden@opencraft.com> Braden MacDonald <braden@opencraft.com>
Jonathan Piacenti <jonathan@opencraft.com> Jonathan Piacenti <jonathan@opencraft.com>
Tim Krones <tim@opencraft.com>
...@@ -834,6 +834,14 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes ...@@ -834,6 +834,14 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes
scope=Scope.settings 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',) editable_fields = ('display_name',)
@lazy @lazy
...@@ -854,27 +862,31 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes ...@@ -854,27 +862,31 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes
def student_view(self, context): def student_view(self, context):
fragment = Fragment() fragment = Fragment()
child_content = u"" children_contents = []
for child_id in self.children: for child_id in self.children:
child = self.runtime.get_block(child_id) 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 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): elif not isinstance(child, MentoringMessageBlock):
child_fragment = self._render_child_fragment(child, context, view='mentoring_view') child_fragment = self._render_child_fragment(child, context, view='mentoring_view')
fragment.add_frag_resources(child_fragment) 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, 'self': self,
'title': self.display_name, 'title': self.display_name,
'show_title': self.show_title, '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_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) self.include_theme_files(fragment)
fragment.initialize_js('MentoringWithStepsBlock')
return fragment return fragment
@property @property
...@@ -897,6 +909,27 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes ...@@ -897,6 +909,27 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes
NestedXBlockSpec(OnAssessmentReviewMentoringMessageShim, boilerplate='on-assessment-review'), 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): def author_edit_view(self, context):
""" """
Add some HTML to the author view that allows authors to add child blocks. 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 @@ ...@@ -18,12 +18,13 @@
# "AGPLv3". If not, see <http://www.gnu.org/licenses/>. # "AGPLv3". If not, see <http://www.gnu.org/licenses/>.
# #
import logging
from lazy.lazy import lazy from lazy.lazy import lazy
from xblock.core import XBlock 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 xblock.fragment import Fragment
from xblockutils.helpers import child_isinstance
from xblockutils.resources import ResourceLoader from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable import ( from xblockutils.studio_editable import (
NestedXBlockSpec, StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin, XBlockWithPreviewMixin NestedXBlockSpec, StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin, XBlockWithPreviewMixin
...@@ -31,11 +32,12 @@ from xblockutils.studio_editable import ( ...@@ -31,11 +32,12 @@ from xblockutils.studio_editable import (
from problem_builder.answer import AnswerBlock, AnswerRecapBlock from problem_builder.answer import AnswerBlock, AnswerRecapBlock
from problem_builder.mcq import MCQBlock, RatingBlock 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.mrq import MRQBlock
from problem_builder.table import MentoringTableBlock from problem_builder.table import MentoringTableBlock
log = logging.getLogger(__name__)
loader = ResourceLoader(__name__) loader = ResourceLoader(__name__)
...@@ -56,6 +58,12 @@ def _normalize_id(key): ...@@ -56,6 +58,12 @@ def _normalize_id(key):
return key return key
class Correctness(object):
CORRECT = 'correct'
PARTIAL = 'partial'
INCORRECT = 'incorrect'
class HtmlBlockShim(object): class HtmlBlockShim(object):
CATEGORY = 'html' CATEGORY = 'html'
STUDIO_LABEL = _(u"HTML") STUDIO_LABEL = _(u"HTML")
...@@ -64,7 +72,7 @@ class HtmlBlockShim(object): ...@@ -64,7 +72,7 @@ class HtmlBlockShim(object):
@XBlock.needs('i18n') @XBlock.needs('i18n')
class MentoringStepBlock( class MentoringStepBlock(
StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin, XBlockWithPreviewMixin, StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin, XBlockWithPreviewMixin,
EnumerableChildMixin, XBlock EnumerableChildMixin, StepParentMixin, XBlock
): ):
""" """
An XBlock for a step. An XBlock for a step.
...@@ -73,7 +81,7 @@ class MentoringStepBlock( ...@@ -73,7 +81,7 @@ class MentoringStepBlock(
STUDIO_LABEL = _(u"Mentoring Step") STUDIO_LABEL = _(u"Mentoring Step")
CATEGORY = 'pb-mentoring-step' CATEGORY = 'pb-mentoring-step'
# Fields: # Settings
display_name = String( display_name = String(
display_name=_("Step Title"), display_name=_("Step Title"),
help=_('Leave blank to use sequential numbering'), help=_('Leave blank to use sequential numbering'),
...@@ -81,6 +89,13 @@ class MentoringStepBlock( ...@@ -81,6 +89,13 @@ class MentoringStepBlock(
scope=Scope.content scope=Scope.content
) )
# User state
student_results = List(
# Store results of student choices.
default=[],
scope=Scope.user_state
)
editable_fields = ('display_name', 'show_title',) editable_fields = ('display_name', 'show_title',)
@lazy @lazy
...@@ -104,13 +119,41 @@ class MentoringStepBlock( ...@@ -104,13 +119,41 @@ class MentoringStepBlock(
AnswerRecapBlock, MentoringTableBlock, AnswerRecapBlock, MentoringTableBlock,
] ]
@property @XBlock.json_handler
def steps(self): def submit(self, submissions, suffix=''):
""" Get the usage_ids of all of this XBlock's children that are "Questions" """ log.info(u'Received submissions: {}'.format(submissions))
from mixins import QuestionMixin
return [ # Submit child blocks (questions) and gather results
_normalize_id(child_id) for child_id in self.children if child_isinstance(self, child_id, QuestionMixin) 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): def author_edit_view(self, context):
""" """
...@@ -160,4 +203,7 @@ class MentoringStepBlock( ...@@ -160,4 +203,7 @@ class MentoringStepBlock(
'child_contents': child_contents, 'child_contents': child_contents,
})) }))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/step.js'))
fragment.initialize_js('MentoringStepBlock')
return fragment 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