Commit cc9498cd by Braden MacDonald

Refactored Step Builder Messages

parent 5b751841
...@@ -35,15 +35,15 @@ from xblock.fragment import Fragment ...@@ -35,15 +35,15 @@ from xblock.fragment import Fragment
from xblock.validation import ValidationMessage from xblock.validation import ValidationMessage
from .message import MentoringMessageBlock from .message import MentoringMessageBlock
from .mixins import ( from .mixins import (
_normalize_id, QuestionMixin, MessageParentMixin, StepParentMixin, XBlockWithTranslationServiceMixin _normalize_id, QuestionMixin, MessageParentMixin, StepParentMixin, XBlockWithTranslationServiceMixin
) )
from .step_review import ReviewStepBlock
from xblockutils.helpers import child_isinstance 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 (
StudioEditableXBlockMixin, StudioContainerXBlockMixin, StudioContainerWithNestedXBlocksMixin NestedXBlockSpec, StudioEditableXBlockMixin, StudioContainerXBlockMixin, StudioContainerWithNestedXBlocksMixin,
) )
...@@ -925,10 +925,16 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes ...@@ -925,10 +925,16 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes
@property @property
def has_review_step(self): def has_review_step(self):
from .step import ReviewStepBlock
return any(child_isinstance(self, child_id, ReviewStepBlock) for child_id in self.children) return any(child_isinstance(self, child_id, ReviewStepBlock) for child_id in self.children)
@property @property
def review_step(self):
""" Get the Review Step XBlock child, if any. Otherwise returns None """
for step_id in self.children:
if child_isinstance(self, step_id, ReviewStepBlock):
return self.runtime.get_block(step_id)
@property
def score(self): def score(self):
questions = self.questions questions = self.questions
total_child_weight = sum(float(question.weight) for question in questions) total_child_weight = sum(float(question.weight) for question in questions)
...@@ -981,11 +987,12 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes ...@@ -981,11 +987,12 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes
context = context or {} context = context or {}
context['hide_prev_answer'] = True # For Step Builder, we don't show the users' old answers when they try again context['hide_prev_answer'] = True # For Step Builder, we don't show the users' old answers when they try again
context['score_summary'] = self.get_score_summary()
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): else:
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
...@@ -1002,11 +1009,12 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes ...@@ -1002,11 +1009,12 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/mentoring_with_steps.js')) fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/mentoring_with_steps.js'))
fragment.add_resource(loader.load_unicode('templates/html/mentoring_attempts.html'), "text/html") fragment.add_resource(loader.load_unicode('templates/html/mentoring_attempts.html'), "text/html")
fragment.add_resource(loader.load_unicode('templates/html/mentoring_review_templates.html'), "text/html")
self.include_theme_files(fragment) self.include_theme_files(fragment)
fragment.initialize_js('MentoringWithStepsBlock') fragment.initialize_js('MentoringWithStepsBlock', {
'show_extended_feedback': self.show_extended_feedback(),
})
return fragment return fragment
...@@ -1021,10 +1029,11 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes ...@@ -1021,10 +1029,11 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes
NestedXBlockSpec allows explicitly setting disabled/enabled state, disabled reason (if any) and single/multiple NestedXBlockSpec allows explicitly setting disabled/enabled state, disabled reason (if any) and single/multiple
instances instances
""" """
from .step import MentoringStepBlock, ReviewStepBlock # Import here to avoid circular dependency # Import here to avoid circular dependency
from .step import MentoringStepBlock
return [ return [
MentoringStepBlock, MentoringStepBlock,
ReviewStepBlock, NestedXBlockSpec(ReviewStepBlock, single_instance=True),
] ]
@XBlock.json_handler @XBlock.json_handler
...@@ -1048,11 +1057,15 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes ...@@ -1048,11 +1057,15 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes
self.active_step = new_value self.active_step = new_value
elif new_value == len(self.step_ids): elif new_value == len(self.step_ids):
# The user just completed the final step. # The user just completed the final step.
if self.has_review_step:
self.active_step = -1
# Update the number of attempts, if necessary: # Update the number of attempts, if necessary:
if self.num_attempts < self.max_attempts: if self.num_attempts < self.max_attempts:
self.num_attempts += 1 self.num_attempts += 1
# Do we need to render a review (summary of the user's score):
if self.has_review_step:
self.active_step = -1
response_data['review_html'] = self.runtime.render(self.review_step, "mentoring_view", {
'score_summary': self.get_score_summary(),
}).content
response_data['num_attempts'] = self.num_attempts response_data['num_attempts'] = self.num_attempts
# And publish the score: # And publish the score:
score = self.score score = self.score
...@@ -1061,12 +1074,13 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes ...@@ -1061,12 +1074,13 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes
'max_value': self.max_score(), 'max_value': self.max_score(),
} }
self.runtime.publish(self, 'grade', grade_data) self.runtime.publish(self, 'grade', grade_data)
response_data['grade_data'] = self.get_grade()
response_data['active_step'] = self.active_step response_data['active_step'] = self.active_step
return response_data return response_data
def get_grade(self, data=None, suffix=None): def get_score_summary(self):
if self.num_attempts == 0:
return {}
score = self.score score = self.score
return { return {
'score': score.percentage, 'score': score.percentage,
...@@ -1078,7 +1092,7 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes ...@@ -1078,7 +1092,7 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes
'partial': self.partial_json(stringify=False), 'partial': self.partial_json(stringify=False),
'complete': self.complete, 'complete': self.complete,
'max_attempts_reached': self.max_attempts_reached, 'max_attempts_reached': self.max_attempts_reached,
'assessment_review_tips': self.review_tips, 'review_tips': self.review_tips,
} }
@XBlock.json_handler @XBlock.json_handler
...@@ -1101,9 +1115,7 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes ...@@ -1101,9 +1115,7 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes
} }
def author_preview_view(self, context): def author_preview_view(self, context):
context = context.copy() if context else {} return self.student_view(context)
context['author_preview_view'] = True
return super(MentoringWithExplicitStepsBlock, self).author_preview_view(context)
def author_edit_view(self, context): def author_edit_view(self, context):
""" """
...@@ -1121,6 +1133,6 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes ...@@ -1121,6 +1133,6 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder-edit.css')) fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder-edit.css'))
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder-tinymce-content.css')) fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder-tinymce-content.css'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/util.js')) fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/util.js'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/mentoring_with_steps_edit.js')) fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/container_edit.js'))
fragment.initialize_js('MentoringWithStepsEdit') fragment.initialize_js('ProblemBuilderContainerEdit')
return fragment return fragment
...@@ -100,18 +100,6 @@ class MentoringMessageBlock(XBlock, StudioEditableXBlockMixin, XBlockWithTransla ...@@ -100,18 +100,6 @@ class MentoringMessageBlock(XBlock, StudioEditableXBlockMixin, XBlockWithTransla
"used up all of their allowed attempts." "used up all of their allowed attempts."
), ),
}, },
"on-review": {
"display_name": _(u"Message shown when no attempts left"),
"long_display_name": _(u"Message shown during review when no attempts remain"),
"default": _(
u"Note: you have used all attempts. Continue to the next unit."
),
"description": _(
u"This message will be shown when the student is reviewing their answers to the assessment, "
"if the student has used up all of their allowed attempts. "
"It is not shown if the student is allowed to try again."
),
},
} }
content = String( content = String(
...@@ -203,8 +191,3 @@ class CompletedMentoringMessageShim(object): ...@@ -203,8 +191,3 @@ class CompletedMentoringMessageShim(object):
class IncompleteMentoringMessageShim(object): class IncompleteMentoringMessageShim(object):
CATEGORY = 'pb-message' CATEGORY = 'pb-message'
STUDIO_LABEL = _("Message (Incomplete)") STUDIO_LABEL = _("Message (Incomplete)")
class OnReviewMentoringMessageShim(object):
CATEGORY = 'pb-message'
STUDIO_LABEL = _("Message (Review)")
...@@ -357,8 +357,8 @@ class PlotBlock(StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin ...@@ -357,8 +357,8 @@ class PlotBlock(StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin
fragment = super(PlotBlock, self).author_edit_view(context) fragment = super(PlotBlock, self).author_edit_view(context)
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder-edit.css')) fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder-edit.css'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/util.js')) fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/util.js'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/plot_edit.js')) fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/container_edit.js'))
fragment.initialize_js('PlotEdit') fragment.initialize_js('ProblemBuilderContainerEdit')
return fragment return fragment
......
...@@ -6,6 +6,13 @@ ...@@ -6,6 +6,13 @@
font-style: italic; font-style: italic;
} }
.xblock[data-block-type=sb-step] .author-preview-view,
.xblock[data-block-type=step-builder] .author-preview-view,
.xblock[data-block-type=problem-builder] .author-preview-view,
.xblock[data-block-type=mentoring] .author-preview-view {
margin: 10px;
}
.xblock[data-block-type=sb-step] .url-name-footer .url-name, .xblock[data-block-type=sb-step] .url-name-footer .url-name,
.xblock[data-block-type=step-builder] .url-name-footer .url-name, .xblock[data-block-type=step-builder] .url-name-footer .url-name,
.xblock[data-block-type=problem-builder] .url-name-footer .url-name, .xblock[data-block-type=problem-builder] .url-name-footer .url-name,
......
...@@ -234,6 +234,11 @@ ...@@ -234,6 +234,11 @@
position: relative; position: relative;
} }
.assessment-question-block div[data-block-type=sb-step],
.assessment-question-block div[data-block-type=sb-review-step] {
display: none; /* Hidden until revealed by JS */
}
.mentoring .sb-step .sb-step-message { .mentoring .sb-step .sb-step-message {
position: absolute; position: absolute;
top: 50%; top: 50%;
......
function ProblemBuilderContainerEdit(runtime, element) {
"use strict";
// Standard initialization for any Problem Builder / Step Builder container XBlocks
// that are instances of StudioContainerXBlockWithNestedXBlocksMixin
StudioContainerXBlockWithNestedXBlocksMixin(runtime, element);
if (window.ProblemBuilderUtil) {
ProblemBuilderUtil.transformClarifications(element);
}
}
function MentoringWithStepsBlock(runtime, element) { function MentoringWithStepsBlock(runtime, element, params) {
// Set up gettext in case it isn't available in the client runtime: // Set up gettext in case it isn't available in the client runtime:
if (typeof gettext == "undefined") { if (typeof gettext == "undefined") {
...@@ -8,23 +8,22 @@ function MentoringWithStepsBlock(runtime, element) { ...@@ -8,23 +8,22 @@ function MentoringWithStepsBlock(runtime, element) {
var children = runtime.children(element); var children = runtime.children(element);
var steps = []; var steps = [];
var reviewStep;
for (var i = 0; i < children.length; i++) { for (var i = 0; i < children.length; i++) {
var child = children[i]; var child = children[i];
var blockType = $(child.element).data('block-type'); var blockType = $(child.element).data('block-type');
if (blockType === 'sb-step') { if (blockType === 'sb-step') {
steps.push(child); steps.push(child);
} else if (blockType === 'sb-review-step') {
reviewStep = child;
} }
} }
var activeStep = $('.mentoring', element).data('active-step'); var activeStep = $('.mentoring', element).data('active-step');
var reviewTipsTemplate = _.template($('#xblock-review-tips-template').html()); // Tips about specific questions the user got wrong
var attemptsTemplate = _.template($('#xblock-attempts-template').html()); var attemptsTemplate = _.template($('#xblock-attempts-template').html());
var message = $('.sb-step-message', element); var message = $('.sb-step-message', element);
var checkmark, submitDOM, nextDOM, reviewDOM, tryAgainDOM, var checkmark, submitDOM, nextDOM, reviewButtonDOM, tryAgainDOM,
gradeDOM, attemptsDOM, reviewTipsDOM, reviewLinkDOM, submitXHR; gradeDOM, attemptsDOM, reviewLinkDOM, submitXHR;
var reviewStepDOM = $("[data-block-type=sb-review-step]", element);
var hasAReviewStep = reviewStepDOM.length == 1;
function isLastStep() { function isLastStep() {
return (activeStep === steps.length-1); return (activeStep === steps.length-1);
...@@ -43,8 +42,7 @@ function MentoringWithStepsBlock(runtime, element) { ...@@ -43,8 +42,7 @@ function MentoringWithStepsBlock(runtime, element) {
} }
function extendedFeedbackEnabled() { function extendedFeedbackEnabled() {
var data = gradeDOM.data(); return !!(params.extended_feedback); // Show extended feedback when all attempts are used up?
return data.extended_feedback === "True";
} }
function showFeedback(response) { function showFeedback(response) {
...@@ -61,22 +59,6 @@ function MentoringWithStepsBlock(runtime, element) { ...@@ -61,22 +59,6 @@ function MentoringWithStepsBlock(runtime, element) {
} }
} }
function updateGrade(grade_data) {
gradeDOM.data('score', grade_data.score);
gradeDOM.data('correct_answer', grade_data.correct_answers);
gradeDOM.data('incorrect_answer', grade_data.incorrect_answers);
gradeDOM.data('partially_correct_answer', grade_data.partially_correct_answers);
gradeDOM.data('correct', grade_data.correct);
gradeDOM.data('incorrect', grade_data.incorrect);
gradeDOM.data('partial', grade_data.partial);
gradeDOM.data('assessment_review_tips', grade_data.assessment_review_tips);
updateReviewStep(grade_data);
}
function updateReviewStep(response) {
reviewStep.updateAssessmentMessage(response, updateControls);
}
function updateControls() { function updateControls() {
submitDOM.attr('disabled', 'disabled'); submitDOM.attr('disabled', 'disabled');
...@@ -84,8 +66,8 @@ function MentoringWithStepsBlock(runtime, element) { ...@@ -84,8 +66,8 @@ function MentoringWithStepsBlock(runtime, element) {
if (nextDOM.is(':visible')) { nextDOM.focus(); } if (nextDOM.is(':visible')) { nextDOM.focus(); }
if (atReviewStep()) { if (atReviewStep()) {
if (reviewStep) { if (hasAReviewStep) {
reviewDOM.removeAttr('disabled'); reviewButtonDOM.removeAttr('disabled');
} else { } else {
if (someAttemptsLeft()) { if (someAttemptsLeft()) {
tryAgainDOM.removeAttr('disabled'); tryAgainDOM.removeAttr('disabled');
...@@ -111,7 +93,8 @@ function MentoringWithStepsBlock(runtime, element) { ...@@ -111,7 +93,8 @@ function MentoringWithStepsBlock(runtime, element) {
// We are now showing the review step / end // We are now showing the review step / end
// Update the number of attempts. // Update the number of attempts.
attemptsDOM.data('num_attempts', response.num_attempts); attemptsDOM.data('num_attempts', response.num_attempts);
updateGrade(response.grade_data); reviewStepDOM.html($(response.review_html).html());
updateControls();
} else if (!hasQuestion) { } else if (!hasQuestion) {
// This was a step with no questions, so proceed to the next step / review: // This was a step with no questions, so proceed to the next step / review:
updateDisplay(); updateDisplay();
...@@ -156,7 +139,6 @@ function MentoringWithStepsBlock(runtime, element) { ...@@ -156,7 +139,6 @@ function MentoringWithStepsBlock(runtime, element) {
hideAllSteps(); hideAllSteps();
hideReviewStep(); hideReviewStep();
attemptsDOM.html(''); attemptsDOM.html('');
reviewTipsDOM.empty().hide();
message.hide(); message.hide();
} }
...@@ -186,54 +168,32 @@ function MentoringWithStepsBlock(runtime, element) { ...@@ -186,54 +168,32 @@ function MentoringWithStepsBlock(runtime, element) {
} else { } else {
nextDOM.removeAttr('disabled'); nextDOM.removeAttr('disabled');
} }
if (isLastStep() && reviewStep) { if (isLastStep() && hasAReviewStep) {
if (step.hasQuestion()) { if (step.hasQuestion()) {
reviewDOM.attr('disabled', 'disabled'); reviewButtonDOM.attr('disabled', 'disabled');
} else { } else {
reviewDOM.removeAttr('disabled') reviewButtonDOM.removeAttr('disabled')
} }
reviewDOM.show(); reviewButtonDOM.show();
} }
} }
} }
function showReviewStep() { function showReviewStep() {
// Forward to review step to show assessment message
reviewStep.showAssessmentMessage();
// Forward to review step to render grade data
var showExtendedFeedback = (!someAttemptsLeft() && extendedFeedbackEnabled());
reviewStep.renderGrade(gradeDOM, showExtendedFeedback);
// Add click handler that takes care of showing associated step to step links
$('a.step-link', element).on('click', getStepToReview);
if (someAttemptsLeft()) { if (someAttemptsLeft()) {
tryAgainDOM.removeAttr('disabled'); tryAgainDOM.removeAttr('disabled');
// Review tips
var data = gradeDOM.data();
if (data.assessment_review_tips.length > 0) {
// on-assessment-review-question messages specific to questions the student got wrong:
reviewTipsDOM.html(reviewTipsTemplate({
tips: data.assessment_review_tips
}));
reviewTipsDOM.show();
}
} }
submitDOM.hide(); submitDOM.hide();
nextDOM.hide(); nextDOM.hide();
reviewDOM.hide(); reviewButtonDOM.hide();
tryAgainDOM.show(); tryAgainDOM.show();
reviewStepDOM.show();
} }
function hideReviewStep() { function hideReviewStep() {
if (reviewStep) { reviewStepDOM.hide()
reviewStep.hideAssessmentMessage();
reviewStep.clearGrade(gradeDOM);
}
} }
function getStepToReview(event) { function getStepToReview(event) {
...@@ -249,8 +209,8 @@ function MentoringWithStepsBlock(runtime, element) { ...@@ -249,8 +209,8 @@ function MentoringWithStepsBlock(runtime, element) {
updateNextLabel(); updateNextLabel();
if (isLastStep()) { if (isLastStep()) {
reviewDOM.show(); reviewButtonDOM.show();
reviewDOM.removeAttr('disabled'); reviewButtonDOM.removeAttr('disabled');
nextDOM.hide(); nextDOM.hide();
nextDOM.attr('disabled', 'disabled'); nextDOM.attr('disabled', 'disabled');
} else { } else {
...@@ -307,8 +267,8 @@ function MentoringWithStepsBlock(runtime, element) { ...@@ -307,8 +267,8 @@ function MentoringWithStepsBlock(runtime, element) {
if (isLastStep() && step.hasQuestion()) { if (isLastStep() && step.hasQuestion()) {
nextDOM.hide(); nextDOM.hide();
} else if (isLastStep()) { } else if (isLastStep()) {
reviewDOM.one('click', submit); reviewButtonDOM.one('click', submit);
reviewDOM.removeAttr('disabled'); reviewButtonDOM.removeAttr('disabled');
nextDOM.hide() nextDOM.hide()
} else if (!step.hasQuestion()) { } else if (!step.hasQuestion()) {
nextDOM.one('click', submit); nextDOM.one('click', submit);
...@@ -388,7 +348,7 @@ function MentoringWithStepsBlock(runtime, element) { ...@@ -388,7 +348,7 @@ function MentoringWithStepsBlock(runtime, element) {
nextDOM.off(); nextDOM.off();
nextDOM.on('click', updateDisplay); nextDOM.on('click', updateDisplay);
nextDOM.show(); nextDOM.show();
reviewDOM.hide(); reviewButtonDOM.hide();
} }
} }
...@@ -434,7 +394,7 @@ function MentoringWithStepsBlock(runtime, element) { ...@@ -434,7 +394,7 @@ function MentoringWithStepsBlock(runtime, element) {
hideAllSteps(); hideAllSteps();
// Initialize references to relevant DOM elements and set up event handlers // Initialize references to relevant DOM elements and set up event handlers
checkmark = $('.assessment-checkmark', element); checkmark = $('.step-overall-checkmark', element);
submitDOM = $(element).find('.submit .input-main'); submitDOM = $(element).find('.submit .input-main');
submitDOM.on('click', submit); submitDOM.on('click', submit);
...@@ -446,19 +406,21 @@ function MentoringWithStepsBlock(runtime, element) { ...@@ -446,19 +406,21 @@ function MentoringWithStepsBlock(runtime, element) {
nextDOM.on('click', updateDisplay); nextDOM.on('click', updateDisplay);
} }
reviewDOM = $(element).find('.submit .input-review'); reviewButtonDOM = $(element).find('.submit .input-review');
reviewDOM.on('click', showGrade); reviewButtonDOM.on('click', showGrade);
tryAgainDOM = $(element).find('.submit .input-try-again'); tryAgainDOM = $(element).find('.submit .input-try-again');
tryAgainDOM.on('click', tryAgain); tryAgainDOM.on('click', tryAgain);
gradeDOM = $('.grade', element); gradeDOM = $('.grade', element);
attemptsDOM = $('.attempts', element); attemptsDOM = $('.attempts', element);
reviewTipsDOM = $('.assessment-review-tips', element);
reviewLinkDOM = $(element).find('.review-link'); reviewLinkDOM = $(element).find('.review-link');
reviewLinkDOM.on('click', showGrade); reviewLinkDOM.on('click', showGrade);
// Add click handler that takes care of links to steps on the extended review:
$('a.step-link', element).on('click', getStepToReview);
// Initialize individual steps // Initialize individual steps
// (sets up click handlers for questions and makes sure answer data is up-to-date) // (sets up click handlers for questions and makes sure answer data is up-to-date)
var options = { var options = {
......
function MentoringWithStepsEdit(runtime, element) {
"use strict";
var $buttons = $('.add-xblock-component-button[data-category=sb-review-step]', element);
var blockIsPresent = function(klass) {
return $('.xblock ' + klass).length > 0;
};
var updateButton = function(button, condition) {
button.toggleClass('disabled', condition);
};
var disableButton = function(ev) {
if ($(this).is('.disabled')) {
ev.preventDefault();
ev.stopPropagation();
} else {
$(this).addClass('disabled');
}
};
var updateButtons = function(buttons) {
buttons.each(function() {
var button = $(this);
updateButton(button, blockIsPresent('.xblock-header-sb-review-step'));
});
};
var initButtons = function() {
updateButtons($buttons);
$buttons.on('click', disableButton);
};
var resetButtons = function() {
var $disabledButtons = $buttons.filter('.disabled');
updateButtons($disabledButtons);
};
ProblemBuilderUtil.transformClarifications(element);
initButtons();
runtime.listenTo('deleted-child', resetButtons);
}
function PlotEdit(runtime, element) {
'use strict';
StudioContainerXBlockWithNestedXBlocksMixin(runtime, element);
ProblemBuilderUtil.transformClarifications(element);
}
function ReviewStepBlock(runtime, element) {
var gradeTemplate = _.template($('#xblock-feedback-template').html());
var reviewStepsTemplate = _.template($('#xblock-step-links-template').html());
var assessmentMessageDOM = $('.assessment-message', element);
return {
'showAssessmentMessage': function() {
var assessmentMessage = assessmentMessageDOM.data('assessment_message');
assessmentMessageDOM.html(assessmentMessage);
assessmentMessageDOM.show();
},
'hideAssessmentMessage': function() {
assessmentMessageDOM.html('');
assessmentMessageDOM.hide();
},
'updateAssessmentMessage': function(grade, callback) {
var handlerUrl = runtime.handlerUrl(element, 'get_assessment_message');
$.post(handlerUrl, JSON.stringify(grade)).success(function(response) {
assessmentMessageDOM.data('assessment_message', response.assessment_message);
callback();
});
},
'renderGrade': function(gradeDOM, showExtendedFeedback) {
var data = gradeDOM.data();
_.extend(data, {
'runDetails': function(correctness) {
if (!showExtendedFeedback) {
return '';
}
var self = this;
return reviewStepsTemplate({'questions': self[correctness], 'correctness': correctness});
}
});
gradeDOM.html(gradeTemplate(data));
},
'clearGrade': function(gradeDOM) {
gradeDOM.html('');
}
};
}
function ReviewStepEdit(runtime, element) {
"use strict";
var $buttons = $('.add-xblock-component-button[data-category=pb-message]', element);
var blockIsPresent = function(klass) {
return $('.xblock ' + klass).length > 0;
};
var updateButton = function(button, condition) {
button.toggleClass('disabled', condition);
};
var disableButton = function(ev) {
if ($(this).is('.disabled')) {
ev.preventDefault();
ev.stopPropagation();
} else {
$(this).addClass('disabled');
}
};
var updateButtons = function(buttons) {
buttons.each(function() {
var button = $(this);
var msgType = button.data('boilerplate');
updateButton(button, blockIsPresent('.submission-message.'+msgType));
});
};
var initButtons = function() {
updateButtons($buttons);
$buttons.on('click', disableButton);
};
var resetButtons = function() {
var $disabledButtons = $buttons.filter('.disabled');
updateButtons($disabledButtons);
};
ProblemBuilderUtil.transformClarifications(element);
initButtons();
runtime.listenTo('deleted-child', resetButtons);
}
function StepEdit(runtime, element) {
'use strict';
StudioContainerXBlockWithNestedXBlocksMixin(runtime, element);
ProblemBuilderUtil.transformClarifications(element);
}
...@@ -32,10 +32,7 @@ from xblockutils.studio_editable import ( ...@@ -32,10 +32,7 @@ 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 .message import ( from problem_builder.mixins import EnumerableChildMixin, StepParentMixin
CompletedMentoringMessageShim, IncompleteMentoringMessageShim, OnReviewMentoringMessageShim
)
from problem_builder.mixins import EnumerableChildMixin, MessageParentMixin, StepParentMixin
from problem_builder.mrq import MRQBlock from problem_builder.mrq import MRQBlock
from problem_builder.plot import PlotBlock from problem_builder.plot import PlotBlock
from problem_builder.slider import SliderBlock from problem_builder.slider import SliderBlock
...@@ -69,11 +66,6 @@ class Correctness(object): ...@@ -69,11 +66,6 @@ class Correctness(object):
INCORRECT = 'incorrect' INCORRECT = 'incorrect'
class HtmlBlockShim(object):
CATEGORY = 'html'
STUDIO_LABEL = _(u"HTML")
@XBlock.needs('i18n') @XBlock.needs('i18n')
class MentoringStepBlock( class MentoringStepBlock(
StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin, XBlockWithPreviewMixin, StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin, XBlockWithPreviewMixin,
...@@ -152,7 +144,8 @@ class MentoringStepBlock( ...@@ -152,7 +144,8 @@ class MentoringStepBlock(
return [ return [
NestedXBlockSpec(AnswerBlock, boilerplate='studio_default'), NestedXBlockSpec(AnswerBlock, boilerplate='studio_default'),
MCQBlock, RatingBlock, MRQBlock, HtmlBlockShim, MCQBlock, RatingBlock, MRQBlock,
NestedXBlockSpec(None, category="html", label=self._("HTML")),
AnswerRecapBlock, MentoringTableBlock, PlotBlock, SliderBlock AnswerRecapBlock, MentoringTableBlock, PlotBlock, SliderBlock
] + additional_blocks ] + additional_blocks
...@@ -229,8 +222,8 @@ class MentoringStepBlock( ...@@ -229,8 +222,8 @@ class MentoringStepBlock(
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder-edit.css')) fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder-edit.css'))
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder-tinymce-content.css')) fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder-tinymce-content.css'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/util.js')) fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/util.js'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/step_edit.js')) fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/container_edit.js'))
fragment.initialize_js('StepEdit') fragment.initialize_js('ProblemBuilderContainerEdit')
return fragment return fragment
def mentoring_view(self, context=None): def mentoring_view(self, context=None):
...@@ -275,96 +268,3 @@ class MentoringStepBlock( ...@@ -275,96 +268,3 @@ class MentoringStepBlock(
fragment.initialize_js('MentoringStepBlock') fragment.initialize_js('MentoringStepBlock')
return fragment return fragment
class ReviewStepBlock(MessageParentMixin, StudioContainerWithNestedXBlocksMixin, XBlockWithPreviewMixin, XBlock):
""" A dedicated step for reviewing results for a mentoring block """
CATEGORY = 'sb-review-step'
STUDIO_LABEL = _("Review Step")
display_name = String(
default="Review Step"
)
@property
def allowed_nested_blocks(self):
"""
Returns a list of allowed nested XBlocks. Each item can be either
* An XBlock class
* A NestedXBlockSpec
If XBlock class is used it is assumed that this XBlock is enabled and allows multiple instances.
NestedXBlockSpec allows explicitly setting disabled/enabled state,
disabled reason (if any) and single/multiple instances.
"""
return [
NestedXBlockSpec(CompletedMentoringMessageShim, boilerplate='completed'),
NestedXBlockSpec(IncompleteMentoringMessageShim, boilerplate='incomplete'),
NestedXBlockSpec(OnReviewMentoringMessageShim, boilerplate='on-review'),
]
@XBlock.json_handler
def get_assessment_message(self, grade, suffix):
# Data passed as "grade" comes from "get_grade" handler of Step Builder (MentoringWithExplicitStepsBlock)
complete = grade.get('complete')
max_attempts_reached = grade.get('max_attempts_reached')
return {
'assessment_message': self.assessment_message(complete, max_attempts_reached)
}
def assessment_message(self, complete=None, max_attempts_reached=None):
if complete is None and max_attempts_reached is None:
parent = self.get_parent()
complete = parent.complete
max_attempts_reached = parent.max_attempts_reached
if max_attempts_reached:
assessment_message = self.get_message_content('on-review', or_default=True)
else:
if complete: # All answers correct
assessment_message = self.get_message_content('completed', or_default=True)
else:
assessment_message = self.get_message_content('incomplete', or_default=True)
return assessment_message
def mentoring_view(self, context=None):
""" Mentoring View """
return self._render_view(context)
def student_view(self, context=None):
""" Student View """
return self._render_view(context)
def studio_view(self, context=None):
""" Studio View """
return Fragment(u'<p>This is a preconfigured block. It is not editable.</p>')
def _render_view(self, context):
fragment = Fragment()
fragment.add_content(loader.render_template('templates/html/review_step.html', {
'self': self,
}))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/review_step.js'))
fragment.initialize_js('ReviewStepBlock')
return fragment
def author_preview_view(self, context):
return Fragment(
u"<p>{}</p>".format(
_(u"This block summarizes a student's performance on the parent Step Builder block.")
)
)
def author_edit_view(self, context):
"""
Add some HTML to the author view that allows authors to add child blocks.
"""
context['wrap_children'] = {
'head': u'<div class="mentoring">',
'tail': u'</div>'
}
fragment = super(ReviewStepBlock, self).author_edit_view(context)
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder-edit.css'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/util.js'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/review_step_edit.js'))
fragment.initialize_js('ReviewStepEdit')
return fragment
<!-- Tips about specific questions the student got wrong. From pb-message[type=on-assessment-review-question] blocks -->
<script type="text/template" id="xblock-review-tips-template">
<p class="review-tips-intro"><%= gettext("You might consider reviewing the following items before your next assessment attempt:") %></p>
<ul class="review-tips-list">
<% for (var tip_idx in tips) {{ %>
<li><%= tips[tip_idx] %></li>
<% }} %>
</ul>
</script>
...@@ -13,20 +13,8 @@ ...@@ -13,20 +13,8 @@
{{ child_content|safe }} {{ child_content|safe }}
{% endfor %} {% endfor %}
<div class="grade"
data-score="{{ self.score.percentage }}"
data-correct_answer="{{ self.score.correct|length }}"
data-incorrect_answer="{{ self.score.incorrect|length }}"
data-partially_correct_answer="{{ self.score.partially_correct|length }}"
data-assessment_review_tips="{{ self.review_tips_json }}"
data-extended_feedback="{{ self.extended_feedback }}"
data-correct="{{ self.correct_json }}"
data-incorrect="{{ self.incorrect_json }}"
data-partial="{{ self.partial_json }}">
</div>
<div class="submit"> <div class="submit">
<span class="assessment-checkmark fa icon-2x"></span> <span class="step-overall-checkmark fa icon-2x"></span>
<input type="button" class="input-main" value="Submit" disabled="disabled" /> <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-next" value="Next Step" disabled="disabled" />
<input type="button" class="input-review" value="Review grade" disabled="disabled" /> <input type="button" class="input-review" value="Review grade" disabled="disabled" />
...@@ -35,11 +23,7 @@ ...@@ -35,11 +23,7 @@
<div class="attempts" <div class="attempts"
data-max_attempts="{{ self.max_attempts }}" data-num_attempts="{{ self.num_attempts }}"> data-max_attempts="{{ self.max_attempts }}" data-num_attempts="{{ self.num_attempts }}">
</div> </div>
</div> </div>
<div class="assessment-review-tips"></div>
</div> </div>
<div class="review-link"><a href="#">Review final grade</a></div> <div class="review-link"><a href="#">Review final grade</a></div>
......
<div class="sb-review-step">
<div class="assessment-message" data-assessment_message="{{ self.assessment_message }}"></div>
<script type="text/template" id="xblock-feedback-template">
<div class="grade-result">
<h2>
<%= _.template(gettext("You scored {percent}% on this assessment."), {percent: score}, {interpolate: /\{(.+?)\}/g}) %>
</h2>
<hr/>
<span class="assessment-checkmark icon-2x checkmark-correct icon-ok fa fa-check"></span>
<div class="results-section">
<p>
<%= _.template(
ngettext(
"You answered 1 question correctly.",
"You answered {number_correct} questions correctly.",
correct_answer
), {number_correct: correct_answer}, {interpolate: /\{(.+?)\}/g})
%>
</p>
<%= runDetails('correct') %>
</div>
<div class="clear"></div>
<span class="assessment-checkmark icon-2x checkmark-partially-correct icon-ok fa fa-check"></span>
<div class="results-section">
<p>
<%= _.template(
ngettext(
"You answered 1 question partially correctly.",
"You answered {number_partially_correct} questions partially correctly.",
partially_correct_answer
), {number_partially_correct: partially_correct_answer}, {interpolate: /\{(.+?)\}/g})
%>
</p>
<%= runDetails('partial') %>
</div>
<div class="clear"></div>
<span class="assessment-checkmark icon-2x checkmark-incorrect icon-exclamation fa fa-exclamation"></span>
<div class="results-section">
<p>
<%= _.template(
ngettext(
"You answered 1 question incorrectly.",
"You answered {number_incorrect} questions incorrectly.",
incorrect_answer
), {number_incorrect: incorrect_answer}, {interpolate: /\{(.+?)\}/g})
%>
</p>
<%= runDetails('incorrect') %>
</div>
<div class="clear"></div>
<hr/>
</div>
</script>
<!-- Template for extended feedback: Show extended feedback details when all attempts are used up. -->
<script type="text/template" id="xblock-step-links-template">
<ul class="review-list <%= correctness %>-list">
<% for (var question in questions) { %>
<%
var q = questions[question];
var last_question = question == questions.length - 1;
var second_last_question = question == questions.length - 2;
%>
<li>
<a href="#" class="step-link" data-step="<%= q.step %>"><%=
_.template(gettext("Question {number}"), {number: q.number}, {interpolate: /\{(.+?)\}/g})
%></a><% if (!last_question) { %><%= (questions.length > 2 ? ", " : "") %><%= (second_last_question ? " " + gettext("and"): "") %><% } %>
</li>
<% } %>
</ul>
</script>
</div>
{% load i18n %}
<!-- Tips about specific questions the student got wrong. From pb-message[type=on-assessment-review-question] blocks -->
<p class="review-tips-intro">{% trans "You might consider reviewing the following items before your next assessment attempt:" %}</p>
<ul class="review-tips-list">
{% for tip in tips %}
<li>{{tip}}</li>
{% endfor %}
</ul>
{% load i18n %}
<div class="sb-review-score">
<div class="grade-result">
<h2>{% blocktrans %}You scored {{score}}% on this assessment. {% endblocktrans %}</h2>
{% if is_example %}
<p><em>{% trans "Note: This is an example score, to show how the review step will look." %}</em></p>
{% endif %}
<hr/>
<span class="assessment-checkmark icon-2x checkmark-correct icon-ok fa fa-check"></span>
<div class="results-section">
<p>
{% blocktrans count correct_answers=correct_answers %}
You answered 1 question correctly.
{% plural %}
You answered {{correct_answers}} questions correctly.
{% endblocktrans %}
</p>
{% if show_extended_review %}
<ul class="review-list correct-list">
{% for question in correct %}
<li>
{% if forloop.last and not forloop.first %} {% trans "and" %} {% endif %}
<a href="#" class="step-link" data-step="{{ question.step }}">{% blocktrans %}Question {{question.number}}{% endblocktrans %}</a>{% if forloop.revcounter > 1 and correct|length > 2 %},{%endif%}
</li>
{% endfor %}
</ul>
{% endif %}
</div>
<div class="clear"></div>
<span class="assessment-checkmark icon-2x checkmark-partially-correct icon-ok fa fa-check"></span>
<div class="results-section">
<p>
{% blocktrans count partially_correct_answers=partially_correct_answers %}
You answered 1 question partially correctly.
{% plural %}
You answered {{partially_correct_answers}} questions partially correctly.
{% endblocktrans %}
</p>
{% if show_extended_review %}
<ul class="review-list partial-list">
{% for question in partial %}
<li>
{% if forloop.last and not forloop.first %} {% trans "and" %} {% endif %}
<a href="#" class="step-link" data-step="{{ question.step }}">{% blocktrans %}Question {{question.number}}{% endblocktrans %}</a>{% if forloop.revcounter > 1 and partial|length > 2 %},{%endif%}
</li>
{% endfor %}
</ul>
{% endif %}
</div>
<div class="clear"></div>
<span class="assessment-checkmark icon-2x checkmark-incorrect icon-exclamation fa fa-exclamation"></span>
<div class="results-section">
<p>
{% blocktrans count incorrect_answers=incorrect_answers %}
You answered 1 question incorrectly.
{% plural %}
You answered {{incorrect_answers}} questions incorrectly.
{% endblocktrans %}
</p>
{% if show_extended_review %}
<ul class="review-list incorrect-list">
{% for question in incorrect %}
<li>
{% if forloop.last and not forloop.first %} {% trans "and" %} {% endif %}
<a href="#" class="step-link" data-step="{{ question.step }}">{% blocktrans %}Question {{question.number}}{% endblocktrans %}</a>{% if forloop.revcounter > 1 and incorrect|length > 2 %},{%endif%}
</li>
{% endfor %}
</ul>
{% endif %}
</div>
<div class="clear"></div>
<hr/>
</div>
</div>
...@@ -43,7 +43,10 @@ BLOCKS = [ ...@@ -43,7 +43,10 @@ BLOCKS = [
'problem-builder = problem_builder.mentoring:MentoringBlock', 'problem-builder = problem_builder.mentoring:MentoringBlock',
'step-builder = problem_builder.mentoring:MentoringWithExplicitStepsBlock', 'step-builder = problem_builder.mentoring:MentoringWithExplicitStepsBlock',
'sb-step = problem_builder.step:MentoringStepBlock', 'sb-step = problem_builder.step:MentoringStepBlock',
'sb-review-step = problem_builder.step:ReviewStepBlock', 'sb-review-step = problem_builder.step_review:ReviewStepBlock',
'sb-conditional-message = problem_builder.step_review:ConditionalMessageBlock',
'sb-review-score = problem_builder.step_review:ScoreSummaryBlock',
'sb-review-per-question-feedback = problem_builder.step_review:PerQuestionFeedbackBlock',
'sb-plot = problem_builder.plot:PlotBlock', 'sb-plot = problem_builder.plot:PlotBlock',
'sb-plot-overlay = problem_builder.plot:PlotOverlayBlock', 'sb-plot-overlay = problem_builder.plot:PlotOverlayBlock',
......
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