Commit cc9498cd by Braden MacDonald

Refactored Step Builder Messages

parent 5b751841
......@@ -35,15 +35,15 @@ from xblock.fragment import Fragment
from xblock.validation import ValidationMessage
from .message import MentoringMessageBlock
from .mixins import (
_normalize_id, QuestionMixin, MessageParentMixin, StepParentMixin, XBlockWithTranslationServiceMixin
)
from .step_review import ReviewStepBlock
from xblockutils.helpers import child_isinstance
from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable import (
StudioEditableXBlockMixin, StudioContainerXBlockMixin, StudioContainerWithNestedXBlocksMixin
NestedXBlockSpec, StudioEditableXBlockMixin, StudioContainerXBlockMixin, StudioContainerWithNestedXBlocksMixin,
)
......@@ -925,10 +925,16 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes
@property
def has_review_step(self):
from .step import ReviewStepBlock
return any(child_isinstance(self, child_id, ReviewStepBlock) for child_id in self.children)
@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):
questions = self.questions
total_child_weight = sum(float(question.weight) for question in questions)
......@@ -981,11 +987,12 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes
context = context or {}
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:
child = self.runtime.get_block(child_id)
if child is None: # child should not be None but it can happen due to bugs or permission issues
child_content = u"<p>[{}]</p>".format(self._(u"Error: Unable to load child component."))
elif not isinstance(child, MentoringMessageBlock):
else:
child_fragment = self._render_child_fragment(child, context, view='mentoring_view')
fragment.add_frag_resources(child_fragment)
child_content = child_fragment.content
......@@ -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_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)
fragment.initialize_js('MentoringWithStepsBlock')
fragment.initialize_js('MentoringWithStepsBlock', {
'show_extended_feedback': self.show_extended_feedback(),
})
return fragment
......@@ -1021,10 +1029,11 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes
NestedXBlockSpec allows explicitly setting disabled/enabled state, disabled reason (if any) and single/multiple
instances
"""
from .step import MentoringStepBlock, ReviewStepBlock # Import here to avoid circular dependency
# Import here to avoid circular dependency
from .step import MentoringStepBlock
return [
MentoringStepBlock,
ReviewStepBlock,
NestedXBlockSpec(ReviewStepBlock, single_instance=True),
]
@XBlock.json_handler
......@@ -1048,11 +1057,15 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes
self.active_step = new_value
elif new_value == len(self.step_ids):
# The user just completed the final step.
if self.has_review_step:
self.active_step = -1
# Update the number of attempts, if necessary:
if self.num_attempts < self.max_attempts:
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
# And publish the score:
score = self.score
......@@ -1061,12 +1074,13 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes
'max_value': self.max_score(),
}
self.runtime.publish(self, 'grade', grade_data)
response_data['grade_data'] = self.get_grade()
response_data['active_step'] = self.active_step
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
return {
'score': score.percentage,
......@@ -1078,7 +1092,7 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes
'partial': self.partial_json(stringify=False),
'complete': self.complete,
'max_attempts_reached': self.max_attempts_reached,
'assessment_review_tips': self.review_tips,
'review_tips': self.review_tips,
}
@XBlock.json_handler
......@@ -1101,9 +1115,7 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes
}
def author_preview_view(self, context):
context = context.copy() if context else {}
context['author_preview_view'] = True
return super(MentoringWithExplicitStepsBlock, self).author_preview_view(context)
return self.student_view(context)
def author_edit_view(self, context):
"""
......@@ -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-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/mentoring_with_steps_edit.js'))
fragment.initialize_js('MentoringWithStepsEdit')
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/container_edit.js'))
fragment.initialize_js('ProblemBuilderContainerEdit')
return fragment
......@@ -100,18 +100,6 @@ class MentoringMessageBlock(XBlock, StudioEditableXBlockMixin, XBlockWithTransla
"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(
......@@ -203,8 +191,3 @@ class CompletedMentoringMessageShim(object):
class IncompleteMentoringMessageShim(object):
CATEGORY = 'pb-message'
STUDIO_LABEL = _("Message (Incomplete)")
class OnReviewMentoringMessageShim(object):
CATEGORY = 'pb-message'
STUDIO_LABEL = _("Message (Review)")
......@@ -357,8 +357,8 @@ class PlotBlock(StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin
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_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.initialize_js('PlotEdit')
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/container_edit.js'))
fragment.initialize_js('ProblemBuilderContainerEdit')
return fragment
......
......@@ -6,6 +6,13 @@
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=step-builder] .url-name-footer .url-name,
.xblock[data-block-type=problem-builder] .url-name-footer .url-name,
......
......@@ -234,6 +234,11 @@
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 {
position: absolute;
top: 50%;
......@@ -242,4 +247,4 @@
padding: 1.5em;
background-color: white;
box-shadow: 0 10px 20px #5C5C5C;
}
\ No newline at end of file
}
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:
if (typeof gettext == "undefined") {
......@@ -8,23 +8,22 @@ function MentoringWithStepsBlock(runtime, element) {
var children = runtime.children(element);
var steps = [];
var reviewStep;
for (var i = 0; i < children.length; i++) {
var child = children[i];
var blockType = $(child.element).data('block-type');
if (blockType === 'sb-step') {
steps.push(child);
} else if (blockType === 'sb-review-step') {
reviewStep = child;
}
}
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 message = $('.sb-step-message', element);
var checkmark, submitDOM, nextDOM, reviewDOM, tryAgainDOM,
gradeDOM, attemptsDOM, reviewTipsDOM, reviewLinkDOM, submitXHR;
var checkmark, submitDOM, nextDOM, reviewButtonDOM, tryAgainDOM,
gradeDOM, attemptsDOM, reviewLinkDOM, submitXHR;
var reviewStepDOM = $("[data-block-type=sb-review-step]", element);
var hasAReviewStep = reviewStepDOM.length == 1;
function isLastStep() {
return (activeStep === steps.length-1);
......@@ -43,8 +42,7 @@ function MentoringWithStepsBlock(runtime, element) {
}
function extendedFeedbackEnabled() {
var data = gradeDOM.data();
return data.extended_feedback === "True";
return !!(params.extended_feedback); // Show extended feedback when all attempts are used up?
}
function showFeedback(response) {
......@@ -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() {
submitDOM.attr('disabled', 'disabled');
......@@ -84,8 +66,8 @@ function MentoringWithStepsBlock(runtime, element) {
if (nextDOM.is(':visible')) { nextDOM.focus(); }
if (atReviewStep()) {
if (reviewStep) {
reviewDOM.removeAttr('disabled');
if (hasAReviewStep) {
reviewButtonDOM.removeAttr('disabled');
} else {
if (someAttemptsLeft()) {
tryAgainDOM.removeAttr('disabled');
......@@ -111,7 +93,8 @@ function MentoringWithStepsBlock(runtime, element) {
// We are now showing the review step / end
// Update the number of attempts.
attemptsDOM.data('num_attempts', response.num_attempts);
updateGrade(response.grade_data);
reviewStepDOM.html($(response.review_html).html());
updateControls();
} else if (!hasQuestion) {
// This was a step with no questions, so proceed to the next step / review:
updateDisplay();
......@@ -156,7 +139,6 @@ function MentoringWithStepsBlock(runtime, element) {
hideAllSteps();
hideReviewStep();
attemptsDOM.html('');
reviewTipsDOM.empty().hide();
message.hide();
}
......@@ -186,54 +168,32 @@ function MentoringWithStepsBlock(runtime, element) {
} else {
nextDOM.removeAttr('disabled');
}
if (isLastStep() && reviewStep) {
if (isLastStep() && hasAReviewStep) {
if (step.hasQuestion()) {
reviewDOM.attr('disabled', 'disabled');
reviewButtonDOM.attr('disabled', 'disabled');
} else {
reviewDOM.removeAttr('disabled')
reviewButtonDOM.removeAttr('disabled')
}
reviewDOM.show();
reviewButtonDOM.show();
}
}
}
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()) {
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();
nextDOM.hide();
reviewDOM.hide();
reviewButtonDOM.hide();
tryAgainDOM.show();
reviewStepDOM.show();
}
function hideReviewStep() {
if (reviewStep) {
reviewStep.hideAssessmentMessage();
reviewStep.clearGrade(gradeDOM);
}
reviewStepDOM.hide()
}
function getStepToReview(event) {
......@@ -249,8 +209,8 @@ function MentoringWithStepsBlock(runtime, element) {
updateNextLabel();
if (isLastStep()) {
reviewDOM.show();
reviewDOM.removeAttr('disabled');
reviewButtonDOM.show();
reviewButtonDOM.removeAttr('disabled');
nextDOM.hide();
nextDOM.attr('disabled', 'disabled');
} else {
......@@ -307,8 +267,8 @@ function MentoringWithStepsBlock(runtime, element) {
if (isLastStep() && step.hasQuestion()) {
nextDOM.hide();
} else if (isLastStep()) {
reviewDOM.one('click', submit);
reviewDOM.removeAttr('disabled');
reviewButtonDOM.one('click', submit);
reviewButtonDOM.removeAttr('disabled');
nextDOM.hide()
} else if (!step.hasQuestion()) {
nextDOM.one('click', submit);
......@@ -388,7 +348,7 @@ function MentoringWithStepsBlock(runtime, element) {
nextDOM.off();
nextDOM.on('click', updateDisplay);
nextDOM.show();
reviewDOM.hide();
reviewButtonDOM.hide();
}
}
......@@ -434,7 +394,7 @@ function MentoringWithStepsBlock(runtime, element) {
hideAllSteps();
// 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.on('click', submit);
......@@ -446,19 +406,21 @@ function MentoringWithStepsBlock(runtime, element) {
nextDOM.on('click', updateDisplay);
}
reviewDOM = $(element).find('.submit .input-review');
reviewDOM.on('click', showGrade);
reviewButtonDOM = $(element).find('.submit .input-review');
reviewButtonDOM.on('click', showGrade);
tryAgainDOM = $(element).find('.submit .input-try-again');
tryAgainDOM.on('click', tryAgain);
gradeDOM = $('.grade', element);
attemptsDOM = $('.attempts', element);
reviewTipsDOM = $('.assessment-review-tips', element);
reviewLinkDOM = $(element).find('.review-link');
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
// (sets up click handlers for questions and makes sure answer data is up-to-date)
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 (
from problem_builder.answer import AnswerBlock, AnswerRecapBlock
from problem_builder.mcq import MCQBlock, RatingBlock
from .message import (
CompletedMentoringMessageShim, IncompleteMentoringMessageShim, OnReviewMentoringMessageShim
)
from problem_builder.mixins import EnumerableChildMixin, MessageParentMixin, StepParentMixin
from problem_builder.mixins import EnumerableChildMixin, StepParentMixin
from problem_builder.mrq import MRQBlock
from problem_builder.plot import PlotBlock
from problem_builder.slider import SliderBlock
......@@ -69,11 +66,6 @@ class Correctness(object):
INCORRECT = 'incorrect'
class HtmlBlockShim(object):
CATEGORY = 'html'
STUDIO_LABEL = _(u"HTML")
@XBlock.needs('i18n')
class MentoringStepBlock(
StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin, XBlockWithPreviewMixin,
......@@ -152,7 +144,8 @@ class MentoringStepBlock(
return [
NestedXBlockSpec(AnswerBlock, boilerplate='studio_default'),
MCQBlock, RatingBlock, MRQBlock, HtmlBlockShim,
MCQBlock, RatingBlock, MRQBlock,
NestedXBlockSpec(None, category="html", label=self._("HTML")),
AnswerRecapBlock, MentoringTableBlock, PlotBlock, SliderBlock
] + additional_blocks
......@@ -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-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/step_edit.js'))
fragment.initialize_js('StepEdit')
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/container_edit.js'))
fragment.initialize_js('ProblemBuilderContainerEdit')
return fragment
def mentoring_view(self, context=None):
......@@ -275,96 +268,3 @@ class MentoringStepBlock(
fragment.initialize_js('MentoringStepBlock')
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 @@
{{ child_content|safe }}
{% 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">
<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-next" value="Next Step" disabled="disabled" />
<input type="button" class="input-review" value="Review grade" disabled="disabled" />
......@@ -35,11 +23,7 @@
<div class="attempts"
data-max_attempts="{{ self.max_attempts }}" data-num_attempts="{{ self.num_attempts }}">
</div>
</div>
<div class="assessment-review-tips"></div>
</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 = [
'problem-builder = problem_builder.mentoring:MentoringBlock',
'step-builder = problem_builder.mentoring:MentoringWithExplicitStepsBlock',
'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-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