Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -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>
43 changes: 38 additions & 5 deletions problem_builder/mentoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -854,27 +862,31 @@ def steps(self):

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
Expand All @@ -897,6 +909,27 @@ def allowed_nested_blocks(self):
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.
Expand Down
150 changes: 150 additions & 0 deletions problem_builder/public/js/mentoring_with_steps.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
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();

}
58 changes: 58 additions & 0 deletions problem_builder/public/js/step.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
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);
});
}

};

}
Loading