Commit ff271b2d by Sven Marnach

Merge pull request #89 from open-craft/step-builder-reviews-refactor

Refactored Step Builder Messages
parents 5b751841 8b17cb4d
...@@ -45,24 +45,6 @@ root folder: ...@@ -45,24 +45,6 @@ root folder:
$ pip install -r requirements.txt $ pip install -r requirements.txt
``` ```
Enabling in Studio
------------------
You can enable the Problem Builder and Step Builder XBlocks in Studio
by modifying the advanced settings for your course:
1. From the main page of a specific course, navigate to **Settings** ->
**Advanced Settings** from the top menu.
2. Find the **Advanced Module List** setting.
3. To enable Problem Builder for your course, add `"problem-builder"`
to the modules listed there.
4. To enable Step Builder for your course, add `"step-builder"` to the
modules listed there.
5. Click the **Save changes** button.
Note that it is perfectly fine to enable both Problem Builder and Step
Builder for your course -- the blocks do not interfere with each other.
Usage Usage
----- -----
......
Problem Builder Usage
=====================
When you add the **Problem Builder** component to a course in the studio, the
built-in editing tools guide you through the process of configuring the block
and adding individual questions.
See [Question Types](Questions.md) to learn about the various types of question
that can be added to a Problem Builder block.
Configuration Options
---------------------
### Maximum Attempts
You can limit the number of times students are allowed to complete a
Mentoring component by setting the **Max. attempts allowed** option.
Before submitting an answer for the first time:
![Max Attempts Before](img/max-attempts-before.png)
After submitting a wrong answer two times:
![Max Attempts Reached](img/max-attempts-reached.png)
### Custom Window Size for Tip Popups
You can specify **Width** and **Height** attributes of any Tip
component to customize the popup window size. The value of those
attributes should be valid CSS (e.g. `50px`).
Questions and Other Components
==============================
These are the types of questions that can be added to Problem Builder and Step
Builder:
### Free-form Questions
Free-form questions are represented by a **Long Answer** component.
Example screenshot before answering the question:
![Answer Initial](img/answer-1.png)
Screenshot after answering the question:
![Answer Complete](img/answer-2.png)
You can add **Long Answer Recap** components to problem builder blocks later on
in the course to provide a read-only view of any answer that the student entered
earlier.
The read-only answer is rendered as a quote in the LMS:
![Answer Read-Only](img/answer-3.png)
### Multiple Choice Questions (MCQs)
Multiple Choice Questions can be added to a problem builder component and have
the following configurable options:
* **Question** - The question to ask the student
* **Message** - A feedback message to display to the student after they have
made their choice.
* **Weight** - The weight is used when computing total grade/score of the
problem builder block. The larger the weight, the more influence this question
will have on the grade. Value of zero means this question has no influence on
the grade (float, defaults to `1`).
* **Correct Choice[s]** - Specify which choice[s] are considered correct. If a
student selects a choice that is not indicated as correct here, the student
will get the question wrong.
Using the Studio editor, you can add **Custom Choice** blocks to an MCQ. Each
Custom Choice represents one of the options from which students will choose
their answer.
You can also add **Tip** entries. Each Tip must be configured to link it to one
or more of the choices. If the student selects a choice, the tip will be
displayed.
**Screenshots**
Before attempting to answer the questions:
![MCQ Initial](img/mcq-1.png)
While attempting to complete the questions:
![MCQ Attempting](img/mcq-2.png)
After successfully completing the questions:
![MCQ Success](img/mcq-3.png)
#### Rating Questions
When constructing questions where the student rates some topic on the scale from
`1` to `5` (e.g. a Likert Scale), you can use the Rating question type, which
includes built-in numbered choices from 1 to 5. The `Low` and `High` settings
specify the text shown next to the lowest and highest valued choice.
Rating questions are a specialized type of MCQ, and the same instructions apply.
You can also still add **Custom Choice** components if you want additional
choices to be available such as "I don't know".
### Multiple Response Questions (MRQs)
Multiple Response Questions are set up similarly to MCQs. The answers are
rendered as checkboxes. Unlike MCQs where only a single answer can be selected,
MRQs allow multiple answers to be selected at the same time.
MRQ questions have these configurable settings:
* **Question** - The question to ask the student
* **Required Choices** - For any choices selected here, if the student does
*not* select that choice, they will lose marks.
* **Ignored Choices** - For any choices selected here, the student will always
be considered correct whether they choose this choice or not.
* Message - A feedback message to display to the student after they have made
their choice.
* **Weight** - The weight is used when computing total grade/score of the
problem builder block. The larger the weight, the more influence this question
will have on the grade. Value of zero means this question has no influence on
the grade (float, defaults to `1`).
* **Hide Result** - If set to `True`, the feedback icons next to each choice
will not be displayed (This is `False` by default).
The **Custom Choice** and **Tip** components work the same way as they do when
used with MCQs (see above).
**Screenshots**
Before attempting to answer the questions:
![MRQ Initial](img/mrq-1.png)
While attempting to answer the questions:
![MRQ Attempt](img/mrq-2.png)
After clicking on the feedback icon next to the "Its bugs" answer:
![MRQ Attempt](img/mrq-3.png)
After successfully completing the questions:
![MRQ Success](img/mrq-4.png)
Other Components
================
### Tables
Tables allow you to present answers to multiple free-form questions in a concise
way. Once you create an **Answer Recap Table** inside a Mentoring component in
Studio, you will be able to add columns to the table. Each column has an
optional **Header** setting that you can use to add a header to that column.
Each column can contain one or more **Answer Recap** elements, as well as HTML
components.
Screenshot:
![Table Screenshot](img/mentoring-table.png)
### "Dashboard" Self-Assessment Summary Block
[Instructions for using the "Dashboard" Self-Assessment Summary Block](Dashboard.md)
Step Builder Usage
==================
The Step Builder is similar to Problem Builder, but it allows authors to group
questions into explict steps, and provide more detailed feedback to students.
Instead of adding questions to Step Builder itself, you'll need to add one or
more **Mentoring Step** blocks to Step Builder. You can then add one or more
questions to each step. This allows you to group questions into logical units
(without being limited to showing only a single question per step). As students
progress through the block, Step Builder will display one step at a time. All
questions belonging to a step need to be completed before the step can be
submitted.
In addition to regular steps, Step Builder can also contain a **Review Step**
component which:
* allows students to review their performance
* allows students to jump back to individual steps to review their
answers (if **Extended feedback** setting is enabled on the Step Builder block
and the maximum number of attempts has been reached.)
* supports "conditional messages" that will can shown during the review step
based on certain conditions such as:
* the student achieved a perfect score, or not
* the student is allowed to try again, or has used up all attempts
**Screenshots: Step**
Step with multiple questions (before submitting it):
![Step with multiple questions, before submit](img/step-with-multiple-questions-before-submit.png)
Step with multiple questions (after submitting it):
![Step with multiple questions, after submit](img/step-with-multiple-questions-after-submit.png)
As indicated by the orange check mark, this step is *partially*
correct (i.e., some answers are correct and some are incorrect or
partially correct).
**Screenshots: Review Step**
Unlimited attempts available, all answers correct, and a conditional message
that says "Great job!" configured to appear if the student gets a perfect score:
![Unlimited attempts available](img/review-step-unlimited-attempts-available.png)
Limited attempts, some attempts remaining, some answers incorrect, and a custom
review/study tip.
![Some attempts remaining](img/review-step-some-attempts-remaining.png)
Limited attempts, no attempts remaining, extended feedback off:
![No attempts remaining, extended feedback off](img/review-step-no-attempts-remaining-extended-feedback-off.png)
Limited attempts, no attempts remaining, extended feedback on:
![No attempts remaining, extended feedback on](img/review-step-no-attempts-remaining-extended-feedback-on.png)
**Screenshots: Step-level feedback**
Reviewing performance for a single step:
![Reviewing performance for single step](img/reviewing-performance-for-single-step.png)
Configuration Options
---------------------
### Maximum Attempts
You can limit the number of times students are allowed to complete a
Mentoring component by setting the **Max. attempts allowed** option.
Before submitting an answer for the first time:
![Max Attempts Before](img/max-attempts-before.png)
After submitting a wrong answer two times:
![Max Attempts Reached](img/max-attempts-reached.png)
...@@ -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)")
from lazy import lazy from lazy import lazy
from xblock.fields import String, Boolean, Float, Scope, UNIQUE_ID from xblock.fields import String, Boolean, Float, Scope, UNIQUE_ID
from xblock.fragment import Fragment
from xblockutils.helpers import child_isinstance from xblockutils.helpers import child_isinstance
from xblockutils.resources import ResourceLoader from xblockutils.resources import ResourceLoader
...@@ -167,3 +168,11 @@ class QuestionMixin(EnumerableChildMixin): ...@@ -167,3 +168,11 @@ class QuestionMixin(EnumerableChildMixin):
decorative elements/instructions. decorative elements/instructions.
""" """
return self.mentoring_view(context) return self.mentoring_view(context)
class NoSettingsMixin(object):
""" Mixin for an XBlock that has no settings """
def studio_view(self, _context=None):
""" Studio View """
return Fragment(u'<p>{}</p>'.format(self._("This XBlock does not have any settings.")))
...@@ -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,
...@@ -43,7 +50,6 @@ ...@@ -43,7 +50,6 @@
cursor: default; cursor: default;
} }
.xblock[data-block-type=sb-review-step] .submission-message-help p,
.xblock[data-block-type=step-builder] .submission-message-help p, .xblock[data-block-type=step-builder] .submission-message-help p,
.xblock[data-block-type=problem-builder] .submission-message-help p { .xblock[data-block-type=problem-builder] .submission-message-help p {
border-top: 1px solid #ddd; border-top: 1px solid #ddd;
...@@ -53,6 +59,18 @@ ...@@ -53,6 +59,18 @@
padding-top: 0.3em; padding-top: 0.3em;
} }
.xblock[data-block-type=sb-review-step] .conditional-message-help p {
font-size: 0.8em;
font-style: italic;
margin-bottom: 0.4em;
}
.xblock-preview_view-sb-conditional-message {
border-top: 1px solid #ddd;
margin-top: 1.3em;
padding-top: 0.2em;
}
.xblock-author_view-pb-slider .url-name-footer { .xblock-author_view-pb-slider .url-name-footer {
margin: 0 -20px -20px -20px; /* Counteract spacing from xblock-render wrapper. */ margin: 0 -20px -20px -20px; /* Counteract spacing from xblock-render wrapper. */
} }
...@@ -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%;
...@@ -242,4 +247,4 @@ ...@@ -242,4 +247,4 @@
padding: 1.5em; padding: 1.5em;
background-color: white; background-color: white;
box-shadow: 0 10px 20px #5C5C5C; 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);
}
}
...@@ -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 = $("div.xblock[data-block-type=sb-review-step], div.xblock-v1[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);
...@@ -42,11 +41,6 @@ function MentoringWithStepsBlock(runtime, element) { ...@@ -42,11 +41,6 @@ function MentoringWithStepsBlock(runtime, element) {
return (data.num_attempts < data.max_attempts); return (data.num_attempts < data.max_attempts);
} }
function extendedFeedbackEnabled() {
var data = gradeDOM.data();
return data.extended_feedback === "True";
}
function showFeedback(response) { function showFeedback(response) {
if (response.step_status === 'correct') { if (response.step_status === 'correct') {
checkmark.addClass('checkmark-correct icon-ok fa-check'); checkmark.addClass('checkmark-correct icon-ok fa-check');
...@@ -61,22 +55,6 @@ function MentoringWithStepsBlock(runtime, element) { ...@@ -61,22 +55,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 +62,8 @@ function MentoringWithStepsBlock(runtime, element) { ...@@ -84,8 +62,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 +89,8 @@ function MentoringWithStepsBlock(runtime, element) { ...@@ -111,7 +89,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 +135,6 @@ function MentoringWithStepsBlock(runtime, element) { ...@@ -156,7 +135,6 @@ function MentoringWithStepsBlock(runtime, element) {
hideAllSteps(); hideAllSteps();
hideReviewStep(); hideReviewStep();
attemptsDOM.html(''); attemptsDOM.html('');
reviewTipsDOM.empty().hide();
message.hide(); message.hide();
} }
...@@ -186,54 +164,32 @@ function MentoringWithStepsBlock(runtime, element) { ...@@ -186,54 +164,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 +205,8 @@ function MentoringWithStepsBlock(runtime, element) { ...@@ -249,8 +205,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 +263,8 @@ function MentoringWithStepsBlock(runtime, element) { ...@@ -307,8 +263,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 +344,7 @@ function MentoringWithStepsBlock(runtime, element) { ...@@ -388,7 +344,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 +390,7 @@ function MentoringWithStepsBlock(runtime, element) { ...@@ -434,7 +390,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 +402,21 @@ function MentoringWithStepsBlock(runtime, element) { ...@@ -446,19 +402,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:
$(element).on('click', 'a.step-link', getStepToReview);
// Initialize individual steps // Initialize individual steps
// (sets up click handlers for questions and makes sure answer data is up-to-date) // (sets up click handlers for questions and makes sure answer data is up-to-date)
var options = { var options = {
......
function MentoringWithStepsEdit(runtime, element) {
"use strict";
var $buttons = $('.add-xblock-component-button[data-category=sb-review-step]', element);
var blockIsPresent = function(klass) {
return $('.xblock ' + klass).length > 0;
};
var updateButton = function(button, condition) {
button.toggleClass('disabled', condition);
};
var disableButton = function(ev) {
if ($(this).is('.disabled')) {
ev.preventDefault();
ev.stopPropagation();
} else {
$(this).addClass('disabled');
}
};
var updateButtons = function(buttons) {
buttons.each(function() {
var button = $(this);
updateButton(button, blockIsPresent('.xblock-header-sb-review-step'));
});
};
var initButtons = function() {
updateButtons($buttons);
$buttons.on('click', disableButton);
};
var resetButtons = function() {
var $disabledButtons = $buttons.filter('.disabled');
updateButtons($disabledButtons);
};
ProblemBuilderUtil.transformClarifications(element);
initButtons();
runtime.listenTo('deleted-child', resetButtons);
}
function PlotEdit(runtime, element) {
'use strict';
StudioContainerXBlockWithNestedXBlocksMixin(runtime, element);
ProblemBuilderUtil.transformClarifications(element);
}
function ReviewStepBlock(runtime, element) {
var gradeTemplate = _.template($('#xblock-feedback-template').html());
var reviewStepsTemplate = _.template($('#xblock-step-links-template').html());
var assessmentMessageDOM = $('.assessment-message', element);
return {
'showAssessmentMessage': function() {
var assessmentMessage = assessmentMessageDOM.data('assessment_message');
assessmentMessageDOM.html(assessmentMessage);
assessmentMessageDOM.show();
},
'hideAssessmentMessage': function() {
assessmentMessageDOM.html('');
assessmentMessageDOM.hide();
},
'updateAssessmentMessage': function(grade, callback) {
var handlerUrl = runtime.handlerUrl(element, 'get_assessment_message');
$.post(handlerUrl, JSON.stringify(grade)).success(function(response) {
assessmentMessageDOM.data('assessment_message', response.assessment_message);
callback();
});
},
'renderGrade': function(gradeDOM, showExtendedFeedback) {
var data = gradeDOM.data();
_.extend(data, {
'runDetails': function(correctness) {
if (!showExtendedFeedback) {
return '';
}
var self = this;
return reviewStepsTemplate({'questions': self[correctness], 'correctness': correctness});
}
});
gradeDOM.html(gradeTemplate(data));
},
'clearGrade': function(gradeDOM) {
gradeDOM.html('');
}
};
}
function ReviewStepEdit(runtime, element) {
"use strict";
var $buttons = $('.add-xblock-component-button[data-category=pb-message]', element);
var blockIsPresent = function(klass) {
return $('.xblock ' + klass).length > 0;
};
var updateButton = function(button, condition) {
button.toggleClass('disabled', condition);
};
var disableButton = function(ev) {
if ($(this).is('.disabled')) {
ev.preventDefault();
ev.stopPropagation();
} else {
$(this).addClass('disabled');
}
};
var updateButtons = function(buttons) {
buttons.each(function() {
var button = $(this);
var msgType = button.data('boilerplate');
updateButton(button, blockIsPresent('.submission-message.'+msgType));
});
};
var initButtons = function() {
updateButtons($buttons);
$buttons.on('click', disableButton);
};
var resetButtons = function() {
var $disabledButtons = $buttons.filter('.disabled');
updateButtons($disabledButtons);
};
ProblemBuilderUtil.transformClarifications(element);
initButtons();
runtime.listenTo('deleted-child', resetButtons);
}
function StepEdit(runtime, element) {
'use strict';
StudioContainerXBlockWithNestedXBlocksMixin(runtime, element);
ProblemBuilderUtil.transformClarifications(element);
}
...@@ -32,10 +32,7 @@ from xblockutils.studio_editable import ( ...@@ -32,10 +32,7 @@ from xblockutils.studio_editable import (
from problem_builder.answer import AnswerBlock, AnswerRecapBlock from problem_builder.answer import AnswerBlock, AnswerRecapBlock
from problem_builder.mcq import MCQBlock, RatingBlock from problem_builder.mcq import MCQBlock, RatingBlock
from .message import ( from problem_builder.mixins import EnumerableChildMixin, StepParentMixin
CompletedMentoringMessageShim, IncompleteMentoringMessageShim, OnReviewMentoringMessageShim
)
from problem_builder.mixins import EnumerableChildMixin, MessageParentMixin, StepParentMixin
from problem_builder.mrq import MRQBlock from problem_builder.mrq import MRQBlock
from problem_builder.plot import PlotBlock from problem_builder.plot import PlotBlock
from problem_builder.slider import SliderBlock from problem_builder.slider import SliderBlock
...@@ -69,11 +66,6 @@ class Correctness(object): ...@@ -69,11 +66,6 @@ class Correctness(object):
INCORRECT = 'incorrect' INCORRECT = 'incorrect'
class HtmlBlockShim(object):
CATEGORY = 'html'
STUDIO_LABEL = _(u"HTML")
@XBlock.needs('i18n') @XBlock.needs('i18n')
class MentoringStepBlock( class MentoringStepBlock(
StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin, XBlockWithPreviewMixin, StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin, XBlockWithPreviewMixin,
...@@ -152,7 +144,8 @@ class MentoringStepBlock( ...@@ -152,7 +144,8 @@ class MentoringStepBlock(
return [ return [
NestedXBlockSpec(AnswerBlock, boilerplate='studio_default'), NestedXBlockSpec(AnswerBlock, boilerplate='studio_default'),
MCQBlock, RatingBlock, MRQBlock, HtmlBlockShim, MCQBlock, RatingBlock, MRQBlock,
NestedXBlockSpec(None, category="html", label=self._("HTML")),
AnswerRecapBlock, MentoringTableBlock, PlotBlock, SliderBlock AnswerRecapBlock, MentoringTableBlock, PlotBlock, SliderBlock
] + additional_blocks ] + additional_blocks
...@@ -229,8 +222,8 @@ class MentoringStepBlock( ...@@ -229,8 +222,8 @@ class MentoringStepBlock(
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder-edit.css')) fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder-edit.css'))
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder-tinymce-content.css')) fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder-tinymce-content.css'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/util.js')) fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/util.js'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/step_edit.js')) fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/container_edit.js'))
fragment.initialize_js('StepEdit') fragment.initialize_js('ProblemBuilderContainerEdit')
return fragment return fragment
def mentoring_view(self, context=None): def mentoring_view(self, context=None):
...@@ -275,96 +268,3 @@ class MentoringStepBlock( ...@@ -275,96 +268,3 @@ class MentoringStepBlock(
fragment.initialize_js('MentoringStepBlock') fragment.initialize_js('MentoringStepBlock')
return fragment return fragment
class ReviewStepBlock(MessageParentMixin, StudioContainerWithNestedXBlocksMixin, XBlockWithPreviewMixin, XBlock):
""" A dedicated step for reviewing results for a mentoring block """
CATEGORY = 'sb-review-step'
STUDIO_LABEL = _("Review Step")
display_name = String(
default="Review Step"
)
@property
def allowed_nested_blocks(self):
"""
Returns a list of allowed nested XBlocks. Each item can be either
* An XBlock class
* A NestedXBlockSpec
If XBlock class is used it is assumed that this XBlock is enabled and allows multiple instances.
NestedXBlockSpec allows explicitly setting disabled/enabled state,
disabled reason (if any) and single/multiple instances.
"""
return [
NestedXBlockSpec(CompletedMentoringMessageShim, boilerplate='completed'),
NestedXBlockSpec(IncompleteMentoringMessageShim, boilerplate='incomplete'),
NestedXBlockSpec(OnReviewMentoringMessageShim, boilerplate='on-review'),
]
@XBlock.json_handler
def get_assessment_message(self, grade, suffix):
# Data passed as "grade" comes from "get_grade" handler of Step Builder (MentoringWithExplicitStepsBlock)
complete = grade.get('complete')
max_attempts_reached = grade.get('max_attempts_reached')
return {
'assessment_message': self.assessment_message(complete, max_attempts_reached)
}
def assessment_message(self, complete=None, max_attempts_reached=None):
if complete is None and max_attempts_reached is None:
parent = self.get_parent()
complete = parent.complete
max_attempts_reached = parent.max_attempts_reached
if max_attempts_reached:
assessment_message = self.get_message_content('on-review', or_default=True)
else:
if complete: # All answers correct
assessment_message = self.get_message_content('completed', or_default=True)
else:
assessment_message = self.get_message_content('incomplete', or_default=True)
return assessment_message
def mentoring_view(self, context=None):
""" Mentoring View """
return self._render_view(context)
def student_view(self, context=None):
""" Student View """
return self._render_view(context)
def studio_view(self, context=None):
""" Studio View """
return Fragment(u'<p>This is a preconfigured block. It is not editable.</p>')
def _render_view(self, context):
fragment = Fragment()
fragment.add_content(loader.render_template('templates/html/review_step.html', {
'self': self,
}))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/review_step.js'))
fragment.initialize_js('ReviewStepBlock')
return fragment
def author_preview_view(self, context):
return Fragment(
u"<p>{}</p>".format(
_(u"This block summarizes a student's performance on the parent Step Builder block.")
)
)
def author_edit_view(self, context):
"""
Add some HTML to the author view that allows authors to add child blocks.
"""
context['wrap_children'] = {
'head': u'<div class="mentoring">',
'tail': u'</div>'
}
fragment = super(ReviewStepBlock, self).author_edit_view(context)
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder-edit.css'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/util.js'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/review_step_edit.js'))
fragment.initialize_js('ReviewStepEdit')
return fragment
<!-- Tips about specific questions the student got wrong. From pb-message[type=on-assessment-review-question] blocks -->
<script type="text/template" id="xblock-review-tips-template">
<p class="review-tips-intro"><%= gettext("You might consider reviewing the following items before your next assessment attempt:") %></p>
<ul class="review-tips-list">
<% for (var tip_idx in tips) {{ %>
<li><%= tips[tip_idx] %></li>
<% }} %>
</ul>
</script>
...@@ -13,20 +13,8 @@ ...@@ -13,20 +13,8 @@
{{ child_content|safe }} {{ child_content|safe }}
{% endfor %} {% endfor %}
<div class="grade"
data-score="{{ self.score.percentage }}"
data-correct_answer="{{ self.score.correct|length }}"
data-incorrect_answer="{{ self.score.incorrect|length }}"
data-partially_correct_answer="{{ self.score.partially_correct|length }}"
data-assessment_review_tips="{{ self.review_tips_json }}"
data-extended_feedback="{{ self.extended_feedback }}"
data-correct="{{ self.correct_json }}"
data-incorrect="{{ self.incorrect_json }}"
data-partial="{{ self.partial_json }}">
</div>
<div class="submit"> <div class="submit">
<span class="assessment-checkmark fa icon-2x"></span> <span class="step-overall-checkmark fa icon-2x"></span>
<input type="button" class="input-main" value="Submit" disabled="disabled" /> <input type="button" class="input-main" value="Submit" disabled="disabled" />
<input type="button" class="input-next" value="Next Step" disabled="disabled" /> <input type="button" class="input-next" value="Next Step" disabled="disabled" />
<input type="button" class="input-review" value="Review grade" disabled="disabled" /> <input type="button" class="input-review" value="Review grade" disabled="disabled" />
...@@ -35,11 +23,7 @@ ...@@ -35,11 +23,7 @@
<div class="attempts" <div class="attempts"
data-max_attempts="{{ self.max_attempts }}" data-num_attempts="{{ self.num_attempts }}"> data-max_attempts="{{ self.max_attempts }}" data-num_attempts="{{ self.num_attempts }}">
</div> </div>
</div> </div>
<div class="assessment-review-tips"></div>
</div> </div>
<div class="review-link"><a href="#">Review final grade</a></div> <div class="review-link"><a href="#">Review final grade</a></div>
......
<div class="sb-review-step">
<div class="assessment-message" data-assessment_message="{{ self.assessment_message }}"></div>
<script type="text/template" id="xblock-feedback-template">
<div class="grade-result">
<h2>
<%= _.template(gettext("You scored {percent}% on this assessment."), {percent: score}, {interpolate: /\{(.+?)\}/g}) %>
</h2>
<hr/>
<span class="assessment-checkmark icon-2x checkmark-correct icon-ok fa fa-check"></span>
<div class="results-section">
<p>
<%= _.template(
ngettext(
"You answered 1 question correctly.",
"You answered {number_correct} questions correctly.",
correct_answer
), {number_correct: correct_answer}, {interpolate: /\{(.+?)\}/g})
%>
</p>
<%= runDetails('correct') %>
</div>
<div class="clear"></div>
<span class="assessment-checkmark icon-2x checkmark-partially-correct icon-ok fa fa-check"></span>
<div class="results-section">
<p>
<%= _.template(
ngettext(
"You answered 1 question partially correctly.",
"You answered {number_partially_correct} questions partially correctly.",
partially_correct_answer
), {number_partially_correct: partially_correct_answer}, {interpolate: /\{(.+?)\}/g})
%>
</p>
<%= runDetails('partial') %>
</div>
<div class="clear"></div>
<span class="assessment-checkmark icon-2x checkmark-incorrect icon-exclamation fa fa-exclamation"></span>
<div class="results-section">
<p>
<%= _.template(
ngettext(
"You answered 1 question incorrectly.",
"You answered {number_incorrect} questions incorrectly.",
incorrect_answer
), {number_incorrect: incorrect_answer}, {interpolate: /\{(.+?)\}/g})
%>
</p>
<%= runDetails('incorrect') %>
</div>
<div class="clear"></div>
<hr/>
</div>
</script>
<!-- Template for extended feedback: Show extended feedback details when all attempts are used up. -->
<script type="text/template" id="xblock-step-links-template">
<ul class="review-list <%= correctness %>-list">
<% for (var question in questions) { %>
<%
var q = questions[question];
var last_question = question == questions.length - 1;
var second_last_question = question == questions.length - 2;
%>
<li>
<a href="#" class="step-link" data-step="<%= q.step %>"><%=
_.template(gettext("Question {number}"), {number: q.number}, {interpolate: /\{(.+?)\}/g})
%></a><% if (!last_question) { %><%= (questions.length > 2 ? ", " : "") %><%= (second_last_question ? " " + gettext("and"): "") %><% } %>
</li>
<% } %>
</ul>
</script>
</div>
{% load i18n %}
{# Tips about specific questions the student got wrong. From pb-message[type=on-assessment-review-question] blocks #}
<p class="review-tips-intro">{% trans "You might consider reviewing the following items before your next assessment attempt:" %}</p>
<ul class="review-tips-list">
{% for tip in tips %}
<li>{{tip|safe}}</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 with number=question.number %}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 with number=question.number %}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 with number=question.number %}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>
...@@ -270,7 +270,7 @@ class MentoringAssessmentBaseTest(ProblemBuilderBaseTest): ...@@ -270,7 +270,7 @@ class MentoringAssessmentBaseTest(ProblemBuilderBaseTest):
states[result] += 1 states[result] += 1
for name, count in states.items(): for name, count in states.items():
self.assertEqual(len(mentoring.find_elements_by_css_selector(".checkmark-{}".format(name))), count) self.assertEqual(len(mentoring.find_elements_by_css_selector(".submit .checkmark-{}".format(name))), count)
class GetChoices(object): class GetChoices(object):
......
...@@ -143,4 +143,4 @@ class SliderStepBlockTest(SliderBlockTestMixins, MentoringAssessmentBaseTest): ...@@ -143,4 +143,4 @@ class SliderStepBlockTest(SliderBlockTestMixins, MentoringAssessmentBaseTest):
def wait_for_init(self): def wait_for_init(self):
""" Wait for the scenario to initialize """ """ Wait for the scenario to initialize """
self.wait_until_hidden(self.browser.find_element_by_css_selector('.assessment-review-tips')) self.wait_until_visible(self.browser.find_elements_by_css_selector('.sb-step')[0])
...@@ -216,6 +216,11 @@ class StepBuilderTest(MentoringAssessmentBaseTest, MultipleSliderBlocksTestMixin ...@@ -216,6 +216,11 @@ class StepBuilderTest(MentoringAssessmentBaseTest, MultipleSliderBlocksTestMixin
self.assert_clickable(controls.review_link) self.assert_clickable(controls.review_link)
self.assert_hidden(controls.try_again) self.assert_hidden(controls.try_again)
def assert_review_conditional_messages_equal(self, step_builder, messages_expected):
""" Test that the Conditional Messages seen on the review match messages_expected. """
messages = step_builder.find_elements_by_css_selector('.review-conditional-message')
self.assertListEqual([msg.text for msg in messages], messages_expected)
def peek_at_review(self, step_builder, controls, expected, extended_feedback=False): def peek_at_review(self, step_builder, controls, expected, extended_feedback=False):
self.wait_until_text_in("You scored {percentage}% on this assessment.".format(**expected), step_builder) self.wait_until_text_in("You scored {percentage}% on this assessment.".format(**expected), step_builder)
...@@ -302,10 +307,12 @@ class StepBuilderTest(MentoringAssessmentBaseTest, MultipleSliderBlocksTestMixin ...@@ -302,10 +307,12 @@ class StepBuilderTest(MentoringAssessmentBaseTest, MultipleSliderBlocksTestMixin
) )
# Step should display 5 checkmarks (4 correct items for MRQ, plus step-level feedback about correctness) # Step should display 5 checkmarks (4 correct items for MRQ, plus step-level feedback about correctness)
correct_marks = step_builder.find_elements_by_css_selector('.checkmark-correct') correct_marks = step_builder.find_elements_by_css_selector('.sb-step .checkmark-correct')
incorrect_marks = step_builder.find_elements_by_css_selector('.checkmark-incorrect') incorrect_marks = step_builder.find_elements_by_css_selector('.sb-step .checkmark-incorrect')
self.assertEqual(len(correct_marks), 5) overall_mark = step_builder.find_elements_by_css_selector('.submit .checkmark-correct')
self.assertEqual(len(correct_marks), 4)
self.assertEqual(len(incorrect_marks), 0) self.assertEqual(len(incorrect_marks), 0)
self.assertEqual(len(overall_mark), 1)
item_feedbacks = [ item_feedbacks = [
"This is something everyone has to like about this MRQ", "This is something everyone has to like about this MRQ",
...@@ -416,18 +423,20 @@ class StepBuilderTest(MentoringAssessmentBaseTest, MultipleSliderBlocksTestMixin ...@@ -416,18 +423,20 @@ class StepBuilderTest(MentoringAssessmentBaseTest, MultipleSliderBlocksTestMixin
self.assertIn("Question 1 and Question 3", step_builder.find_element_by_css_selector('.correct-list').text) self.assertIn("Question 1 and Question 3", step_builder.find_element_by_css_selector('.correct-list').text)
if max_attempts == 1: if max_attempts == 1:
self.assert_message_text(step_builder, "On review message text") self.assert_review_conditional_messages_equal(
step_builder,
["Not quite!", "This message is shown when you run out of attempts."]
)
self.assert_disabled(controls.try_again) self.assert_disabled(controls.try_again)
return return
self.assert_message_text(step_builder, "Block incomplete message text") self.assert_review_conditional_messages_equal(step_builder, ["Not quite! You can try again, though."])
self.assert_clickable(controls.try_again) self.assert_clickable(controls.try_again)
# Try again # Try again
controls.try_again.click() controls.try_again.click()
self.wait_until_hidden(controls.try_again) self.wait_until_hidden(controls.try_again)
self.assert_no_message_text(step_builder)
# Step 1 # Step 1
# Submit free-form answer, go to next step # Submit free-form answer, go to next step
...@@ -464,15 +473,15 @@ class StepBuilderTest(MentoringAssessmentBaseTest, MultipleSliderBlocksTestMixin ...@@ -464,15 +473,15 @@ class StepBuilderTest(MentoringAssessmentBaseTest, MultipleSliderBlocksTestMixin
) )
if max_attempts == 2: if max_attempts == 2:
self.assert_review_conditional_messages_equal(
step_builder,
["Not quite!", "This message is shown when you run out of attempts."]
)
self.assert_disabled(controls.try_again) self.assert_disabled(controls.try_again)
else: else:
self.assert_review_conditional_messages_equal(step_builder, ["Not quite! You can try again, though."])
self.assert_clickable(controls.try_again) self.assert_clickable(controls.try_again)
if 1 <= max_attempts <= 2:
self.assert_message_text(step_builder, "On review message text")
else:
self.assert_message_text(step_builder, "Block incomplete message text")
if extended_feedback: if extended_feedback:
self.extended_feedback_checks(step_builder, controls, expected_results) self.extended_feedback_checks(step_builder, controls, expected_results)
...@@ -492,15 +501,21 @@ class StepBuilderTest(MentoringAssessmentBaseTest, MultipleSliderBlocksTestMixin ...@@ -492,15 +501,21 @@ class StepBuilderTest(MentoringAssessmentBaseTest, MultipleSliderBlocksTestMixin
self.multiple_response_question(None, step_builder, controls, ("Its beauty",), PARTIAL, last=True) self.multiple_response_question(None, step_builder, controls, ("Its beauty",), PARTIAL, last=True)
# The review tips for MCQ 2 and the MRQ should be shown: # The review tips for MCQ 2 and the MRQ should be shown:
review_tips = step_builder.find_element_by_css_selector('.assessment-review-tips') review_tips_intro = step_builder.find_element_by_css_selector('.review-tips-intro')
self.assertEqual(
review_tips_intro.text,
"You might consider reviewing the following items before your next assessment attempt:"
)
review_tips = step_builder.find_element_by_css_selector('.review-tips-list')
self.assertTrue(review_tips.is_displayed()) self.assertTrue(review_tips.is_displayed())
self.assertIn('You might consider reviewing the following items', review_tips.text) self.assertIn('Take another look at Lesson 1', review_tips.text)
self.assertIn('Take another look at', review_tips.text)
self.assertIn('Lesson 1', review_tips.text)
self.assertNotIn('Lesson 2', review_tips.text) # This MCQ was correct self.assertNotIn('Lesson 2', review_tips.text) # This MCQ was correct
self.assertIn('Lesson 3', review_tips.text) self.assertIn('Take another look at Lesson 3', review_tips.text)
# If attempts remain and student got some answers wrong, show "incomplete" message # If attempts remain and student got some answers wrong, show "incomplete" message
self.assert_message_text(step_builder, "Block incomplete message text") self.assert_review_conditional_messages_equal(
step_builder,
["Not quite! You can try again, though."]
)
# Try again # Try again
self.assert_clickable(controls.try_again) self.assert_clickable(controls.try_again)
...@@ -516,9 +531,10 @@ class StepBuilderTest(MentoringAssessmentBaseTest, MultipleSliderBlocksTestMixin ...@@ -516,9 +531,10 @@ class StepBuilderTest(MentoringAssessmentBaseTest, MultipleSliderBlocksTestMixin
self.html_section(step_builder, controls) self.html_section(step_builder, controls)
self.multiple_response_question(None, step_builder, controls, user_selection, CORRECT, last=True) self.multiple_response_question(None, step_builder, controls, user_selection, CORRECT, last=True)
# If attempts remain and student got all answers right, show "complete" message # We expect to see this congratulatory message now:
self.assert_message_text(step_builder, "Block completed message text") self.assert_review_conditional_messages_equal(step_builder, ["Great job!"])
self.assertFalse(review_tips.is_displayed()) # And no review tips should be shown:
self.assertEqual(len(step_builder.find_elements_by_css_selector('.review-tips-intro')), 0)
# Try again # Try again
self.assert_clickable(controls.try_again) self.assert_clickable(controls.try_again)
...@@ -534,16 +550,21 @@ class StepBuilderTest(MentoringAssessmentBaseTest, MultipleSliderBlocksTestMixin ...@@ -534,16 +550,21 @@ class StepBuilderTest(MentoringAssessmentBaseTest, MultipleSliderBlocksTestMixin
self.multiple_response_question(None, step_builder, controls, ("Its beauty",), PARTIAL, last=True) self.multiple_response_question(None, step_builder, controls, ("Its beauty",), PARTIAL, last=True)
# The review tips will not be shown because no attempts remain: # The review tips will not be shown because no attempts remain:
self.assertFalse(review_tips.is_displayed()) self.assertEqual(len(step_builder.find_elements_by_css_selector('.review-tips-intro')), 0)
def test_default_messages(self): @data(True, False)
def test_conditional_messages(self, include_messages):
"""
Test that conditional messages in the review step are visible or not, as appropriate.
"""
max_attempts = 3 max_attempts = 3
extended_feedback = False extended_feedback = False
params = { params = {
"max_attempts": max_attempts, "max_attempts": max_attempts,
"extended_feedback": extended_feedback, "extended_feedback": extended_feedback,
"include_messages": include_messages,
} }
step_builder, controls = self.load_assessment_scenario("step_builder_default_messages.xml", params) step_builder, controls = self.load_assessment_scenario("step_builder_conditional_messages.xml", params)
# First attempt: incomplete (second question wrong) # First attempt: incomplete (second question wrong)
...@@ -562,14 +583,16 @@ class StepBuilderTest(MentoringAssessmentBaseTest, MultipleSliderBlocksTestMixin ...@@ -562,14 +583,16 @@ class StepBuilderTest(MentoringAssessmentBaseTest, MultipleSliderBlocksTestMixin
} }
self.peek_at_review(step_builder, controls, expected_results, extended_feedback=extended_feedback) self.peek_at_review(step_builder, controls, expected_results, extended_feedback=extended_feedback)
# Should show default message for incomplete submission # Should show the following message for incomplete submission
self.assert_message_text(step_builder, "Not quite! You can try again, though.") self.assert_review_conditional_messages_equal(
step_builder,
["Not quite! You can try again, though."] if include_messages else []
)
# Try again # Try again
controls.try_again.click() controls.try_again.click()
self.wait_until_hidden(controls.try_again) self.wait_until_hidden(controls.try_again)
self.assert_no_message_text(step_builder)
# Second attempt: complete (both questions correct) # Second attempt: complete (both questions correct)
...@@ -589,14 +612,16 @@ class StepBuilderTest(MentoringAssessmentBaseTest, MultipleSliderBlocksTestMixin ...@@ -589,14 +612,16 @@ class StepBuilderTest(MentoringAssessmentBaseTest, MultipleSliderBlocksTestMixin
} }
self.peek_at_review(step_builder, controls, expected_results, extended_feedback=extended_feedback) self.peek_at_review(step_builder, controls, expected_results, extended_feedback=extended_feedback)
# Should show default message for complete submission # Should show the following message for perfect ("complete") submission
self.assert_message_text(step_builder, "Great job!") self.assert_review_conditional_messages_equal(
step_builder,
["Great job!"] if include_messages else []
)
# Try again # Try again
controls.try_again.click() controls.try_again.click()
self.wait_until_hidden(controls.try_again) self.wait_until_hidden(controls.try_again)
self.assert_no_message_text(step_builder)
# Last attempt: complete (both questions correct) # Last attempt: complete (both questions correct)
...@@ -617,8 +642,11 @@ class StepBuilderTest(MentoringAssessmentBaseTest, MultipleSliderBlocksTestMixin ...@@ -617,8 +642,11 @@ class StepBuilderTest(MentoringAssessmentBaseTest, MultipleSliderBlocksTestMixin
} }
self.peek_at_review(step_builder, controls, expected_results, extended_feedback=extended_feedback) self.peek_at_review(step_builder, controls, expected_results, extended_feedback=extended_feedback)
# Should show default message for review # Should show the following messages:
self.assert_message_text(step_builder, "Note: you have used all attempts. Continue to the next unit.") self.assert_review_conditional_messages_equal(
step_builder,
["Great job!", "Note: you have used all attempts. Continue to the next unit."] if include_messages else []
)
def answer_rating_question(self, step_number, question_number, step_builder, question, choice_name): def answer_rating_question(self, step_number, question_number, step_builder, question, choice_name):
question_text = self.question_text(question_number) question_text = self.question_text(question_number)
......
...@@ -11,6 +11,8 @@ ...@@ -11,6 +11,8 @@
</pb-mcq> </pb-mcq>
</sb-step> </sb-step>
<sb-review-step/> <sb-review-step>
<sb-review-score/>
</sb-review-step>
</step-builder> </step-builder>
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
<pb-tip values='["notwant"]'>Your loss!</pb-tip> <pb-tip values='["notwant"]'>Your loss!</pb-tip>
{% if include_review_tips %} {% if include_review_tips %}
<pb-message type="on-assessment-review-question"> <pb-message type="on-assessment-review-question">
<html>Take another look at <a href="#">Lesson 2</a></html> Take another look at <a href="#">Lesson 2</a>
</pb-message> </pb-message>
{% endif %} {% endif %}
</pb-rating> </pb-rating>
...@@ -52,24 +52,21 @@ ...@@ -52,24 +52,21 @@
<pb-tip values='["bugs"]'>Nah, there aren't any!</pb-tip> <pb-tip values='["bugs"]'>Nah, there aren't any!</pb-tip>
{% if include_review_tips %} {% if include_review_tips %}
<pb-message type="on-assessment-review-question"> <pb-message type="on-assessment-review-question">
<html>Take another look at <a href="#">Lesson 3</a></html> Take another look at <a href="#">Lesson 3</a>
</pb-message> </pb-message>
{% endif %} {% endif %}
</pb-mrq> </pb-mrq>
</sb-step> </sb-step>
<sb-review-step> <sb-review-step>
<pb-message type="completed"> <sb-conditional-message score_condition="imperfect" num_attempts_condition="can_try_again">Not quite! You can try again, though.</sb-conditional-message>
<html>Block completed message text</html> <sb-conditional-message score_condition="imperfect" num_attempts_condition="cannot_try_again">Not quite!</sb-conditional-message>
</pb-message> <sb-conditional-message score_condition="perfect">Great job!</sb-conditional-message>
<sb-conditional-message num_attempts_condition="cannot_try_again">This message is shown when you run out of attempts.</sb-conditional-message>
<pb-message type="incomplete"> <sb-review-score/>
<html>Block incomplete message text</html> {% if include_review_tips %}
</pb-message> <sb-review-per-question-feedback/>
{% endif %}
<pb-message type="on-review">
<html>On review message text</html>
</pb-message>
</sb-review-step> </sb-review-step>
</step-builder> </step-builder>
...@@ -13,6 +13,14 @@ ...@@ -13,6 +13,14 @@
</pb-mcq> </pb-mcq>
</sb-step> </sb-step>
<sb-review-step></sb-review-step> <sb-review-step>
{% if include_messages %}
<sb-conditional-message score_condition="imperfect" num_attempts_condition="can_try_again">Not quite! You can try again, though.</sb-conditional-message>
<sb-conditional-message score_condition="perfect">Great job!</sb-conditional-message>
<sb-conditional-message num_attempts_condition="cannot_try_again">Note: you have used all attempts. Continue to the next unit.</sb-conditional-message>
{% endif %}
<sb-review-score/>
<sb-review-per-question-feedback/>
</sb-review-step>
</step-builder> </step-builder>
...@@ -21,6 +21,8 @@ ...@@ -21,6 +21,8 @@
<html_demo>Bla bla bla</html_demo> <html_demo>Bla bla bla</html_demo>
</sb-step> </sb-step>
<sb-review-step /> <sb-review-step>
<html_demo>This is the review step.</html_demo>
</sb-review-step>
</step-builder> </step-builder>
...@@ -26,6 +26,8 @@ ...@@ -26,6 +26,8 @@
</pb-rating> </pb-rating>
</sb-step> </sb-step>
<sb-review-step /> <sb-review-step>
<sb-review-score/>
</sb-review-step>
</step-builder> </step-builder>
...@@ -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