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
# -*- coding: utf-8 -*-
#
# Copyright (c) 2014-2015 Harvard, edX & OpenCraft
#
# This software's license gives you freedom; you can copy, convey,
# propagate, redistribute and/or modify this program under the terms of
# the GNU Affero General Public License (AGPL) as published by the Free
# Software Foundation (FSF), either version 3 of the License, or (at your
# option) any later version of the AGPL published by the FSF.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
# General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program in a file in the toplevel directory called
# "AGPLv3". If not, see <http://www.gnu.org/licenses/>.
#
import logging
from xblock.core import XBlock
from xblock.fields import String, Scope, Integer
from xblock.fragment import Fragment
from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable import (
NestedXBlockSpec, StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin, XBlockWithPreviewMixin
)
from .mixins import XBlockWithTranslationServiceMixin
log = logging.getLogger(__name__)
loader = ResourceLoader(__name__)
# Make '_' a no-op so we can scrape strings
def _(text):
return text
@XBlock.needs("i18n")
class ConditionalMessageBlock(
StudioEditableXBlockMixin, XBlockWithTranslationServiceMixin, XBlockWithPreviewMixin, XBlock
):
"""
A message shown as part of a Step Builder review step, but only under certain conditions.
"""
CATEGORY = 'sb-conditional-message'
STUDIO_LABEL = _("Conditional Message")
content = String(
display_name=_("Message"),
help=_("Message to display upon completion"),
scope=Scope.content,
default="",
multiline_editor="html",
resettable_editor=False,
)
SCORE_PERFECT, SCORE_IMPERFECT, SCORE_ANY = 1, 2, 0
SCORE_CONDITIONS_DESCRIPTIONS = {
SCORE_PERFECT: _("Show only if student got a perfect score"),
SCORE_IMPERFECT: _("Show only if student got at least one question wrong"),
SCORE_ANY: _("Show for any score"),
}
score_condition = Integer(
display_name=_("Score condition"),
default=SCORE_ANY,
values=[{"display_name": val, "value": key} for key, val in SCORE_CONDITIONS_DESCRIPTIONS.items()],
)
IF_ATTEMPTS_REMAIN, IF_NO_ATTEMPTS_REMAIN, ATTEMPTS_ANY = 1, 2, 0
NUM_ATTEMPTS_COND_DESCRIPTIONS = {
IF_ATTEMPTS_REMAIN: _("Show only if student can try again"),
IF_NO_ATTEMPTS_REMAIN: _("Show only if student has used up all attempts"),
ATTEMPTS_ANY: _("Show whether student can try again or not"),
}
num_attempts_condition = Integer(
display_name=_("Try again condition"),
default=ATTEMPTS_ANY,
values=[{"display_name": val, "value": key} for key, val in NUM_ATTEMPTS_COND_DESCRIPTIONS.items()],
)
editable_fields = ('content', 'score_condition', 'num_attempts_condition')
has_author_view = True # Without this flag, studio will use student_view on newly-added blocks :/
@property
def display_name_with_default(self):
return self._(self.STUDIO_LABEL)
def is_applicable(self, context):
""" Return true if this block should appear in the review step, false otherwise """
score_summary = context['score_summary']
attempts_remain = not score_summary['max_attempts_reached']
if (
(self.num_attempts_condition == self.IF_ATTEMPTS_REMAIN and not attempts_remain) or
(self.num_attempts_condition == self.IF_NO_ATTEMPTS_REMAIN and attempts_remain)
):
return False
perfect_score = (score_summary['incorrect'] == 0 and score_summary['partial'] == 0)
if (
(self.score_condition == self.SCORE_PERFECT and not perfect_score) or
(self.score_condition == self.SCORE_IMPERFECT and perfect_score)
):
return False
return True
def student_view(self, context=None):
""" Render this message. """
html = u'<div class="review-conditional-message">{content}</div>'.format(
content=self.content
)
return Fragment(html)
preview_view = student_view
mentoring_view = student_view # Same as student_view but Studio won't wrap it with the editing header/buttons
def author_view(self, context=None):
fragment = self.student_view(context)
desc = ""
if self.num_attempts_condition == self.ATTEMPTS_ANY and self.score_condition == self.SCORE_ANY:
desc = self._("Always shown")
else:
if self.score_condition != self.SCORE_ANY:
desc += self.SCORE_CONDITIONS_DESCRIPTIONS[self.score_condition] + "<br>"
if self.num_attempts_condition != self.ATTEMPTS_ANY:
desc += self.NUM_ATTEMPTS_COND_DESCRIPTIONS[self.num_attempts_condition]
fragment.content += u'<div class="submission-message-help"><p>{}</p></div>'.format(desc)
return fragment
@XBlock.needs("i18n")
class ScoreSummaryBlock(XBlockWithTranslationServiceMixin, XBlockWithPreviewMixin, XBlock):
"""
Summaryize the score that the student earned.
"""
CATEGORY = 'sb-review-score'
STUDIO_LABEL = _("Score Summary")
has_author_view = True # Without this flag, studio will use student_view on newly-added blocks :/
@property
def display_name_with_default(self):
return self._(self.STUDIO_LABEL)
def student_view(self, context=None):
""" Render the score summary message. """
html = loader.render_template("templates/html/sb-review-score.html", context.get("score_summary", {}))
return Fragment(html)
mentoring_view = student_view # Same as student_view but Studio won't wrap it with the editing header/buttons
def author_view(self, context=None):
if not context.get("score_summary"):
context["score_summary"] = {
'score': 75,
'correct_answers': 3,
'incorrect_answers': 1,
'partially_correct_answers': 0,
'correct': [],
'incorrect': [],
'partial': [],
'complete': True,
'max_attempts_reached': False,
'is_example': True,
}
return self.student_view(context)
@XBlock.needs("i18n")
class PerQuestionFeedbackBlock(XBlockWithTranslationServiceMixin, XBlockWithPreviewMixin, XBlock):
"""
Summaryize the score that the student earned.
"""
CATEGORY = 'sb-review-per-question-feedback'
STUDIO_LABEL = _("Per-Question Feedback")
has_author_view = True # Without this flag, studio will use student_view on newly-added blocks :/
@property
def display_name_with_default(self):
return self._(self.STUDIO_LABEL)
def student_view(self, context=None):
""" Render the per-question feedback, if any. """
review_tips = (context or {}).get("score_summary", {}).get("review_tips")
if review_tips:
html = loader.render_template("templates/html/sb-review-per-question-feedback.html", {
'tips': review_tips,
})
else:
html = u""
return Fragment(html)
mentoring_view = student_view # Same as student_view but Studio won't wrap it with the editing header/buttons
def author_view(self, context=None):
""" Show example content in Studio """
if not context.get("per_question_review_tips"):
example = self._("(Example tip:) Since you got Question 1 wrong, review Chapter 12 of your textbook.")
context["score_summary"] = {"review_tips": [example]}
return self.student_view(context)
@XBlock.needs("i18n")
class ReviewStepBlock(
StudioContainerWithNestedXBlocksMixin, XBlockWithTranslationServiceMixin, XBlockWithPreviewMixin, XBlock
):
"""
A dedicated step for reviewing results as the last step of a Step Builder sequence.
"""
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 [
ConditionalMessageBlock,
NestedXBlockSpec(None, category='html', label=self._("HTML")),
NestedXBlockSpec(ScoreSummaryBlock, single_instance=True),
NestedXBlockSpec(PerQuestionFeedbackBlock, single_instance=True),
]
def student_view(self, context=None):
"""
Normal view of the review step.
The parent Step Builder block should pass in appropriate context information:
- score_summary
"""
context = context.copy() if context else {}
fragment = Fragment()
if "score_summary" not in context:
fragment.add_content(u"Error: This block only works inside a Step Builder block.")
elif not context["score_summary"]:
# Note: The following text should never be seen (in theory) so does not need to be translated.
fragment.add_content(u"Your score and review messages will appear here.")
else:
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
fragment.add_content(u"<p>[{}]</p>".format(u"Error: Unable to load child component."))
else:
if hasattr(child, 'is_applicable'):
if not child.is_applicable(context):
continue # Hide conditional messages that don't meet their criteria
context["is_pages_view"] = True # This is a hack so Studio doesn't wrap our component blocks.
child_fragment = child.render('student_view', context)
fragment.add_frag_resources(child_fragment)
fragment.add_content(child_fragment.content)
return fragment
mentoring_view = student_view
def studio_view(self, context=None):
""" Studio View """
return Fragment(u'<p>{}</p>'.format(self._("This XBlock does not have any settings.")))
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.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/container_edit.js'))
fragment.initialize_js('ProblemBuilderContainerEdit')
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