Commit d08ad4c7 by Sven Marnach

Merge remote-tracking branch 'origin/master' into smarnach/review-link

parents b5db41c4 ee1c5132
...@@ -17,3 +17,5 @@ script: ...@@ -17,3 +17,5 @@ script:
- python run_tests.py --with-coverage --cover-package=problem_builder - python run_tests.py --with-coverage --cover-package=problem_builder
notifications: notifications:
email: false email: false
addons:
firefox: "36.0"
...@@ -26,7 +26,7 @@ import json ...@@ -26,7 +26,7 @@ import json
from collections import namedtuple from collections import namedtuple
from xblock.core import XBlock from xblock.core import XBlock
from xblock.exceptions import NoSuchViewError from xblock.exceptions import NoSuchViewError, JsonHandlerError
from xblock.fields import Boolean, Scope, String, Integer, Float, List from xblock.fields import Boolean, Scope, String, Integer, Float, List
from xblock.fragment import Fragment from xblock.fragment import Fragment
from xblock.validation import ValidationMessage from xblock.validation import ValidationMessage
...@@ -148,6 +148,7 @@ class MentoringBlock(XBlock, StepParentMixin, StudioEditableXBlockMixin, StudioC ...@@ -148,6 +148,7 @@ class MentoringBlock(XBlock, StepParentMixin, StudioEditableXBlockMixin, StudioC
# Has the student attempted this mentoring step? # Has the student attempted this mentoring step?
default=False, default=False,
scope=Scope.user_state scope=Scope.user_state
# TODO: Does anything use this 'attempted' field? May want to delete it.
) )
completed = Boolean( completed = Boolean(
# Has the student completed this mentoring step? # Has the student completed this mentoring step?
...@@ -376,15 +377,24 @@ class MentoringBlock(XBlock, StepParentMixin, StudioEditableXBlockMixin, StudioC ...@@ -376,15 +377,24 @@ class MentoringBlock(XBlock, StepParentMixin, StudioEditableXBlockMixin, StudioC
return {'result': 'ok'} return {'result': 'ok'}
def get_message(self, completed): def get_message(self, completed):
if self.max_attempts_reached: """
return self.get_message_html('max_attempts_reached') Get the message to display to a student following a submission in normal mode.
elif completed: """
if completed:
# Student has achieved a perfect score
return self.get_message_html('completed') return self.get_message_html('completed')
elif self.max_attempts_reached:
# Student has not achieved a perfect score and cannot try again
return self.get_message_html('max_attempts_reached')
else: else:
# Student did not achieve a perfect score but can try again:
return self.get_message_html('incomplete') return self.get_message_html('incomplete')
@property @property
def assessment_message(self): def assessment_message(self):
"""
Get the message to display to a student following a submission in assessment mode.
"""
if not self.max_attempts_reached: if not self.max_attempts_reached:
return self.get_message_html('on-assessment-review') return self.get_message_html('on-assessment-review')
else: else:
...@@ -449,7 +459,6 @@ class MentoringBlock(XBlock, StepParentMixin, StudioEditableXBlockMixin, StudioC ...@@ -449,7 +459,6 @@ class MentoringBlock(XBlock, StepParentMixin, StudioEditableXBlockMixin, StudioC
return { return {
'results': results, 'results': results,
'completed': completed, 'completed': completed,
'attempted': self.attempted,
'message': message, 'message': message,
'step': step, 'step': step,
'max_attempts': self.max_attempts, 'max_attempts': self.max_attempts,
...@@ -459,12 +468,23 @@ class MentoringBlock(XBlock, StepParentMixin, StudioEditableXBlockMixin, StudioC ...@@ -459,12 +468,23 @@ class MentoringBlock(XBlock, StepParentMixin, StudioEditableXBlockMixin, StudioC
@XBlock.json_handler @XBlock.json_handler
def submit(self, submissions, suffix=''): def submit(self, submissions, suffix=''):
log.info(u'Received submissions: {}'.format(submissions)) log.info(u'Received submissions: {}'.format(submissions))
# server-side check that the user is allowed to submit:
if self.max_attempts_reached:
raise JsonHandlerError(403, "Maximum number of attempts already reached.")
elif self.has_missing_dependency:
raise JsonHandlerError(
403,
"You need to complete all previous steps before being able to complete the current one."
)
# This has now been attempted:
self.attempted = True self.attempted = True
if self.is_assessment: if self.is_assessment:
return self.handle_assessment_submit(submissions, suffix) return self.handle_assessment_submit(submissions, suffix)
submit_results = [] submit_results = []
previously_completed = self.completed
completed = True completed = True
for child_id in self.steps: for child_id in self.steps:
child = self.runtime.get_block(child_id) child = self.runtime.get_block(child_id)
...@@ -475,40 +495,32 @@ class MentoringBlock(XBlock, StepParentMixin, StudioEditableXBlockMixin, StudioC ...@@ -475,40 +495,32 @@ class MentoringBlock(XBlock, StepParentMixin, StudioEditableXBlockMixin, StudioC
child.save() child.save()
completed = completed and (child_result['status'] == 'correct') completed = completed and (child_result['status'] == 'correct')
message = self.get_message(completed) if completed and self.next_step == self.url_name:
# Once it has been completed once, keep completion even if user changes values
if self.completed:
completed = True
# server-side check to not set completion if the max_attempts is reached
if self.max_attempts_reached:
completed = False
if self.has_missing_dependency:
completed = False
message = 'You need to complete all previous steps before being able to complete the current one.'
elif completed and self.next_step == self.url_name:
self.next_step = self.followed_by self.next_step = self.followed_by
# Once it was completed, lock score # Update the score and attempts, unless the user had already achieved a perfect score ("completed"):
if not self.completed: if not previously_completed:
# save user score and results # Update the results
while self.student_results: while self.student_results:
self.student_results.pop() self.student_results.pop()
for result in submit_results: for result in submit_results:
self.student_results.append(result) self.student_results.append(result)
# Save the user's latest score
self.runtime.publish(self, 'grade', { self.runtime.publish(self, 'grade', {
'value': self.score.raw, 'value': self.score.raw,
'max_value': 1, 'max_value': 1,
}) })
if not self.completed and self.max_attempts > 0: # Mark this as having used an attempt:
self.num_attempts += 1 if self.max_attempts > 0:
self.num_attempts += 1
self.completed = completed is True # Save the completion status.
# Once it has been completed once, keep completion even if user changes values
self.completed = bool(completed) or previously_completed
message = self.get_message(completed)
raw_score = self.score.raw raw_score = self.score.raw
self.runtime.publish(self, 'xblock.problem_builder.submitted', { self.runtime.publish(self, 'xblock.problem_builder.submitted', {
...@@ -520,10 +532,9 @@ class MentoringBlock(XBlock, StepParentMixin, StudioEditableXBlockMixin, StudioC ...@@ -520,10 +532,9 @@ class MentoringBlock(XBlock, StepParentMixin, StudioEditableXBlockMixin, StudioC
return { return {
'results': submit_results, 'results': submit_results,
'completed': self.completed, 'completed': self.completed,
'attempted': self.attempted,
'message': message, 'message': message,
'max_attempts': self.max_attempts, 'max_attempts': self.max_attempts,
'num_attempts': self.num_attempts 'num_attempts': self.num_attempts,
} }
def handle_assessment_submit(self, submissions, suffix): def handle_assessment_submit(self, submissions, suffix):
...@@ -561,14 +572,13 @@ class MentoringBlock(XBlock, StepParentMixin, StudioEditableXBlockMixin, StudioC ...@@ -561,14 +572,13 @@ class MentoringBlock(XBlock, StepParentMixin, StudioEditableXBlockMixin, StudioC
if current_child == steps[-1]: if current_child == steps[-1]:
log.info(u'Last assessment step submitted: {}'.format(submissions)) log.info(u'Last assessment step submitted: {}'.format(submissions))
if not self.max_attempts_reached: self.runtime.publish(self, 'grade', {
self.runtime.publish(self, 'grade', { 'value': score.raw,
'value': score.raw, 'max_value': 1,
'max_value': 1, 'score_type': 'proficiency',
'score_type': 'proficiency', })
}) event_data['final_grade'] = score.raw
event_data['final_grade'] = score.raw assessment_message = self.assessment_message
assessment_message = self.assessment_message
self.num_attempts += 1 self.num_attempts += 1
self.completed = True self.completed = True
...@@ -581,7 +591,6 @@ class MentoringBlock(XBlock, StepParentMixin, StudioEditableXBlockMixin, StudioC ...@@ -581,7 +591,6 @@ class MentoringBlock(XBlock, StepParentMixin, StudioEditableXBlockMixin, StudioC
return { return {
'completed': completed, 'completed': completed,
'attempted': self.attempted,
'max_attempts': self.max_attempts, 'max_attempts': self.max_attempts,
'num_attempts': self.num_attempts, 'num_attempts': self.num_attempts,
'step': self.step, 'step': self.step,
......
...@@ -40,6 +40,52 @@ class MentoringMessageBlock(XBlock, StudioEditableXBlockMixin): ...@@ -40,6 +40,52 @@ class MentoringMessageBlock(XBlock, StudioEditableXBlockMixin):
A message which can be conditionally displayed at the mentoring block level, A message which can be conditionally displayed at the mentoring block level,
for example upon completion of the block for example upon completion of the block
""" """
MESSAGE_TYPES = {
"completed": {
"display_name": _(u"Completed"),
"long_display_name": _(u"Message shown when complete"),
"default": _(u"Great job!"),
"description": _(
u"In standard mode, this message will be shown when the student achieves a "
"perfect score. "
"This message is ignored in assessment mode."
),
},
"incomplete": {
"display_name": _(u"Incomplete"),
"long_display_name": _(u"Message shown when incomplete"),
"default": _(u"Not quite! You can try again, though."),
"description": _(
u"In standard mode, this message will be shown when the student gets at least "
"one question wrong, but is allowed to try again. "
"This message is ignored in assessment mode."
),
},
"max_attempts_reached": {
"display_name": _(u"Reached max. # of attempts"),
"long_display_name": _(u"Message shown when student reaches max. # of attempts"),
"default": _(u"Sorry, you have used up all of your allowed submissions."),
"description": _(
u"In standard mode, this message will be shown when the student has used up "
"all of their allowed attempts without achieving a perfect score. "
"This message is ignored in assessment mode."
),
},
"on-assessment-review": {
"display_name": _(u"Review with attempts left"),
"long_display_name": _(u"Message shown during review when attempts remain"),
"default": _(
u"You may try this assessment again, and only the latest score will be used."
),
"description": _(
u"In assessment mode, this message will be shown when the student is reviewing "
"their answers to the assessment, if the student is allowed to try again. "
"This message is ignored in standard mode and is not shown if the student has "
"used up all of their allowed attempts."
),
},
}
content = String( content = String(
display_name=_("Message"), display_name=_("Message"),
help=_("Message to display upon completion"), help=_("Message to display upon completion"),
...@@ -53,10 +99,10 @@ class MentoringMessageBlock(XBlock, StudioEditableXBlockMixin): ...@@ -53,10 +99,10 @@ class MentoringMessageBlock(XBlock, StudioEditableXBlockMixin):
scope=Scope.content, scope=Scope.content,
default="completed", default="completed",
values=( values=(
{"display_name": "Completed", "value": "completed"}, {"value": "completed", "display_name": MESSAGE_TYPES["completed"]["display_name"]},
{"display_name": "Incompleted", "value": "incomplete"}, {"value": "incomplete", "display_name": MESSAGE_TYPES["incomplete"]["display_name"]},
{"display_name": "Reached max. # of attemps", "value": "max_attempts_reached"}, {"value": "max_attempts_reached", "display_name": MESSAGE_TYPES["max_attempts_reached"]["display_name"]},
{"display_name": "Review with attempts left", "value": "on-assessment-review"} {"value": "on-assessment-review", "display_name": MESSAGE_TYPES["on-assessment-review"]["display_name"]},
), ),
) )
editable_fields = ("content", ) editable_fields = ("content", )
...@@ -67,34 +113,44 @@ class MentoringMessageBlock(XBlock, StudioEditableXBlockMixin): ...@@ -67,34 +113,44 @@ class MentoringMessageBlock(XBlock, StudioEditableXBlockMixin):
def mentoring_view(self, context=None): def mentoring_view(self, context=None):
""" Render this message for use by a mentoring block. """ """ Render this message for use by a mentoring block. """
html = u'<div class="message {msg_type}">{content}</div>'.format(msg_type=self.type, content=self.content) html = u'<div class="submission-message {msg_type}">{content}</div>'.format(
msg_type=self.type,
content=self.content
)
return Fragment(html) return Fragment(html)
def student_view(self, context=None): def student_view(self, context=None):
""" Normal view of this XBlock, identical to mentoring_view """ """ Normal view of this XBlock, identical to mentoring_view """
return self.mentoring_view(context) return self.mentoring_view(context)
def author_view(self, context=None):
fragment = self.mentoring_view(context)
fragment.content += u'<div class="submission-message-help"><p>{}</p></div>'.format(self.help_text)
return fragment
@property @property
def display_name_with_default(self): def display_name_with_default(self):
if self.type == 'max_attempts_reached': try:
max_attempts = self.get_parent().max_attempts return self._(self.MESSAGE_TYPES[self.type]["long_display_name"])
return self._(u"Message when student reaches max. # of attempts ({limit})").format( except KeyError:
limit=self._(u"unlimited") if max_attempts == 0 else max_attempts return u"INVALID MESSAGE"
)
if self.type == 'completed': @property
return self._(u"Message shown when complete") def help_text(self):
if self.type == 'incomplete': try:
return self._(u"Message shown when incomplete") return self._(self.MESSAGE_TYPES[self.type]["description"])
if self.type == 'on-assessment-review': except KeyError:
return self._(u"Message shown during review when attempts remain") return u"This message is not a valid message type!"
return u"INVALID MESSAGE"
@classmethod @classmethod
def get_template(cls, template_id): def get_template(cls, template_id):
""" """
Used to interact with Studio's create_xblock method to instantiate pre-defined templates. Used to interact with Studio's create_xblock method to instantiate pre-defined templates.
""" """
return {'data': {'type': template_id, 'content': "Message goes here."}} return {'data': {
'type': template_id,
'content': cls.MESSAGE_TYPES[template_id]["default"],
}}
@classmethod @classmethod
def parse_xml(cls, node, runtime, keys, id_generator): def parse_xml(cls, node, runtime, keys, id_generator):
......
...@@ -26,3 +26,11 @@ ...@@ -26,3 +26,11 @@
border-color: #888; border-color: #888;
cursor: default; cursor: default;
} }
.xblock[data-block-type=problem-builder] .submission-message-help p {
border-top: 1px solid #ddd;
font-size: 0.85em;
font-style: italic;
margin-top: 1em;
padding-top: 0.3em;
}
...@@ -24,7 +24,6 @@ function AnswerBlock(runtime, element) { ...@@ -24,7 +24,6 @@ function AnswerBlock(runtime, element) {
handleSubmit: function(result) { handleSubmit: function(result) {
var checkmark = $('.answer-checkmark', element); var checkmark = $('.answer-checkmark', element);
$(element).find('.message').text((result || {}).error || '');
this.clearResult(); this.clearResult();
......
...@@ -5,7 +5,7 @@ function MentoringEditComponents(runtime, element) { ...@@ -5,7 +5,7 @@ function MentoringEditComponents(runtime, element) {
var updateButtons = function() { var updateButtons = function() {
$buttons.each(function() { $buttons.each(function() {
var msg_type = $(this).data('boilerplate'); var msg_type = $(this).data('boilerplate');
$(this).toggleClass('disabled', $('.xblock .message.'+msg_type).length > 0); $(this).toggleClass('disabled', $('.xblock .submission-message.'+msg_type).length > 0);
}); });
}; };
updateButtons(); updateButtons();
......
...@@ -32,6 +32,24 @@ function MentoringStandardView(runtime, element, mentoring) { ...@@ -32,6 +32,24 @@ function MentoringStandardView(runtime, element, mentoring) {
submitDOM.attr('disabled', 'disabled'); submitDOM.attr('disabled', 'disabled');
} }
function handleSubmitError(jqXHR, textStatus, errorThrown) {
if (textStatus == "error") {
var errMsg = errorThrown;
// Check if there's a more specific JSON error message:
if (jqXHR.responseText) {
// Is there a more specific error message we can show?
try {
errMsg = JSON.parse(jqXHR.responseText).error;
} catch (error) { errMsg = jqXHR.responseText.substr(0, 300); }
}
mentoring.setContent(messagesDOM, errMsg);
messagesDOM.show();
submitDOM.attr('disabled', 'disabled');
}
}
function calculate_results(handler_name) { function calculate_results(handler_name) {
var data = {}; var data = {};
var children = mentoring.children; var children = mentoring.children;
...@@ -45,11 +63,7 @@ function MentoringStandardView(runtime, element, mentoring) { ...@@ -45,11 +63,7 @@ function MentoringStandardView(runtime, element, mentoring) {
if (submitXHR) { if (submitXHR) {
submitXHR.abort(); submitXHR.abort();
} }
submitXHR = $.post(handlerUrl, JSON.stringify(data)).success(handleSubmitResults); submitXHR = $.post(handlerUrl, JSON.stringify(data)).success(handleSubmitResults).error(handleSubmitError);
}
function get_results() {
calculate_results('get_results');
} }
function submit() { function submit() {
......
...@@ -65,13 +65,33 @@ class MentoringBaseTest(SeleniumBaseTest, PopupCheckMixin): ...@@ -65,13 +65,33 @@ class MentoringBaseTest(SeleniumBaseTest, PopupCheckMixin):
default_css_selector = 'div.mentoring' default_css_selector = 'div.mentoring'
class MentoringAssessmentBaseTest(SeleniumXBlockTest, PopupCheckMixin): class MentoringBaseTemplateTest(SeleniumXBlockTest, PopupCheckMixin):
""" """
Base class for tests of assessment mode Base class for mentoring tests that use templated XML.
All new tests should inherit from this rather than MentoringBaseTest
""" """
module_name = __name__ module_name = __name__
default_css_selector = 'div.mentoring' default_css_selector = 'div.mentoring'
def load_scenario(self, xml_file, params=None):
params = params or {}
scenario = loader.render_template("xml_templates/{}".format(xml_file), params)
self.set_scenario_xml(scenario)
return self.go_to_view("student_view")
def click_submit(self, mentoring):
""" Click the submit button and wait for the response """
submit = mentoring.find_element_by_css_selector('.submit input.input-main')
self.assertTrue(submit.is_displayed())
self.assertTrue(submit.is_enabled())
submit.click()
self.wait_until_disabled(submit)
class MentoringAssessmentBaseTest(MentoringBaseTemplateTest):
"""
Base class for tests of assessment mode
"""
@staticmethod @staticmethod
def question_text(number): def question_text(number):
if number: if number:
......
...@@ -394,7 +394,7 @@ class MentoringAssessmentTest(MentoringAssessmentBaseTest): ...@@ -394,7 +394,7 @@ class MentoringAssessmentTest(MentoringAssessmentBaseTest):
self.peek_at_review(mentoring, controls, expected_results) self.peek_at_review(mentoring, controls, expected_results)
self.assert_messages_empty(mentoring) self.assert_messages_empty(mentoring)
self.wait_until_clickable(controls.try_again)
controls.try_again.click() controls.try_again.click()
# this is a wait and assertion all together - it waits until expected text is in mentoring block self.wait_until_hidden(controls.try_again)
# and it fails with PrmoiseFailed exception if it's not self.assertIn(self.question_text(0), mentoring.text)
self.wait_until_text_in(self.question_text(0), mentoring)
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
# "AGPLv3". If not, see <http://www.gnu.org/licenses/>. # "AGPLv3". If not, see <http://www.gnu.org/licenses/>.
# #
from mock import Mock, patch from mock import Mock, patch
from xblockutils.base_test import SeleniumXBlockTest from .base_test import MentoringBaseTemplateTest
class MockSubmissionsAPI(object): class MockSubmissionsAPI(object):
...@@ -50,77 +50,20 @@ class MockSubmissionsAPI(object): ...@@ -50,77 +50,20 @@ class MockSubmissionsAPI(object):
return [] return []
class TestDashboardBlock(SeleniumXBlockTest): class TestDashboardBlock(MentoringBaseTemplateTest):
""" """
Test the Student View of a dashboard XBlock linked to some problem builder blocks Test the Student View of a dashboard XBlock linked to some problem builder blocks
""" """
def setUp(self): def setUp(self):
super(TestDashboardBlock, self).setUp() super(TestDashboardBlock, self).setUp()
# Set up our scenario: # Set up our scenario:
self.set_scenario_xml(""" self.load_scenario('dashboard.xml')
<vertical_demo>
<problem-builder display_name="Step 1">
<pb-mcq display_name="1.1 First MCQ" question="Which option?" correct_choices='["1","2","3","4"]'>
<pb-choice value="1">Option 1</pb-choice>
<pb-choice value="2">Option 2</pb-choice>
<pb-choice value="3">Option 3</pb-choice>
<pb-choice value="4">Option 4</pb-choice>
</pb-mcq>
<pb-mcq display_name="1.2 Second MCQ" question="Which option?" correct_choices='["1","2","3","4"]'>
<pb-choice value="1">Option 1</pb-choice>
<pb-choice value="2">Option 2</pb-choice>
<pb-choice value="3">Option 3</pb-choice>
<pb-choice value="4">Option 4</pb-choice>
</pb-mcq>
<pb-mcq display_name="1.3 Third MCQ" question="Which option?" correct_choices='["1","2","3","4"]'>
<pb-choice value="1">Option 1</pb-choice>
<pb-choice value="2">Option 2</pb-choice>
<pb-choice value="3">Option 3</pb-choice>
<pb-choice value="4">Option 4</pb-choice>
</pb-mcq>
<html_demo> This message here should be ignored. </html_demo>
</problem-builder>
<problem-builder display_name="Step 2">
<pb-mcq display_name="2.1 First MCQ" question="Which option?" correct_choices='["1","2","3","4"]'>
<pb-choice value="4">Option 4</pb-choice>
<pb-choice value="5">Option 5</pb-choice>
<pb-choice value="6">Option 6</pb-choice>
</pb-mcq>
<pb-mcq display_name="2.2 Second MCQ" question="Which option?" correct_choices='["1","2","3","4"]'>
<pb-choice value="1">Option 1</pb-choice>
<pb-choice value="2">Option 2</pb-choice>
<pb-choice value="3">Option 3</pb-choice>
<pb-choice value="4">Option 4</pb-choice>
</pb-mcq>
<pb-mcq display_name="2.3 Third MCQ" question="Which option?" correct_choices='["1","2","3","4"]'>
<pb-choice value="1">Option 1</pb-choice>
<pb-choice value="2">Option 2</pb-choice>
<pb-choice value="3">Option 3</pb-choice>
<pb-choice value="4">Option 4</pb-choice>
</pb-mcq>
</problem-builder>
<problem-builder display_name="Step 3">
<pb-mcq display_name="3.1 First MCQ" question="Which option?" correct_choices='["1","2","3","4"]'>
<pb-choice value="1">Option 1</pb-choice>
<pb-choice value="2">Option 2</pb-choice>
<pb-choice value="3">Option 3</pb-choice>
<pb-choice value="4">Option 4</pb-choice>
</pb-mcq>
<pb-mcq display_name="3.2 MCQ with non-numeric values"
question="Which option?" correct_choices='["1","2","3","4"]'>
<pb-choice value="A">Option A</pb-choice>
<pb-choice value="B">Option B</pb-choice>
<pb-choice value="C">Option C</pb-choice>
</pb-mcq>
</problem-builder>
<pb-dashboard mentoring_ids='["dummy-value"]'>
</pb-dashboard>
</vertical_demo>
""")
# Apply a whole bunch of patches that are needed in lieu of the LMS/CMS runtime and edx-submissions: # Apply a whole bunch of patches that are needed in lieu of the LMS/CMS runtime and edx-submissions:
def get_mentoring_blocks(dashboard_block, mentoring_ids, ignore_errors=True): def get_mentoring_blocks(dashboard_block, mentoring_ids, ignore_errors=True):
return [dashboard_block.runtime.get_block(key) for key in dashboard_block.get_parent().children[:-1]] return [dashboard_block.runtime.get_block(key) for key in dashboard_block.get_parent().children[:-1]]
mock_submisisons_api = MockSubmissionsAPI() mock_submisisons_api = MockSubmissionsAPI()
patches = ( patches = (
( (
...@@ -174,10 +117,7 @@ class TestDashboardBlock(SeleniumXBlockTest): ...@@ -174,10 +117,7 @@ class TestDashboardBlock(SeleniumXBlockTest):
for idx, mcq in enumerate(mcqs): for idx, mcq in enumerate(mcqs):
choices = mcq.find_elements_by_css_selector('.choices .choice label') choices = mcq.find_elements_by_css_selector('.choices .choice label')
choices[idx].click() choices[idx].click()
submit = pb.find_element_by_css_selector('.submit input.input-main') self.click_submit(pb)
self.assertTrue(submit.is_enabled())
submit.click()
self.wait_until_disabled(submit)
# Reload the page: # Reload the page:
self.go_to_view("student_view") self.go_to_view("student_view")
......
# -*- 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/>.
#
from .base_test import MentoringBaseTemplateTest
import ddt
COMPLETED, INCOMPLETE, MAX_REACHED = "completed", "incomplete", "max_attempts_reached"
MESSAGES = {
COMPLETED: u"Great job! (completed message)",
INCOMPLETE: u"Not quite! You can try again, though. (incomplete message)",
MAX_REACHED: (
u"Sorry, you have used up all of your allowed submissions. (max_attempts_reached message)"
),
}
@ddt.ddt
class MessagesTest(MentoringBaseTemplateTest):
"""
Test the various types of message that can be added to a problem.
"""
def expect_message(self, msg_type, mentoring):
"""
Assert that the message of the specified type is shown to the user.
"""
messages_element = mentoring.find_element_by_css_selector('.messages')
if msg_type is None:
self.assertFalse(messages_element.is_displayed())
else:
self.assertTrue(messages_element.is_displayed())
message_text = messages_element.text.strip()
self.assertTrue(message_text.startswith("FEEDBACK"))
message_text = message_text[8:].lstrip()
self.assertEqual(MESSAGES[msg_type], message_text)
def click_choice(self, container, choice_text):
""" Click on the choice label with the specified text """
for label in container.find_elements_by_css_selector('.choices .choice label'):
if choice_text in label.text:
label.click()
break
@ddt.data(
("One", COMPLETED),
("Two", COMPLETED),
("I don't understand", MAX_REACHED),
)
@ddt.unpack
def test_one_shot(self, choice_text, expected_message_type):
"""
Test a question that has max_attempts set to 1
"""
mentoring = self.load_scenario("messages.xml", {"max_attempts": 1})
self.expect_message(None, mentoring)
self.click_choice(mentoring, choice_text)
self.click_submit(mentoring)
self.expect_message(expected_message_type, mentoring)
@ddt.data(
(2, "One", COMPLETED),
(2, "I don't understand", MAX_REACHED),
(0, "I don't understand", INCOMPLETE),
(10, "I don't understand", INCOMPLETE),
)
@ddt.unpack
def test_retry(self, max_attempts, choice_text, expected_message_type):
"""
Test submitting a wrong answer, seeing a message, then submitting another answer.
In each case, max_attempts is not 1.
"""
mentoring = self.load_scenario("messages.xml", {"max_attempts": max_attempts})
# First, there should be no message.
self.expect_message(None, mentoring)
# Let's get the question wrong:
self.click_choice(mentoring, "I don't understand")
self.click_submit(mentoring)
# We should now see the INCOMPLETE message:
self.expect_message(INCOMPLETE, mentoring)
# Now, answer as directed and expect the given message:
self.click_choice(mentoring, "One") # Make sure we change our choice so we will be able to submit
self.click_choice(mentoring, choice_text)
self.click_submit(mentoring)
self.expect_message(expected_message_type, mentoring)
...@@ -68,6 +68,7 @@ class MentoringProgressionTest(MentoringBaseTest): ...@@ -68,6 +68,7 @@ class MentoringProgressionTest(MentoringBaseTest):
answer = mentoring.find_element_by_css_selector('textarea') answer = mentoring.find_element_by_css_selector('textarea')
answer.send_keys('This is the answer') answer.send_keys('This is the answer')
submit = mentoring.find_element_by_css_selector('.submit input.input-main') submit = mentoring.find_element_by_css_selector('.submit input.input-main')
self.assertTrue(submit.is_displayed() and submit.is_enabled())
submit.click() submit.click()
self.wait_until_disabled(submit) self.wait_until_disabled(submit)
...@@ -90,6 +91,7 @@ class MentoringProgressionTest(MentoringBaseTest): ...@@ -90,6 +91,7 @@ class MentoringProgressionTest(MentoringBaseTest):
answer = mentoring.find_element_by_css_selector('textarea') answer = mentoring.find_element_by_css_selector('textarea')
answer.send_keys('This is the answer') answer.send_keys('This is the answer')
submit = mentoring.find_element_by_css_selector('.submit input.input-main') submit = mentoring.find_element_by_css_selector('.submit input.input-main')
self.assertTrue(submit.is_displayed() and submit.is_enabled())
submit.click() submit.click()
self.wait_until_disabled(submit) self.wait_until_disabled(submit)
self.assert_warning_is_hidden(mentoring) self.assert_warning_is_hidden(mentoring)
...@@ -106,7 +108,11 @@ class MentoringProgressionTest(MentoringBaseTest): ...@@ -106,7 +108,11 @@ class MentoringProgressionTest(MentoringBaseTest):
# Complete step 2 - no more warnings anywhere # Complete step 2 - no more warnings anywhere
submit = mentoring.find_element_by_css_selector('.submit input.input-main') submit = mentoring.find_element_by_css_selector('.submit input.input-main')
submit.click() # Already filled the textarea in previous step answer = mentoring.find_element_by_css_selector('textarea')
self.assertEqual(answer.text, "") # Earlier attempt to submit did not save
answer.send_keys('This is the answer')
self.assertTrue(submit.is_displayed() and submit.is_enabled())
submit.click()
self.wait_until_disabled(submit) self.wait_until_disabled(submit)
messages = mentoring.find_element_by_css_selector('.messages') messages = mentoring.find_element_by_css_selector('.messages')
......
<vertical_demo>
<problem-builder display_name="Step 1">
<pb-mcq display_name="1.1 First MCQ" question="Which option?" correct_choices='["1","2","3","4"]'>
<pb-choice value="1">Option 1</pb-choice>
<pb-choice value="2">Option 2</pb-choice>
<pb-choice value="3">Option 3</pb-choice>
<pb-choice value="4">Option 4</pb-choice>
</pb-mcq>
<pb-mcq display_name="1.2 Second MCQ" question="Which option?" correct_choices='["1","2","3","4"]'>
<pb-choice value="1">Option 1</pb-choice>
<pb-choice value="2">Option 2</pb-choice>
<pb-choice value="3">Option 3</pb-choice>
<pb-choice value="4">Option 4</pb-choice>
</pb-mcq>
<pb-mcq display_name="1.3 Third MCQ" question="Which option?" correct_choices='["1","2","3","4"]'>
<pb-choice value="1">Option 1</pb-choice>
<pb-choice value="2">Option 2</pb-choice>
<pb-choice value="3">Option 3</pb-choice>
<pb-choice value="4">Option 4</pb-choice>
</pb-mcq>
<html_demo> This message here should be ignored. </html_demo>
</problem-builder>
<problem-builder display_name="Step 2">
<pb-mcq display_name="2.1 First MCQ" question="Which option?" correct_choices='["1","2","3","4"]'>
<pb-choice value="4">Option 4</pb-choice>
<pb-choice value="5">Option 5</pb-choice>
<pb-choice value="6">Option 6</pb-choice>
</pb-mcq>
<pb-mcq display_name="2.2 Second MCQ" question="Which option?" correct_choices='["1","2","3","4"]'>
<pb-choice value="1">Option 1</pb-choice>
<pb-choice value="2">Option 2</pb-choice>
<pb-choice value="3">Option 3</pb-choice>
<pb-choice value="4">Option 4</pb-choice>
</pb-mcq>
<pb-mcq display_name="2.3 Third MCQ" question="Which option?" correct_choices='["1","2","3","4"]'>
<pb-choice value="1">Option 1</pb-choice>
<pb-choice value="2">Option 2</pb-choice>
<pb-choice value="3">Option 3</pb-choice>
<pb-choice value="4">Option 4</pb-choice>
</pb-mcq>
</problem-builder>
<problem-builder display_name="Step 3">
<pb-mcq display_name="3.1 First MCQ" question="Which option?" correct_choices='["1","2","3","4"]'>
<pb-choice value="1">Option 1</pb-choice>
<pb-choice value="2">Option 2</pb-choice>
<pb-choice value="3">Option 3</pb-choice>
<pb-choice value="4">Option 4</pb-choice>
</pb-mcq>
<pb-mcq display_name="3.2 MCQ with non-numeric values"
question="Which option?" correct_choices='["1","2","3","4"]'>
<pb-choice value="A">Option A</pb-choice>
<pb-choice value="B">Option B</pb-choice>
<pb-choice value="C">Option C</pb-choice>
</pb-mcq>
</problem-builder>
<pb-dashboard mentoring_ids='["dummy-value"]'>
</pb-dashboard>
</vertical_demo>
<problem-builder url_name="messages-test" display_name="A Simple Problem Set" max_attempts="{{max_attempts}}">
<html_demo>
<p>This is a test of messages</p>
</html_demo>
<pb-mcq name="mcq_1_1" question="What is 1+0x1?" correct_choices='["one", "two"]'>
<pb-choice value="one">One</pb-choice>
<pb-choice value="two">Two</pb-choice>
<pb-choice value="huh">I don't understand</pb-choice>
<pb-tip values='["one"]'>Yep, if you interpret 'x' as multiplication, this equals one.</pb-tip>
<pb-tip values='["two"]'>You must be a programmer. If you interpret '0x' as a hexadecimal prefix, this equals two.</pb-tip>
</pb-mcq>
<pb-message type="completed">Great job! (completed message)</pb-message>
<pb-message type="incomplete">Not quite! You can try again, though. (incomplete message)</pb-message>
<pb-message type="max_attempts_reached">Sorry, you have used up all of your allowed submissions. (max_attempts_reached message)</pb-message>
<pb-message type="on-assessment-review">You may try this assessment again, and only the latest score will be used. (on-assessment-review message)</pb-message>
</problem-builder>
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