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)
Problem Builder Usage Using Problem Builder and Step Builder
===================== ======================================
When you add the **Problem Builder** component to a course in the First, enable the blocks in Studio (see "Enabling in Studio", below).
studio, the built-in editing tools guide you through the process of
configuring the block and adding individual questions. Next, decide whether you want to use **Problem Builder** or **Step Builder** to
create your exercise. Select the name of the block below for detailed usage
### Problem Builder modes instructions.
There are 2 mentoring modes available: * [Problem Builder](Problem Builder.md) is simply a group of one or more
question[s].
* **standard**: Traditional mentoring. All questions are displayed on the * [Step Builder](Step Builder.md) lets authors build more complex exercises
page and submitted at the same time. The students get some tips and where questions are grouped into "steps" and students answer the questions in
feedback about their answers. This is the default mode. each step at a time. An optional "review step" can be added to the end of the
exercise, which can summarize the student's results and provide tailored
* **assessment**: Questions are displayed and submitted one by one. The feedback and study suggestions.
students don't get tips or feedback, but only know if their answer was
correct. Assessment mode comes with a default `max_attempts` of `2`. Once you add a Problem Builder or Step Builder component to a course, you can
then click on the "View" link (seen at the top right of the component) to open
**Note that assessment mode is deprecated**: In the future, Problem the component for editing. You can then add [any of the supported question and
Builder will only provide functionality that is currently part of content types](Questions.md).
standard mode. Assessment mode will remain functional for a while to
ensure backward compatibility with courses that are currently using
it. If you want to use assessment functionality for a new course, Enabling in Studio
please use the Step Builder XBlock (described below). ------------------
Below are some LMS screenshots of a Problem Builder block in assessment mode. You can enable the Problem Builder and Step Builder XBlocks in Studio by
modifying the advanced settings for your course:
Question before submitting an answer:
1. From the main page of a specific course, navigate to **Settings** ->
![Assessment Step 1](img/assessment-1.png) **Advanced Settings** from the top menu.
2. Find the **Advanced Module List** setting.
Question after submitting the correct answer: 3. To enable Problem Builder for your course, add `"problem-builder"` to the
modules listed there.
![Assessment Step 2](img/assessment-2.png) 4. To enable Step Builder for your course, add `"step-builder"` to the modules
listed there.
Question after submitting a wrong answer: 5. Click the **Save changes** button.
![Assessment Step 3](img/assessment-3.png) 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.
Score review and the "Try Again" button:
![Assessment Step 4](img/assessment-4.png)
Step Builder Usage
==================
The Step Builder XBlock replaces assessment mode functionality of the
Problem Builder XBlock, while allowing to group questions into explict
steps:
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 also provides a **Review
Step** block which
* allows students to review their performance
* allows students to jump back to individual steps to review their
answers (if **Extended feedback** setting is on and maximum number
of attempts has been reached)
* supports customizable messages that will be shown when
* the block is *complete*, i.e., if all answers that the student
provided are correct
* the block is *incomplete*, i.e., if some answers that the student
provided are incorrect or partially correct
* the student has used up all attempts
Note that only one such block is allowed per instance.
**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:
![Unlimited attempts available](img/review-step-unlimited-attempts-available.png)
Limited attempts, some attempts remaining, some answers incorrect:
![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)
Question Types
==============
### 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".
### Self-assessment 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)
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`).
...@@ -35,15 +35,15 @@ from xblock.fragment import Fragment ...@@ -35,15 +35,15 @@ from xblock.fragment import Fragment
from xblock.validation import ValidationMessage from xblock.validation import ValidationMessage
from .message import MentoringMessageBlock from .message import MentoringMessageBlock
from .mixins import ( from .mixins import (
_normalize_id, QuestionMixin, MessageParentMixin, StepParentMixin, XBlockWithTranslationServiceMixin _normalize_id, QuestionMixin, MessageParentMixin, StepParentMixin, XBlockWithTranslationServiceMixin
) )
from .step_review import ReviewStepBlock
from xblockutils.helpers import child_isinstance from xblockutils.helpers import child_isinstance
from xblockutils.resources import ResourceLoader from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable import ( from xblockutils.studio_editable import (
StudioEditableXBlockMixin, StudioContainerXBlockMixin, StudioContainerWithNestedXBlocksMixin NestedXBlockSpec, StudioEditableXBlockMixin, StudioContainerXBlockMixin, StudioContainerWithNestedXBlocksMixin,
) )
...@@ -175,22 +175,6 @@ class BaseMentoringBlock( ...@@ -175,22 +175,6 @@ class BaseMentoringBlock(
for theme_file in theme_files: for theme_file in theme_files:
fragment.add_css(ResourceLoader(theme_package).load_unicode(theme_file)) fragment.add_css(ResourceLoader(theme_package).load_unicode(theme_file))
def feedback_dispatch(self, target_data, stringify):
if self.show_extended_feedback():
if stringify:
return json.dumps(target_data)
else:
return target_data
def correct_json(self, stringify=True):
return self.feedback_dispatch(self.score.correct, stringify)
def incorrect_json(self, stringify=True):
return self.feedback_dispatch(self.score.incorrect, stringify)
def partial_json(self, stringify=True):
return self.feedback_dispatch(self.score.partially_correct, stringify)
@XBlock.json_handler @XBlock.json_handler
def view(self, data, suffix=''): def view(self, data, suffix=''):
""" """
...@@ -675,6 +659,22 @@ class MentoringBlock(BaseMentoringBlock, StudioContainerXBlockMixin, StepParentM ...@@ -675,6 +659,22 @@ class MentoringBlock(BaseMentoringBlock, StudioContainerXBlockMixin, StepParentM
'num_attempts': self.num_attempts, 'num_attempts': self.num_attempts,
} }
def feedback_dispatch(self, target_data, stringify):
if self.show_extended_feedback():
if stringify:
return json.dumps(target_data)
else:
return target_data
def correct_json(self, stringify=True):
return self.feedback_dispatch(self.score.correct, stringify)
def incorrect_json(self, stringify=True):
return self.feedback_dispatch(self.score.incorrect, stringify)
def partial_json(self, stringify=True):
return self.feedback_dispatch(self.score.partially_correct, stringify)
def handle_assessment_submit(self, submissions, suffix): def handle_assessment_submit(self, submissions, suffix):
completed = False completed = False
current_child = None current_child = None
...@@ -925,10 +925,16 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes ...@@ -925,10 +925,16 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes
@property @property
def has_review_step(self): def has_review_step(self):
from .step import ReviewStepBlock
return any(child_isinstance(self, child_id, ReviewStepBlock) for child_id in self.children) return any(child_isinstance(self, child_id, ReviewStepBlock) for child_id in self.children)
@property @property
def review_step(self):
""" Get the Review Step XBlock child, if any. Otherwise returns None """
for step_id in self.children:
if child_isinstance(self, step_id, ReviewStepBlock):
return self.runtime.get_block(step_id)
@property
def score(self): def score(self):
questions = self.questions questions = self.questions
total_child_weight = sum(float(question.weight) for question in questions) total_child_weight = sum(float(question.weight) for question in questions)
...@@ -956,6 +962,9 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes ...@@ -956,6 +962,9 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes
@property @property
def review_tips(self): def review_tips(self):
""" Get review tips, shown for wrong answers. """ """ Get review tips, shown for wrong answers. """
if self.max_attempts > 0 and self.num_attempts >= self.max_attempts:
# Review tips are only shown if the student is allowed to try again.
return []
review_tips = [] review_tips = []
status_cache = dict() status_cache = dict()
steps = self.steps steps = self.steps
...@@ -967,13 +976,13 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes ...@@ -967,13 +976,13 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes
# The student got this wrong. Check if there is a review tip to show. # The student got this wrong. Check if there is a review tip to show.
tip_html = question.get_review_tip() tip_html = question.get_review_tip()
if tip_html: if tip_html:
if hasattr(self.runtime, 'replace_jump_to_id_urls'): if getattr(self.runtime, 'replace_jump_to_id_urls', None) is not None:
tip_html = self.runtime.replace_jump_to_id_urls(tip_html) tip_html = self.runtime.replace_jump_to_id_urls(tip_html)
review_tips.append(tip_html) review_tips.append(tip_html)
return review_tips return review_tips
def show_extended_feedback(self): def show_extended_feedback(self):
return self.extended_feedback return self.extended_feedback and self.max_attempts_reached
def student_view(self, context): def student_view(self, context):
fragment = Fragment() fragment = Fragment()
...@@ -981,11 +990,12 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes ...@@ -981,11 +990,12 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes
context = context or {} context = context or {}
context['hide_prev_answer'] = True # For Step Builder, we don't show the users' old answers when they try again context['hide_prev_answer'] = True # For Step Builder, we don't show the users' old answers when they try again
context['score_summary'] = self.get_score_summary()
for child_id in self.children: for child_id in self.children:
child = self.runtime.get_block(child_id) child = self.runtime.get_block(child_id)
if child is None: # child should not be None but it can happen due to bugs or permission issues if child is None: # child should not be None but it can happen due to bugs or permission issues
child_content = u"<p>[{}]</p>".format(self._(u"Error: Unable to load child component.")) child_content = u"<p>[{}]</p>".format(self._(u"Error: Unable to load child component."))
elif not isinstance(child, MentoringMessageBlock): else:
child_fragment = self._render_child_fragment(child, context, view='mentoring_view') child_fragment = self._render_child_fragment(child, context, view='mentoring_view')
fragment.add_frag_resources(child_fragment) fragment.add_frag_resources(child_fragment)
child_content = child_fragment.content child_content = child_fragment.content
...@@ -1002,11 +1012,12 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes ...@@ -1002,11 +1012,12 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/mentoring_with_steps.js')) fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/mentoring_with_steps.js'))
fragment.add_resource(loader.load_unicode('templates/html/mentoring_attempts.html'), "text/html") fragment.add_resource(loader.load_unicode('templates/html/mentoring_attempts.html'), "text/html")
fragment.add_resource(loader.load_unicode('templates/html/mentoring_review_templates.html'), "text/html")
self.include_theme_files(fragment) self.include_theme_files(fragment)
fragment.initialize_js('MentoringWithStepsBlock') fragment.initialize_js('MentoringWithStepsBlock', {
'show_extended_feedback': self.show_extended_feedback(),
})
return fragment return fragment
...@@ -1021,10 +1032,11 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes ...@@ -1021,10 +1032,11 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes
NestedXBlockSpec allows explicitly setting disabled/enabled state, disabled reason (if any) and single/multiple NestedXBlockSpec allows explicitly setting disabled/enabled state, disabled reason (if any) and single/multiple
instances instances
""" """
from .step import MentoringStepBlock, ReviewStepBlock # Import here to avoid circular dependency # Import here to avoid circular dependency
from .step import MentoringStepBlock
return [ return [
MentoringStepBlock, MentoringStepBlock,
ReviewStepBlock, NestedXBlockSpec(ReviewStepBlock, single_instance=True),
] ]
@XBlock.json_handler @XBlock.json_handler
...@@ -1048,11 +1060,14 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes ...@@ -1048,11 +1060,14 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes
self.active_step = new_value self.active_step = new_value
elif new_value == len(self.step_ids): elif new_value == len(self.step_ids):
# The user just completed the final step. # The user just completed the final step.
# Update the number of attempts:
self.num_attempts += 1
# Do we need to render a review (summary of the user's score):
if self.has_review_step: if self.has_review_step:
self.active_step = -1 self.active_step = -1
# Update the number of attempts, if necessary: response_data['review_html'] = self.runtime.render(self.review_step, "mentoring_view", {
if self.num_attempts < self.max_attempts: 'score_summary': self.get_score_summary(),
self.num_attempts += 1 }).content
response_data['num_attempts'] = self.num_attempts response_data['num_attempts'] = self.num_attempts
# And publish the score: # And publish the score:
score = self.score score = self.score
...@@ -1061,24 +1076,26 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes ...@@ -1061,24 +1076,26 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes
'max_value': self.max_score(), 'max_value': self.max_score(),
} }
self.runtime.publish(self, 'grade', grade_data) self.runtime.publish(self, 'grade', grade_data)
response_data['grade_data'] = self.get_grade()
response_data['active_step'] = self.active_step response_data['active_step'] = self.active_step
return response_data return response_data
def get_grade(self, data=None, suffix=None): def get_score_summary(self):
if self.num_attempts == 0:
return {}
score = self.score score = self.score
return { return {
'score': score.percentage, 'score': score.percentage,
'correct_answers': len(score.correct), 'correct_answers': len(score.correct),
'incorrect_answers': len(score.incorrect), 'incorrect_answers': len(score.incorrect),
'partially_correct_answers': len(score.partially_correct), 'partially_correct_answers': len(score.partially_correct),
'correct': self.correct_json(stringify=False), 'correct': score.correct,
'incorrect': self.incorrect_json(stringify=False), 'incorrect': score.incorrect,
'partial': self.partial_json(stringify=False), 'partial': score.partially_correct,
'complete': self.complete, 'complete': self.complete,
'max_attempts_reached': self.max_attempts_reached, 'max_attempts_reached': self.max_attempts_reached,
'assessment_review_tips': self.review_tips, 'show_extended_review': self.show_extended_feedback(),
'review_tips': self.review_tips,
} }
@XBlock.json_handler @XBlock.json_handler
...@@ -1101,9 +1118,7 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes ...@@ -1101,9 +1118,7 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes
} }
def author_preview_view(self, context): def author_preview_view(self, context):
context = context.copy() if context else {} return self.student_view(context)
context['author_preview_view'] = True
return super(MentoringWithExplicitStepsBlock, self).author_preview_view(context)
def author_edit_view(self, context): def author_edit_view(self, context):
""" """
...@@ -1121,6 +1136,6 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes ...@@ -1121,6 +1136,6 @@ class MentoringWithExplicitStepsBlock(BaseMentoringBlock, StudioContainerWithNes
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder-edit.css')) fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder-edit.css'))
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder-tinymce-content.css')) fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder-tinymce-content.css'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/util.js')) fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/util.js'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/mentoring_with_steps_edit.js')) fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/container_edit.js'))
fragment.initialize_js('MentoringWithStepsEdit') fragment.initialize_js('ProblemBuilderContainerEdit')
return fragment return fragment
...@@ -100,18 +100,6 @@ class MentoringMessageBlock(XBlock, StudioEditableXBlockMixin, XBlockWithTransla ...@@ -100,18 +100,6 @@ class MentoringMessageBlock(XBlock, StudioEditableXBlockMixin, XBlockWithTransla
"used up all of their allowed attempts." "used up all of their allowed attempts."
), ),
}, },
"on-review": {
"display_name": _(u"Message shown when no attempts left"),
"long_display_name": _(u"Message shown during review when no attempts remain"),
"default": _(
u"Note: you have used all attempts. Continue to the next unit."
),
"description": _(
u"This message will be shown when the student is reviewing their answers to the assessment, "
"if the student has used up all of their allowed attempts. "
"It is not shown if the student is allowed to try again."
),
},
} }
content = String( content = String(
...@@ -203,8 +191,3 @@ class CompletedMentoringMessageShim(object): ...@@ -203,8 +191,3 @@ class CompletedMentoringMessageShim(object):
class IncompleteMentoringMessageShim(object): class IncompleteMentoringMessageShim(object):
CATEGORY = 'pb-message' CATEGORY = 'pb-message'
STUDIO_LABEL = _("Message (Incomplete)") STUDIO_LABEL = _("Message (Incomplete)")
class OnReviewMentoringMessageShim(object):
CATEGORY = 'pb-message'
STUDIO_LABEL = _("Message (Review)")
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
# -*- 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
from xblock.fragment import Fragment
from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable import (
NestedXBlockSpec, StudioEditableXBlockMixin, StudioContainerWithNestedXBlocksMixin, XBlockWithPreviewMixin
)
from .mixins import XBlockWithTranslationServiceMixin, NoSettingsMixin
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 = "perfect", "imperfect", "any"
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 = String(
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 = "can_try_again", "cannot_try_again", "any"
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 = String(
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_answers'] == 0 and score_summary['partially_correct_answers'] == 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)
embedded_student_view = student_view
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="conditional-message-help"><p>{}</p></div>'.format(desc) + fragment.content
return fragment
@XBlock.needs("i18n")
class ScoreSummaryBlock(XBlockWithTranslationServiceMixin, XBlockWithPreviewMixin, NoSettingsMixin, XBlock):
"""
Summarize 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. """
context = context or {}
html = loader.render_template("templates/html/sb-review-score.html", context.get("score_summary", {}))
return Fragment(html)
embedded_student_view = student_view
def author_view(self, context=None):
context = context or {}
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,
'show_extended_review': False,
'is_example': True,
}
return self.student_view(context)
@XBlock.needs("i18n")
class PerQuestionFeedbackBlock(XBlockWithTranslationServiceMixin, XBlockWithPreviewMixin, NoSettingsMixin, XBlock):
"""
Display any on-assessment-review-question messages.
These messages are defined within individual questions and are only displayed if the student
got that particular question wrong.
"""
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)
embedded_student_view = student_view
def author_view(self, context=None):
""" Show example content in Studio """
context = context or {}
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,
NoSettingsMixin,
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
# Render children as "embedded_student_view" rather than "student_view" so
# that Studio doesn't wrap with with unwanted controls and the XBlock SDK
# workbench doesn't add the acid-aside to the fragment.
child_fragment = self._render_child_fragment(child, context, view="embedded_student_view")
fragment.add_frag_resources(child_fragment)
fragment.add_content(child_fragment.content)
return fragment
mentoring_view = student_view
def author_edit_view(self, context):
"""
Add some HTML to the author view that allows authors to add child blocks.
"""
context['wrap_children'] = {
'head': u'<div class="mentoring">',
'tail': u'</div>'
}
fragment = super(ReviewStepBlock, self).author_edit_view(context)
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder.css'))
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/problem-builder-edit.css'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/util.js'))
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/container_edit.js'))
fragment.initialize_js('ProblemBuilderContainerEdit')
return fragment
<!-- Tips about specific questions the student got wrong. From pb-message[type=on-assessment-review-question] blocks -->
<script type="text/template" id="xblock-review-tips-template">
<p class="review-tips-intro"><%= gettext("You might consider reviewing the following items before your next assessment attempt:") %></p>
<ul class="review-tips-list">
<% for (var tip_idx in tips) {{ %>
<li><%= tips[tip_idx] %></li>
<% }} %>
</ul>
</script>
...@@ -13,20 +13,8 @@ ...@@ -13,20 +13,8 @@
{{ child_content|safe }} {{ child_content|safe }}
{% endfor %} {% endfor %}
<div class="grade"
data-score="{{ self.score.percentage }}"
data-correct_answer="{{ self.score.correct|length }}"
data-incorrect_answer="{{ self.score.incorrect|length }}"
data-partially_correct_answer="{{ self.score.partially_correct|length }}"
data-assessment_review_tips="{{ self.review_tips_json }}"
data-extended_feedback="{{ self.extended_feedback }}"
data-correct="{{ self.correct_json }}"
data-incorrect="{{ self.incorrect_json }}"
data-partial="{{ self.partial_json }}">
</div>
<div class="submit"> <div class="submit">
<span class="assessment-checkmark fa icon-2x"></span> <span class="step-overall-checkmark fa icon-2x"></span>
<input type="button" class="input-main" value="Submit" disabled="disabled" /> <input type="button" class="input-main" value="Submit" disabled="disabled" />
<input type="button" class="input-next" value="Next Step" disabled="disabled" /> <input type="button" class="input-next" value="Next Step" disabled="disabled" />
<input type="button" class="input-review" value="Review grade" disabled="disabled" /> <input type="button" class="input-review" value="Review grade" disabled="disabled" />
...@@ -35,11 +23,7 @@ ...@@ -35,11 +23,7 @@
<div class="attempts" <div class="attempts"
data-max_attempts="{{ self.max_attempts }}" data-num_attempts="{{ self.num_attempts }}"> data-max_attempts="{{ self.max_attempts }}" data-num_attempts="{{ self.num_attempts }}">
</div> </div>
</div> </div>
<div class="assessment-review-tips"></div>
</div> </div>
<div class="review-link"><a href="#">Review final grade</a></div> <div class="review-link"><a href="#">Review final grade</a></div>
......
<div class="sb-review-step">
<div class="assessment-message" data-assessment_message="{{ self.assessment_message }}"></div>
<script type="text/template" id="xblock-feedback-template">
<div class="grade-result">
<h2>
<%= _.template(gettext("You scored {percent}% on this assessment."), {percent: score}, {interpolate: /\{(.+?)\}/g}) %>
</h2>
<hr/>
<span class="assessment-checkmark icon-2x checkmark-correct icon-ok fa fa-check"></span>
<div class="results-section">
<p>
<%= _.template(
ngettext(
"You answered 1 question correctly.",
"You answered {number_correct} questions correctly.",
correct_answer
), {number_correct: correct_answer}, {interpolate: /\{(.+?)\}/g})
%>
</p>
<%= runDetails('correct') %>
</div>
<div class="clear"></div>
<span class="assessment-checkmark icon-2x checkmark-partially-correct icon-ok fa fa-check"></span>
<div class="results-section">
<p>
<%= _.template(
ngettext(
"You answered 1 question partially correctly.",
"You answered {number_partially_correct} questions partially correctly.",
partially_correct_answer
), {number_partially_correct: partially_correct_answer}, {interpolate: /\{(.+?)\}/g})
%>
</p>
<%= runDetails('partial') %>
</div>
<div class="clear"></div>
<span class="assessment-checkmark icon-2x checkmark-incorrect icon-exclamation fa fa-exclamation"></span>
<div class="results-section">
<p>
<%= _.template(
ngettext(
"You answered 1 question incorrectly.",
"You answered {number_incorrect} questions incorrectly.",
incorrect_answer
), {number_incorrect: incorrect_answer}, {interpolate: /\{(.+?)\}/g})
%>
</p>
<%= runDetails('incorrect') %>
</div>
<div class="clear"></div>
<hr/>
</div>
</script>
<!-- Template for extended feedback: Show extended feedback details when all attempts are used up. -->
<script type="text/template" id="xblock-step-links-template">
<ul class="review-list <%= correctness %>-list">
<% for (var question in questions) { %>
<%
var q = questions[question];
var last_question = question == questions.length - 1;
var second_last_question = question == questions.length - 2;
%>
<li>
<a href="#" class="step-link" data-step="<%= q.step %>"><%=
_.template(gettext("Question {number}"), {number: q.number}, {interpolate: /\{(.+?)\}/g})
%></a><% if (!last_question) { %><%= (questions.length > 2 ? ", " : "") %><%= (second_last_question ? " " + gettext("and"): "") %><% } %>
</li>
<% } %>
</ul>
</script>
</div>
{% load i18n %}
{# Tips about specific questions the student got wrong. From pb-message[type=on-assessment-review-question] blocks #}
<p class="review-tips-intro">{% trans "You might consider reviewing the following items before your next assessment attempt:" %}</p>
<ul class="review-tips-list">
{% for tip in tips %}
<li>{{tip|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