Commit dde3c4ce by Tim Krones

Merge pull request #65 from open-craft/review-step

Step Builder: Review step and assessment functionality
parents be0c46aa 1ff860a1
Problem Builder XBlock
----------------------
Problem Builder and Step Builder
--------------------------------
[![Build Status](https://travis-ci.org/open-craft/problem-builder.svg?branch=master)](https://travis-ci.org/open-craft/problem-builder)
This XBlock allows creation of questions of various types and simulating the
workflow of real-life mentoring, within an edX course.
This repository provides two XBlocks: Problem Builder and Step Builder.
It supports:
Both blocks allow to create questions of various types. They can be
used to simulate the workflow of real-life mentoring, within an edX
course.
Supported features include:
* **Free-form answers** (textarea) which can be shared accross
different XBlock instances (for example, to allow a student to
review and edit an answer he gave before).
* **Self-assessment MCQs** (multiple choice), to display predetermined
feedback to a student based on his choices in the
review and edit an answer they gave before).
* **Self-assessment MCQs** (multiple choice questions), to display
predetermined feedback to a student based on his choices in the
self-assessment. Supports rating scales and arbitrary answers.
* **MRQs (Multiple Response Questions)**, a type of multiple choice
question that allows the student to choose more than one choice.
question that allows the student to select more than one choice.
* **Answer recaps** that display a read-only summary of a user's
answer to a free-form question asked earlier in the course.
* **Progression tracking**, to require that the student has
......@@ -26,15 +29,15 @@ It supports:
* **Dashboards**, for displaying a summary of the student's answers
to multiple choice questions. [Details](doc/Dashboard.md)
The screenshot shows an example of a problem builder block containing a
free-form question, two MCQs and one MRQ.
The following screenshot shows an example of a Problem Builder block
containing a free-form question, two MCQs and one MRQ:
![Problem Builder Example](doc/img/mentoring-example.png)
Installation
------------
Install the requirements into the python virtual environment of your
Install the requirements into the Python virtual environment of your
`edx-platform` installation by running the following command from the
root folder:
......@@ -45,14 +48,20 @@ $ pip install -r requirements.txt
Enabling in Studio
------------------
You can enable the Problem Builder XBlock in studio through the advanced
settings.
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.
1. From the main page of a specific course, navigate to `Settings ->
Advanced Settings` from the top menu.
2. Check for the `advanced_modules` policy key, and add `"problem-builder"`
to the policy value list.
3. 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
-----
......
from .mentoring import MentoringBlock, MentoringWithExplicitStepsBlock
from .step import MentoringStepBlock
from .step import MentoringStepBlock, ReviewStepBlock
from .answer import AnswerBlock, AnswerRecapBlock
from .choice import ChoiceBlock
from .dashboard import DashboardBlock
......
......@@ -79,7 +79,7 @@ class StepParentMixin(object):
"""
@lazy
def steps(self):
def step_ids(self):
"""
Get the usage_ids of all of this XBlock's children that are "Steps"
"""
......@@ -87,11 +87,10 @@ class StepParentMixin(object):
_normalize_id(child_id) for child_id in self.children if child_isinstance(self, child_id, QuestionMixin)
]
def get_steps(self):
@lazy
def steps(self):
""" Get the step children of this block, cached if possible. """
if getattr(self, "_steps_cache", None) is None:
self._steps_cache = [self.runtime.get_block(child_id) for child_id in self.steps]
return self._steps_cache
return [self.runtime.get_block(child_id) for child_id in self.step_ids]
class QuestionMixin(EnumerableChildMixin):
......@@ -114,7 +113,7 @@ class QuestionMixin(EnumerableChildMixin):
@lazy
def siblings(self):
return self.get_parent().steps
return self.get_parent().step_ids
def author_view(self, context):
context = context.copy() if context else {}
......
/* Display of url_name below content */
.xblock[data-block-type=pb-mentoring-step] .url-name-footer,
.xblock[data-block-type=pb-mentoring] .url-name-footer,
.xblock[data-block-type=sb-step] .url-name-footer,
.xblock[data-block-type=step-builder] .url-name-footer,
.xblock[data-block-type=problem-builder] .url-name-footer,
.xblock[data-block-type=mentoring] .url-name-footer {
font-style: italic;
}
.xblock[data-block-type=pb-mentoring-step] .url-name-footer .url-name,
.xblock[data-block-type=pb-mentoring] .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=problem-builder] .url-name-footer .url-name,
.xblock[data-block-type=mentoring] .url-name-footer .url-name {
margin: 0 10px;
......@@ -15,8 +15,8 @@
}
/* Custom appearance for our "Add" buttons */
.xblock[data-block-type=pb-mentoring-step] .add-xblock-component .new-component .new-component-type .add-xblock-component-button,
.xblock[data-block-type=pb-mentoring] .add-xblock-component .new-component .new-component-type .add-xblock-component-button,
.xblock[data-block-type=sb-step] .add-xblock-component .new-component .new-component-type .add-xblock-component-button,
.xblock[data-block-type=step-builder] .add-xblock-component .new-component .new-component-type .add-xblock-component-button,
.xblock[data-block-type=problem-builder] .add-xblock-component .new-component .new-component-type .add-xblock-component-button,
.xblock[data-block-type=mentoring] .add-xblock-component .new-component .new-component-type .add-xblock-component-button {
width: 200px;
......@@ -24,10 +24,10 @@
line-height: 30px;
}
.xblock[data-block-type=pb-mentoring-step] .add-xblock-component .new-component .new-component-type .add-xblock-component-button.disabled,
.xblock[data-block-type=pb-mentoring-step] .add-xblock-component .new-component .new-component-type .add-xblock-component-button.disabled:hover,
.xblock[data-block-type=pb-mentoring] .add-xblock-component .new-component .new-component-type .add-xblock-component-button.disabled,
.xblock[data-block-type=pb-mentoring] .add-xblock-component .new-component .new-component-type .add-xblock-component-button.disabled:hover,
.xblock[data-block-type=sb-step] .add-xblock-component .new-component .new-component-type .add-xblock-component-button.disabled,
.xblock[data-block-type=sb-step] .add-xblock-component .new-component .new-component-type .add-xblock-component-button.disabled:hover,
.xblock[data-block-type=step-builder] .add-xblock-component .new-component .new-component-type .add-xblock-component-button.disabled,
.xblock[data-block-type=step-builder] .add-xblock-component .new-component .new-component-type .add-xblock-component-button.disabled:hover,
.xblock[data-block-type=problem-builder] .add-xblock-component .new-component .new-component-type .add-xblock-component-button.disabled,
.xblock[data-block-type=problem-builder] .add-xblock-component .new-component .new-component-type .add-xblock-component-button.disabled:hover,
.xblock[data-block-type=mentoring] .add-xblock-component .new-component .new-component-type .add-xblock-component-button.disabled,
......@@ -37,7 +37,7 @@
cursor: default;
}
.xblock[data-block-type=pb-mentoring] .submission-message-help p,
.xblock[data-block-type=step-builder] .submission-message-help p,
.xblock[data-block-type=problem-builder] .submission-message-help p {
border-top: 1px solid #ddd;
font-size: 0.85em;
......
function MentoringWithStepsEdit(runtime, element) {
"use strict";
// Disable "add" buttons when a message of that type already exists:
var $buttons = $('.add-xblock-component-button[data-category=pb-message]', element);
var updateButtons = function() {
$buttons.each(function() {
var msg_type = $(this).data('boilerplate');
$(this).toggleClass('disabled', $('.xblock .submission-message.'+msg_type).length > 0);
});
var blockIsPresent = function(klass) {
return $('.xblock ' + klass).length > 0;
};
updateButtons();
$buttons.click(function(ev) {
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 initButtons = function(dataCategory) {
var $buttons = $('.add-xblock-component-button[data-category='+dataCategory+']', element);
$buttons.each(function() {
if (dataCategory === 'pb-message') {
var msg_type = $(this).data('boilerplate');
updateButton($(this), blockIsPresent('.submission-message.'+msg_type));
} else {
updateButton($(this), blockIsPresent('.xblock-header-sb-review-step'));
}
});
$buttons.on('click', disableButton);
};
initButtons('pb-message');
initButtons('sb-review-step');
ProblemBuilderUtil.transformClarifications(element);
StudioEditableXBlockMixin(runtime, element);
......
function ReviewStepBlock(runtime, element) {
var gradeTemplate = _.template($('#xblock-feedback-template').html());
var reviewStepsTemplate = _.template($('#xblock-step-links-template').html());
return {
'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));
}
};
}
function MentoringStepBlock(runtime, element) {
var children = runtime.children(element);
var submitXHR;
var submitXHR, resultsXHR;
function callIfExists(obj, fn) {
if (typeof obj !== 'undefined' && typeof obj[fn] == 'function') {
......@@ -34,13 +34,13 @@ function MentoringStepBlock(runtime, element) {
return is_valid;
},
submit: function(result_handler) {
submit: function(resultHandler) {
var handler_name = 'submit';
var data = {};
for (var i = 0; i < children.length; i++) {
var child = children[i];
if (child && child.name !== undefined && typeof(child[handler_name]) !== "undefined") {
data[child.name.toString()] = child[handler_name]();
if (child && child.name !== undefined) {
data[child.name.toString()] = callIfExists(child, handler_name);
}
}
var handlerUrl = runtime.handlerUrl(element, handler_name);
......@@ -49,8 +49,38 @@ function MentoringStepBlock(runtime, element) {
}
submitXHR = $.post(handlerUrl, JSON.stringify(data))
.success(function(response) {
result_handler(response);
resultHandler(response);
});
},
getResults: function(resultHandler) {
var handler_name = 'get_results';
var data = [];
for (var i = 0; i < children.length; i++) {
var child = children[i];
if (child && child.name !== undefined) { // Check if we are dealing with a question
data[i] = child.name;
}
}
var handlerUrl = runtime.handlerUrl(element, handler_name);
if (resultsXHR) {
resultsXHR.abort();
}
resultsXHR = $.post(handlerUrl, JSON.stringify(data))
.success(function(response) {
resultHandler(response);
});
},
handleReview: function(results, options) {
for (var i = 0; i < children.length; i++) {
var child = children[i];
if (child && child.name !== undefined) { // Check if we are dealing with a question
var result = results[child.name];
callIfExists(child, 'handleSubmit', result, options);
callIfExists(child, 'handleReview', result);
}
}
}
};
......
......@@ -79,7 +79,7 @@ class MentoringStepBlock(
"""
CAPTION = _(u"Step")
STUDIO_LABEL = _(u"Mentoring Step")
CATEGORY = 'pb-mentoring-step'
CATEGORY = 'sb-step'
# Settings
display_name = String(
......@@ -100,7 +100,12 @@ class MentoringStepBlock(
@lazy
def siblings(self):
return self.get_parent().steps
return self.get_parent().step_ids
@property
def is_last_step(self):
parent = self.get_parent()
return self.step_number == len(parent.step_ids)
@property
def allowed_nested_blocks(self):
......@@ -125,7 +130,7 @@ class MentoringStepBlock(
# Submit child blocks (questions) and gather results
submit_results = []
for child in self.get_steps():
for child in self.steps:
if child.name and child.name in submissions:
submission = submissions[child.name]
child_result = child.submit(submission)
......@@ -137,24 +142,41 @@ class MentoringStepBlock(
for result in submit_results:
self.student_results.append(result)
# Compute "answer status" for this step
if all(result[1]['status'] == 'correct' for result in submit_results):
completed = Correctness.CORRECT
elif all(result[1]['status'] == 'incorrect' for result in submit_results):
completed = Correctness.INCORRECT
else:
completed = Correctness.PARTIAL
return {
'message': 'Success!',
'completed': completed,
'step_status': self.answer_status,
'results': submit_results,
}
@XBlock.json_handler
def get_results(self, queries, suffix=''):
results = {}
answers = dict(self.student_results)
for question in self.steps:
previous_results = answers[question.name]
result = question.get_results(previous_results)
results[question.name] = result
# Add 'message' to results? Looks like it's not used on the client ...
return {
'results': results,
'step_status': self.answer_status,
}
def reset(self):
while self.student_results:
self.student_results.pop()
@property
def answer_status(self):
if all(result[1]['status'] == 'correct' for result in self.student_results):
answer_status = Correctness.CORRECT
elif all(result[1]['status'] == 'incorrect' for result in self.student_results):
answer_status = Correctness.INCORRECT
else:
answer_status = Correctness.PARTIAL
return answer_status
def author_edit_view(self, context):
"""
Add some HTML to the author view that allows authors to add child blocks.
......@@ -207,3 +229,34 @@ class MentoringStepBlock(
fragment.initialize_js('MentoringStepBlock')
return fragment
class ReviewStepBlock(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"
)
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
<!-- 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>
......@@ -9,16 +9,42 @@
<div class="assessment-question-block">
<div class="assessment-message"></div>
{% for child_content in children_contents %}
{{ child_content|safe }}
{% endfor %}
<div class="grade"
data-assessment_message="{{ self.assessment_message }}"
data-score="{{ self.score.percentage }}"
data-correct_answer="{{ self.score.correct|length }}"
data-incorrect_answer="{{ self.score.incorrect|length }}"
data-partially_correct_answer="{{ self.score.partially_correct|length }}"
data-assessment_review_tips="{{ self.review_tips_json }}"
data-extended_feedback="{{ self.extended_feedback }}"
data-correct="{{ self.correct_json }}"
data-incorrect="{{ self.incorrect_json }}"
data-partial="{{ self.partial_json }}">
</div>
<div class="submit">
<span class="assessment-checkmark fa icon-2x"></span>
<input type="button" class="input-main" value="Submit" disabled="disabled" />
<input type="button" class="input-next" value="Next Step" disabled="disabled" />
<input type="button" class="input-review" value="Review grade" disabled="disabled" />
<input type="button" class="input-try-again" value="Try again" disabled="disabled" />
<div class="attempts"
data-max_attempts="{{ self.max_attempts }}" data-num_attempts="{{ self.num_attempts }}">
</div>
</div>
<div class="assessment-review-tips"></div>
</div>
<div class="review-link"><a href="#">Review final grade</a></div>
</div>
<div class="sb-review-step">
<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">
<% var q, last_question; %>
<ul class="review-list <%= correctness %>-list">
<% for (var question in questions) {{ q = questions[question]; last_question = question == questions.length - 1; %>
<li><a href="#" class="step-link" data-step="<%= q.step %>"><%= _.template(gettext("Question {number}"), {number: q.number}, {interpolate: /\{(.+?)\}/g}) %></a></li>
<% }} %>
</ul>
</script>
</div>
<div class="pb-step">
<div class="sb-step">
{% if show_title %}
<div class="title">
<h3>
......
......@@ -30,6 +30,8 @@ MentoringBlock.url_name = String()
loader = ResourceLoader(__name__)
CORRECT, INCORRECT, PARTIAL = "correct", "incorrect", "partially-correct"
class PopupCheckMixin(object):
"""
......@@ -133,6 +135,88 @@ class MentoringAssessmentBaseTest(ProblemBuilderBaseTest):
return mentoring, controls
def assert_hidden(self, elem):
self.assertFalse(elem.is_displayed())
def assert_disabled(self, elem):
self.assertTrue(elem.is_displayed())
self.assertFalse(elem.is_enabled())
def assert_clickable(self, elem):
self.assertTrue(elem.is_displayed())
self.assertTrue(elem.is_enabled())
def ending_controls(self, controls, last):
if last:
self.assert_hidden(controls.next_question)
self.assert_disabled(controls.review)
else:
self.assert_disabled(controls.next_question)
self.assert_hidden(controls.review)
def selected_controls(self, controls, last):
self.assert_clickable(controls.submit)
self.ending_controls(controls, last)
def assert_message_text(self, mentoring, text):
message_wrapper = mentoring.find_element_by_css_selector('.assessment-message')
self.assertEqual(message_wrapper.text, text)
self.assertTrue(message_wrapper.is_displayed())
def assert_no_message_text(self, mentoring):
message_wrapper = mentoring.find_element_by_css_selector('.assessment-message')
self.assertEqual(message_wrapper.text, '')
def check_question_feedback(self, step_builder, question):
question_checkmark = step_builder.find_element_by_css_selector('.assessment-checkmark')
question_feedback = question.find_element_by_css_selector(".feedback")
self.assertTrue(question_feedback.is_displayed())
self.assertEqual(question_feedback.text, "Question Feedback Message")
question.click()
self.assertFalse(question_feedback.is_displayed())
question_checkmark.click()
self.assertTrue(question_feedback.is_displayed())
def do_submit_wait(self, controls, last):
if last:
self.wait_until_clickable(controls.review)
else:
self.wait_until_clickable(controls.next_question)
def do_post(self, controls, last):
if last:
controls.review.click()
else:
controls.next_question.click()
def multiple_response_question(self, number, mentoring, controls, choice_names, result, last=False):
question = self.peek_at_multiple_response_question(number, mentoring, controls, last=last)
choices = GetChoices(question)
expected_choices = {
"Its elegance": False,
"Its beauty": False,
"Its gracefulness": False,
"Its bugs": False,
}
self.assertEquals(choices.state, expected_choices)
for name in choice_names:
choices.select(name)
expected_choices[name] = True
self.assertEquals(choices.state, expected_choices)
self.selected_controls(controls, last)
controls.submit.click()
self.do_submit_wait(controls, last)
self._assert_checkmark(mentoring, result)
controls.review.click()
def expect_question_visible(self, number, mentoring, question_text=None):
if not question_text:
question_text = self.question_text(number)
......@@ -163,6 +247,14 @@ class MentoringAssessmentBaseTest(ProblemBuilderBaseTest):
self.wait_until_clickable(controls.next_question)
controls.next_question.click()
def _assert_checkmark(self, mentoring, result):
"""Assert that only the desired checkmark is present."""
states = {CORRECT: 0, INCORRECT: 0, PARTIAL: 0}
states[result] += 1
for name, count in states.items():
self.assertEqual(len(mentoring.find_elements_by_css_selector(".checkmark-{}".format(name))), count)
class GetChoices(object):
""" Helper class for interacting with MCQ options """
......
......@@ -18,9 +18,7 @@
# "AGPLv3". If not, see <http://www.gnu.org/licenses/>.
#
from ddt import ddt, unpack, data
from .base_test import MentoringAssessmentBaseTest, GetChoices
CORRECT, INCORRECT, PARTIAL = "correct", "incorrect", "partially-correct"
from .base_test import CORRECT, INCORRECT, PARTIAL, MentoringAssessmentBaseTest, GetChoices
@ddt
......@@ -47,29 +45,10 @@ class MentoringAssessmentTest(MentoringAssessmentBaseTest):
controls.click()
title.click()
def assert_hidden(self, elem):
self.assertFalse(elem.is_displayed())
def assert_disabled(self, elem):
self.assertTrue(elem.is_displayed())
self.assertFalse(elem.is_enabled())
def assert_clickable(self, elem):
self.assertTrue(elem.is_displayed())
self.assertTrue(elem.is_enabled())
def assert_persistent_elements_present(self, mentoring):
self.assertIn("A Simple Assessment", mentoring.text)
self.assertIn("This paragraph is shared between all questions.", mentoring.text)
def _assert_checkmark(self, mentoring, result):
"""Assert that only the desired checkmark is present."""
states = {CORRECT: 0, INCORRECT: 0, PARTIAL: 0}
states[result] += 1
for name, count in states.items():
self.assertEqual(len(mentoring.find_elements_by_css_selector(".checkmark-{}".format(name))), count)
def go_to_workbench_main_page(self):
self.browser.get(self.live_server_url)
......@@ -104,35 +83,6 @@ class MentoringAssessmentTest(MentoringAssessmentBaseTest):
self._assert_checkmark(mentoring, result)
self.do_post(controls, last)
def ending_controls(self, controls, last):
if last:
self.assert_hidden(controls.next_question)
self.assert_disabled(controls.review)
else:
self.assert_disabled(controls.next_question)
self.assert_hidden(controls.review)
def selected_controls(self, controls, last):
self.assert_clickable(controls.submit)
if last:
self.assert_hidden(controls.next_question)
self.assert_disabled(controls.review)
else:
self.assert_disabled(controls.next_question)
self.assert_hidden(controls.review)
def do_submit_wait(self, controls, last):
if last:
self.wait_until_clickable(controls.review)
else:
self.wait_until_clickable(controls.next_question)
def do_post(self, controls, last):
if last:
controls.review.click()
else:
controls.next_question.click()
def single_choice_question(self, number, mentoring, controls, choice_name, result, last=False):
question = self.expect_question_visible(number, mentoring)
......@@ -213,44 +163,6 @@ class MentoringAssessmentTest(MentoringAssessmentBaseTest):
return question
def check_question_feedback(self, mentoring, question):
question_checkmark = mentoring.find_element_by_css_selector('.assessment-checkmark')
question_feedback = question.find_element_by_css_selector(".feedback")
self.assertTrue(question_feedback.is_displayed())
self.assertEqual(question_feedback.text, "Question Feedback Message")
question.click()
self.assertFalse(question_feedback.is_displayed())
question_checkmark.click()
self.assertTrue(question_feedback.is_displayed())
def multiple_response_question(self, number, mentoring, controls, choice_names, result, last=False):
question = self.peek_at_multiple_response_question(number, mentoring, controls, last=last)
choices = GetChoices(question)
expected_choices = {
"Its elegance": False,
"Its beauty": False,
"Its gracefulness": False,
"Its bugs": False,
}
self.assertEquals(choices.state, expected_choices)
for name in choice_names:
choices.select(name)
expected_choices[name] = True
self.assertEquals(choices.state, expected_choices)
self.selected_controls(controls, last)
controls.submit.click()
self.do_submit_wait(controls, last)
self._assert_checkmark(mentoring, result)
controls.review.click()
def peek_at_review(self, mentoring, controls, expected, extended_feedback=False):
self.wait_until_text_in("You scored {percentage}% on this assessment.".format(**expected), mentoring)
self.assert_persistent_elements_present(mentoring)
......@@ -288,15 +200,6 @@ class MentoringAssessmentTest(MentoringAssessmentBaseTest):
self.assert_hidden(controls.review)
self.assert_hidden(controls.review_link)
def assert_message_text(self, mentoring, text):
message_wrapper = mentoring.find_element_by_css_selector('.assessment-message')
self.assertEqual(message_wrapper.text, text)
self.assertTrue(message_wrapper.is_displayed())
def assert_no_message_text(self, mentoring):
message_wrapper = mentoring.find_element_by_css_selector('.assessment-message')
self.assertEqual(message_wrapper.text, '')
def extended_feedback_checks(self, mentoring, controls, expected_results):
# Multiple choice is third correctly answered question
self.assert_hidden(controls.review_link)
......
......@@ -38,8 +38,8 @@ class TitleTest(SeleniumXBlockTest):
@ddt.data(
('<problem-builder show_title="false"><pb-answer name="a"/></problem-builder>', None),
('<problem-builder><pb-answer name="a"/></problem-builder>', "Mentoring Questions"),
('<problem-builder mode="assessment"><pb-answer name="a"/></problem-builder>', "Mentoring Questions"),
('<problem-builder><pb-answer name="a"/></problem-builder>', "Problem Builder"),
('<problem-builder mode="assessment"><pb-answer name="a"/></problem-builder>', "Problem Builder"),
('<problem-builder display_name="A Question"><pb-answer name="a"/></problem-builder>', "A Question"),
('<problem-builder display_name="A Question" show_title="false"><pb-answer name="a"/></problem-builder>', None),
)
......
<step-builder url_name="step-builder" display_name="Step Builder"
max_attempts="{{max_attempts}}" extended_feedback="{{extended_feedback}}">
<sb-step display_name="First step">
<pb-answer name="goal" question="What is your goal?" />
</sb-step>
<sb-step display_name="Second step">
<pb-mcq name="mcq_1_1" question="Do you like this MCQ?" correct_choices='["yes"]'>
<pb-choice value="yes">Yes</pb-choice>
<pb-choice value="maybenot">Maybe not</pb-choice>
<pb-choice value="understand">I don't understand</pb-choice>
<pb-tip values='["yes"]'>Great!</pb-tip>
<pb-tip values='["maybenot"]'>Ah, damn.</pb-tip>
<pb-tip values='["understand"]'><div id="test-custom-html">Really?</div></pb-tip>
{% if include_review_tips %}
<pb-message type="on-assessment-review-question">
<html>Take another look at <a href="#">Lesson 1</a></html>
</pb-message>
{% endif %}
</pb-mcq>
</sb-step>
<sb-step display_name="Third step">
<pb-rating name="mcq_1_2" low="Not good at all" high="Extremely good" question="How much do you rate this MCQ?" correct_choices='["4","5"]'>
<pb-choice value="notwant">I don't want to rate it</pb-choice>
<pb-tip values='["4","5"]'>I love good grades.</pb-tip>
<pb-tip values='["1","2", "3"]'>Will do better next time...</pb-tip>
<pb-tip values='["notwant"]'>Your loss!</pb-tip>
{% if include_review_tips %}
<pb-message type="on-assessment-review-question">
<html>Take another look at <a href="#">Lesson 2</a></html>
</pb-message>
{% endif %}
</pb-rating>
</sb-step>
<sb-step display_name="Last step">
<pb-mrq name="mrq_1_1" question="What do you like in this MRQ?" required_choices='["gracefulness","elegance","beauty"]' message="Question Feedback Message">
<pb-choice value="elegance">Its elegance</pb-choice>
<pb-choice value="beauty">Its beauty</pb-choice>
<pb-choice value="gracefulness">Its gracefulness</pb-choice>
<pb-choice value="bugs">Its bugs</pb-choice>
<pb-tip values='["gracefulness"]'>This MRQ is indeed very graceful</pb-tip>
<pb-tip values='["elegance","beauty"]'>This is something everyone has to like about this MRQ</pb-tip>
<pb-tip values='["bugs"]'>Nah, there aren't any!</pb-tip>
{% if include_review_tips %}
<pb-message type="on-assessment-review-question">
<html>Take another look at <a href="#">Lesson 3</a></html>
</pb-message>
{% endif %}
</pb-mrq>
</sb-step>
<sb-review-step></sb-review-step>
<pb-message type="on-assessment-review">
<html>Assessment additional feedback message text</html>
</pb-message>
</step-builder>
......@@ -164,8 +164,7 @@ class TestMentoringBlockJumpToIds(unittest.TestCase):
self.mcq_block = MCQBlock(self.runtime_mock, DictFieldData({'name': 'test_mcq'}), Mock())
self.mcq_block.get_review_tip = Mock()
self.mcq_block.get_review_tip.return_value = self.message_block.content
self.block.steps = []
self.block.get_steps = Mock()
self.block.get_steps.return_value = [self.mcq_block]
self.block.step_ids = []
self.block.steps = [self.mcq_block]
self.block.student_results = {'test_mcq': {'status': 'incorrect'}}
self.assertEqual(self.block.review_tips, ['replaced-url'])
......@@ -47,7 +47,7 @@ class TestQuestionMixin(unittest.TestCase):
step = Step()
block._children = [step]
steps = [block.runtime.get_block(cid) for cid in block.steps]
steps = [block.runtime.get_block(cid) for cid in block.step_ids]
self.assertSequenceEqual(steps, [step])
def test_only_steps_are_returned(self):
......@@ -56,7 +56,7 @@ class TestQuestionMixin(unittest.TestCase):
step2 = Step()
block._set_children_for_test(step1, 1, "2", "Step", NotAStep(), False, step2, NotAStep())
steps = [block.runtime.get_block(cid) for cid in block.steps]
steps = [block.runtime.get_block(cid) for cid in block.step_ids]
self.assertSequenceEqual(steps, [step1, step2])
def test_proper_number_is_returned_for_step(self):
......
......@@ -41,8 +41,9 @@ def package_data(pkg, root_list):
BLOCKS = [
'problem-builder = problem_builder:MentoringBlock',
'pb-mentoring = problem_builder:MentoringWithExplicitStepsBlock',
'pb-mentoring-step = problem_builder:MentoringStepBlock',
'step-builder = problem_builder:MentoringWithExplicitStepsBlock',
'sb-step = problem_builder:MentoringStepBlock',
'sb-review-step = problem_builder:ReviewStepBlock',
'pb-table = problem_builder:MentoringTableBlock',
'pb-column = problem_builder:MentoringTableColumn',
......
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