* Minimum possible changes were made to merge CapaModule & CapaDescriptor into one ProblemBlock class. * There are no known changes in behavior. * CapaModule and CapaDescriptor inherited from a number of classes which inherit from XModule or XModuleDescriptor but did not depend on them. For all these classes the methods were moved to mixins which did not inherit from either and then these mixins were added to ProblemBlock in the order which maintains MRO.
3328 lines
137 KiB
Python
3328 lines
137 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
Tests of the Capa XModule
|
|
"""
|
|
# pylint: disable=missing-docstring
|
|
# pylint: disable=invalid-name
|
|
|
|
import datetime
|
|
import json
|
|
import random
|
|
import requests
|
|
import os
|
|
import textwrap
|
|
import unittest
|
|
|
|
import ddt
|
|
from django.utils.encoding import smart_text
|
|
from edx_user_state_client.interface import XBlockUserState
|
|
from lxml import etree
|
|
from mock import Mock, patch, DEFAULT
|
|
import six
|
|
import webob
|
|
from webob.multidict import MultiDict
|
|
|
|
import xmodule
|
|
from xmodule.tests import DATA_DIR
|
|
from capa import responsetypes
|
|
from capa.responsetypes import (StudentInputError, LoncapaProblemError,
|
|
ResponseError)
|
|
from capa.xqueue_interface import XQueueInterface
|
|
from xmodule.capa_module import ComplexEncoder, ProblemBlock
|
|
from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator
|
|
from xblock.field_data import DictFieldData
|
|
from xblock.fields import ScopeIds
|
|
from xblock.scorable import Score
|
|
|
|
from . import get_test_system
|
|
from pytz import UTC
|
|
from capa.correctmap import CorrectMap
|
|
from ..capa_base import RANDOMIZATION, SHOWANSWER
|
|
|
|
|
|
class CapaFactory(object):
|
|
"""
|
|
A helper class to create problem modules with various parameters for testing.
|
|
"""
|
|
|
|
sample_problem_xml = textwrap.dedent("""\
|
|
<?xml version="1.0"?>
|
|
<problem>
|
|
<text>
|
|
<p>What is pi, to two decimal places?</p>
|
|
</text>
|
|
<numericalresponse answer="3.14">
|
|
<textline math="1" size="30"/>
|
|
</numericalresponse>
|
|
</problem>
|
|
""")
|
|
|
|
num = 0
|
|
|
|
@classmethod
|
|
def next_num(cls):
|
|
cls.num += 1
|
|
return cls.num
|
|
|
|
@classmethod
|
|
def input_key(cls, response_num=2, input_num=1):
|
|
"""
|
|
Return the input key to use when passing GET parameters
|
|
"""
|
|
return "input_" + cls.answer_key(response_num, input_num)
|
|
|
|
@classmethod
|
|
def answer_key(cls, response_num=2, input_num=1):
|
|
"""
|
|
Return the key stored in the capa problem answer dict
|
|
"""
|
|
return (
|
|
"%s_%d_%d" % (
|
|
"-".join(['i4x', 'edX', 'capa_test', 'problem', 'SampleProblem%d' % cls.num]),
|
|
response_num,
|
|
input_num
|
|
)
|
|
)
|
|
|
|
@classmethod
|
|
def create(cls, attempts=None, problem_state=None, correct=False, xml=None, override_get_score=True, **kwargs):
|
|
"""
|
|
All parameters are optional, and are added to the created problem if specified.
|
|
|
|
Arguments:
|
|
graceperiod:
|
|
due:
|
|
max_attempts:
|
|
showanswer:
|
|
force_save_button:
|
|
rerandomize: all strings, as specified in the policy for the problem
|
|
|
|
problem_state: a dict to to be serialized into the instance_state of the
|
|
module.
|
|
|
|
attempts: also added to instance state. Will be converted to an int.
|
|
"""
|
|
location = BlockUsageLocator(
|
|
CourseLocator("edX", "capa_test", "2012_Fall", deprecated=True),
|
|
"problem",
|
|
"SampleProblem{0}".format(cls.next_num()),
|
|
deprecated=True,
|
|
)
|
|
if xml is None:
|
|
xml = cls.sample_problem_xml
|
|
field_data = {'data': xml}
|
|
field_data.update(kwargs)
|
|
if problem_state is not None:
|
|
field_data.update(problem_state)
|
|
if attempts is not None:
|
|
# converting to int here because I keep putting "0" and "1" in the tests
|
|
# since everything else is a string.
|
|
field_data['attempts'] = int(attempts)
|
|
|
|
system = get_test_system(course_id=location.course_key)
|
|
system.user_is_staff = kwargs.get('user_is_staff', False)
|
|
system.render_template = Mock(return_value="<div>Test Template HTML</div>")
|
|
module = ProblemBlock(
|
|
system,
|
|
DictFieldData(field_data),
|
|
ScopeIds(None, 'problem', location, location),
|
|
)
|
|
assert module.lcp
|
|
|
|
if override_get_score:
|
|
if correct:
|
|
# TODO: probably better to actually set the internal state properly, but...
|
|
module.score = Score(raw_earned=1, raw_possible=1)
|
|
else:
|
|
module.score = Score(raw_earned=0, raw_possible=1)
|
|
|
|
module.graded = 'False'
|
|
module.weight = 1
|
|
return module
|
|
|
|
|
|
class CapaFactoryWithFiles(CapaFactory):
|
|
"""
|
|
A factory for creating a Capa problem with files attached.
|
|
"""
|
|
sample_problem_xml = textwrap.dedent("""\
|
|
<problem>
|
|
<coderesponse queuename="BerkeleyX-cs188x">
|
|
<!-- actual filenames here don't matter for server-side tests,
|
|
they are only acted upon in the browser. -->
|
|
<filesubmission
|
|
points="25"
|
|
allowed_files="prog1.py prog2.py prog3.py"
|
|
required_files="prog1.py prog2.py prog3.py"
|
|
/>
|
|
<codeparam>
|
|
<answer_display>
|
|
If you're having trouble with this Project,
|
|
please refer to the Lecture Slides and attend office hours.
|
|
</answer_display>
|
|
<grader_payload>{"project": "p3"}</grader_payload>
|
|
</codeparam>
|
|
</coderesponse>
|
|
|
|
<customresponse>
|
|
<text>
|
|
If you worked with a partner, enter their username or email address. If you
|
|
worked alone, enter None.
|
|
</text>
|
|
|
|
<textline points="0" size="40" correct_answer="Your partner's username or 'None'"/>
|
|
<answer type="loncapa/python">
|
|
correct=['correct']
|
|
s = str(submission[0]).strip()
|
|
if submission[0] == '':
|
|
correct[0] = 'incorrect'
|
|
</answer>
|
|
</customresponse>
|
|
</problem>
|
|
""")
|
|
|
|
|
|
@ddt.ddt
|
|
class ProblemBlockTest(unittest.TestCase):
|
|
|
|
def setUp(self):
|
|
super(ProblemBlockTest, self).setUp()
|
|
|
|
now = datetime.datetime.now(UTC)
|
|
day_delta = datetime.timedelta(days=1)
|
|
self.yesterday_str = str(now - day_delta)
|
|
self.today_str = str(now)
|
|
self.tomorrow_str = str(now + day_delta)
|
|
|
|
# in the capa grace period format, not in time delta format
|
|
self.two_day_delta_str = "2 days"
|
|
|
|
def test_import(self):
|
|
module = CapaFactory.create()
|
|
self.assertEqual(module.get_score().raw_earned, 0)
|
|
|
|
other_module = CapaFactory.create()
|
|
self.assertEqual(module.get_score().raw_earned, 0)
|
|
self.assertNotEqual(module.url_name, other_module.url_name,
|
|
"Factory should be creating unique names for each problem")
|
|
|
|
def test_correct(self):
|
|
"""
|
|
Check that the factory creates correct and incorrect problems properly.
|
|
"""
|
|
module = CapaFactory.create()
|
|
self.assertEqual(module.get_score().raw_earned, 0)
|
|
|
|
other_module = CapaFactory.create(correct=True)
|
|
self.assertEqual(other_module.get_score().raw_earned, 1)
|
|
|
|
def test_get_score(self):
|
|
"""
|
|
Tests the internals of get_score. In keeping with the ScorableXBlock spec,
|
|
Capa modules store their score independently of the LCP internals, so it must
|
|
be explicitly updated.
|
|
"""
|
|
student_answers = {'1_2_1': 'abcd'}
|
|
correct_map = CorrectMap(answer_id='1_2_1', correctness="correct", npoints=0.9)
|
|
module = CapaFactory.create(correct=True, override_get_score=False)
|
|
module.lcp.correct_map = correct_map
|
|
module.lcp.student_answers = student_answers
|
|
self.assertEqual(module.get_score().raw_earned, 0.0)
|
|
module.set_score(module.score_from_lcp(module.lcp))
|
|
self.assertEqual(module.get_score().raw_earned, 0.9)
|
|
|
|
other_correct_map = CorrectMap(answer_id='1_2_1', correctness="incorrect", npoints=0.1)
|
|
other_module = CapaFactory.create(correct=False, override_get_score=False)
|
|
other_module.lcp.correct_map = other_correct_map
|
|
other_module.lcp.student_answers = student_answers
|
|
self.assertEqual(other_module.get_score().raw_earned, 0.0)
|
|
other_module.set_score(other_module.score_from_lcp(other_module.lcp))
|
|
self.assertEqual(other_module.get_score().raw_earned, 0.1)
|
|
|
|
def test_showanswer_default(self):
|
|
"""
|
|
Make sure the show answer logic does the right thing.
|
|
"""
|
|
# default, no due date, showanswer 'closed', so problem is open, and show_answer
|
|
# not visible.
|
|
problem = CapaFactory.create()
|
|
self.assertFalse(problem.answer_available())
|
|
|
|
@ddt.data(
|
|
(requests.exceptions.ReadTimeout, (1, 'failed to read from the server')),
|
|
(requests.exceptions.ConnectionError, (1, 'cannot connect to server')),
|
|
)
|
|
@ddt.unpack
|
|
def test_xqueue_request_exception(self, exception, result):
|
|
"""
|
|
Makes sure that platform will raise appropriate exception in case of
|
|
connect/read timeout(s) to request to xqueue
|
|
"""
|
|
xqueue_interface = XQueueInterface("http://example.com/xqueue", Mock())
|
|
with patch.object(xqueue_interface.session, 'post', side_effect=exception):
|
|
# pylint: disable = protected-access
|
|
response = xqueue_interface._http_post('http://some/fake/url', {})
|
|
self.assertEqual(response, result)
|
|
|
|
def test_showanswer_attempted(self):
|
|
problem = CapaFactory.create(showanswer='attempted')
|
|
self.assertFalse(problem.answer_available())
|
|
problem.attempts = 1
|
|
self.assertTrue(problem.answer_available())
|
|
|
|
@ddt.data(
|
|
# If show_correctness=always, Answer is visible after attempted
|
|
({
|
|
'showanswer': 'attempted',
|
|
'max_attempts': '1',
|
|
'show_correctness': 'always',
|
|
}, False, True),
|
|
# If show_correctness=never, Answer is never visible
|
|
({
|
|
'showanswer': 'attempted',
|
|
'max_attempts': '1',
|
|
'show_correctness': 'never',
|
|
}, False, False),
|
|
# If show_correctness=past_due, answer is not visible before due date
|
|
({
|
|
'showanswer': 'attempted',
|
|
'show_correctness': 'past_due',
|
|
'max_attempts': '1',
|
|
'due': 'tomorrow_str',
|
|
}, False, False),
|
|
# If show_correctness=past_due, answer is visible after due date
|
|
({
|
|
'showanswer': 'attempted',
|
|
'show_correctness': 'past_due',
|
|
'max_attempts': '1',
|
|
'due': 'yesterday_str',
|
|
}, True, True),
|
|
)
|
|
@ddt.unpack
|
|
def test_showanswer_hide_correctness(self, problem_data, answer_available_no_attempt, answer_available_after_attempt):
|
|
"""
|
|
Ensure that the answer will not be shown when correctness is being hidden.
|
|
"""
|
|
if 'due' in problem_data:
|
|
problem_data['due'] = getattr(self, problem_data['due'])
|
|
problem = CapaFactory.create(**problem_data)
|
|
self.assertEqual(problem.answer_available(), answer_available_no_attempt)
|
|
problem.attempts = 1
|
|
self.assertEqual(problem.answer_available(), answer_available_after_attempt)
|
|
|
|
def test_showanswer_closed(self):
|
|
|
|
# can see after attempts used up, even with due date in the future
|
|
used_all_attempts = CapaFactory.create(showanswer='closed',
|
|
max_attempts="1",
|
|
attempts="1",
|
|
due=self.tomorrow_str)
|
|
self.assertTrue(used_all_attempts.answer_available())
|
|
|
|
# can see after due date
|
|
after_due_date = CapaFactory.create(showanswer='closed',
|
|
max_attempts="1",
|
|
attempts="0",
|
|
due=self.yesterday_str)
|
|
|
|
self.assertTrue(after_due_date.answer_available())
|
|
|
|
# can't see because attempts left
|
|
attempts_left_open = CapaFactory.create(showanswer='closed',
|
|
max_attempts="1",
|
|
attempts="0",
|
|
due=self.tomorrow_str)
|
|
self.assertFalse(attempts_left_open.answer_available())
|
|
|
|
# Can't see because grace period hasn't expired
|
|
still_in_grace = CapaFactory.create(showanswer='closed',
|
|
max_attempts="1",
|
|
attempts="0",
|
|
due=self.yesterday_str,
|
|
graceperiod=self.two_day_delta_str)
|
|
self.assertFalse(still_in_grace.answer_available())
|
|
|
|
def test_showanswer_correct_or_past_due(self):
|
|
"""
|
|
With showanswer="correct_or_past_due" should show answer after the answer is correct
|
|
or after the problem is closed for everyone--e.g. after due date + grace period.
|
|
"""
|
|
|
|
# can see because answer is correct, even with due date in the future
|
|
answer_correct = CapaFactory.create(showanswer='correct_or_past_due',
|
|
max_attempts="1",
|
|
attempts="0",
|
|
due=self.tomorrow_str,
|
|
correct=True)
|
|
self.assertTrue(answer_correct.answer_available())
|
|
|
|
# can see after due date, even when answer isn't correct
|
|
past_due_date = CapaFactory.create(showanswer='correct_or_past_due',
|
|
max_attempts="1",
|
|
attempts="0",
|
|
due=self.yesterday_str)
|
|
self.assertTrue(past_due_date.answer_available())
|
|
|
|
# can also see after due date when answer _is_ correct
|
|
past_due_date_correct = CapaFactory.create(showanswer='correct_or_past_due',
|
|
max_attempts="1",
|
|
attempts="0",
|
|
due=self.yesterday_str,
|
|
correct=True)
|
|
self.assertTrue(past_due_date_correct.answer_available())
|
|
|
|
# Can't see because grace period hasn't expired and answer isn't correct
|
|
still_in_grace = CapaFactory.create(showanswer='correct_or_past_due',
|
|
max_attempts="1",
|
|
attempts="1",
|
|
due=self.yesterday_str,
|
|
graceperiod=self.two_day_delta_str)
|
|
self.assertFalse(still_in_grace.answer_available())
|
|
|
|
def test_showanswer_past_due(self):
|
|
"""
|
|
With showanswer="past_due" should only show answer after the problem is closed
|
|
for everyone--e.g. after due date + grace period.
|
|
"""
|
|
|
|
# can't see after attempts used up, even with due date in the future
|
|
used_all_attempts = CapaFactory.create(showanswer='past_due',
|
|
max_attempts="1",
|
|
attempts="1",
|
|
due=self.tomorrow_str)
|
|
self.assertFalse(used_all_attempts.answer_available())
|
|
|
|
# can see after due date
|
|
past_due_date = CapaFactory.create(showanswer='past_due',
|
|
max_attempts="1",
|
|
attempts="0",
|
|
due=self.yesterday_str)
|
|
self.assertTrue(past_due_date.answer_available())
|
|
|
|
# can't see because attempts left
|
|
attempts_left_open = CapaFactory.create(showanswer='past_due',
|
|
max_attempts="1",
|
|
attempts="0",
|
|
due=self.tomorrow_str)
|
|
self.assertFalse(attempts_left_open.answer_available())
|
|
|
|
# Can't see because grace period hasn't expired, even though have no more
|
|
# attempts.
|
|
still_in_grace = CapaFactory.create(showanswer='past_due',
|
|
max_attempts="1",
|
|
attempts="1",
|
|
due=self.yesterday_str,
|
|
graceperiod=self.two_day_delta_str)
|
|
self.assertFalse(still_in_grace.answer_available())
|
|
|
|
def test_showanswer_after_attempts_with_max(self):
|
|
"""
|
|
Button should not be visible when attempts < required attempts.
|
|
|
|
Even with max attempts set, the show answer button should only
|
|
show up after the user has attempted answering the question for
|
|
the requisite number of times, i.e `attempts_before_showanswer_button`
|
|
"""
|
|
problem = CapaFactory.create(
|
|
showanswer='after_attempts',
|
|
attempts='2',
|
|
attempts_before_showanswer_button='3',
|
|
max_attempts='5',
|
|
)
|
|
self.assertFalse(problem.answer_available())
|
|
|
|
def test_showanswer_after_attempts_no_max(self):
|
|
"""
|
|
Button should not be visible when attempts < required attempts.
|
|
|
|
Even when max attempts is NOT set, the answer should still
|
|
only be available after the student has attempted the
|
|
problem at least `attempts_before_showanswer_button` times
|
|
"""
|
|
problem = CapaFactory.create(
|
|
showanswer='after_attempts',
|
|
attempts='2',
|
|
attempts_before_showanswer_button='3',
|
|
)
|
|
self.assertFalse(problem.answer_available())
|
|
|
|
def test_showanswer_after_attempts_used_all_attempts(self):
|
|
"""
|
|
Button should be visible even after all attempts are used up.
|
|
|
|
As long as the student has attempted the question for
|
|
the requisite number of times, then the show ans. button is
|
|
visible even after they have exhausted their attempts.
|
|
"""
|
|
problem = CapaFactory.create(
|
|
showanswer='after_attempts',
|
|
attempts_before_showanswer_button='2',
|
|
max_attempts='3',
|
|
attempts='3',
|
|
due=self.tomorrow_str,
|
|
)
|
|
self.assertTrue(problem.answer_available())
|
|
|
|
def test_showanswer_after_attempts_past_due_date(self):
|
|
"""
|
|
Show Answer button should be visible even after the due date.
|
|
|
|
As long as the student has attempted the problem for the requisite
|
|
number of times, the answer should be available past the due date.
|
|
"""
|
|
problem = CapaFactory.create(
|
|
showanswer='after_attempts',
|
|
attempts_before_showanswer_button='2',
|
|
attempts='2',
|
|
due=self.yesterday_str,
|
|
)
|
|
self.assertTrue(problem.answer_available())
|
|
|
|
def test_showanswer_after_attempts_still_in_grace(self):
|
|
"""
|
|
If attempts > required attempts, ans. is available in grace period.
|
|
|
|
As long as the user has attempted for the requisite # of times,
|
|
the show answer button is visible throughout the grace period.
|
|
"""
|
|
problem = CapaFactory.create(
|
|
showanswer='after_attempts',
|
|
after_attempts='3',
|
|
attempts='4',
|
|
due=self.yesterday_str,
|
|
graceperiod=self.two_day_delta_str,
|
|
)
|
|
self.assertTrue(problem.answer_available())
|
|
|
|
def test_showanswer_after_attempts_large(self):
|
|
"""
|
|
If required attempts > max attempts then required attempts = max attempts.
|
|
|
|
Ensure that if attempts_before_showanswer_button > max_attempts,
|
|
the button should show up after all attempts are used up,
|
|
i.e after_attempts falls back to max_attempts
|
|
"""
|
|
problem = CapaFactory.create(
|
|
showanswer='after_attempts',
|
|
attempts_before_showanswer_button='5',
|
|
max_attempts='3',
|
|
attempts='3',
|
|
)
|
|
self.assertTrue(problem.answer_available())
|
|
|
|
def test_showanswer_after_attempts_zero(self):
|
|
"""
|
|
Button should always be visible if required min attempts = 0.
|
|
|
|
If attempts_before_showanswer_button = 0, then the show answer
|
|
button should be visible at all times.
|
|
"""
|
|
problem = CapaFactory.create(
|
|
showanswer='after_attempts',
|
|
attempts_before_showanswer_button='0',
|
|
attempts='0',
|
|
)
|
|
self.assertTrue(problem.answer_available())
|
|
|
|
def test_showanswer_finished(self):
|
|
"""
|
|
With showanswer="finished" should show answer after the problem is closed,
|
|
or after the answer is correct.
|
|
"""
|
|
|
|
# can see after attempts used up, even with due date in the future
|
|
used_all_attempts = CapaFactory.create(showanswer='finished',
|
|
max_attempts="1",
|
|
attempts="1",
|
|
due=self.tomorrow_str)
|
|
self.assertTrue(used_all_attempts.answer_available())
|
|
|
|
# can see after due date
|
|
past_due_date = CapaFactory.create(showanswer='finished',
|
|
max_attempts="1",
|
|
attempts="0",
|
|
due=self.yesterday_str)
|
|
self.assertTrue(past_due_date.answer_available())
|
|
|
|
# can't see because attempts left and wrong
|
|
attempts_left_open = CapaFactory.create(showanswer='finished',
|
|
max_attempts="1",
|
|
attempts="0",
|
|
due=self.tomorrow_str)
|
|
self.assertFalse(attempts_left_open.answer_available())
|
|
|
|
# _can_ see because attempts left and right
|
|
correct_ans = CapaFactory.create(showanswer='finished',
|
|
max_attempts="1",
|
|
attempts="0",
|
|
due=self.tomorrow_str,
|
|
correct=True)
|
|
self.assertTrue(correct_ans.answer_available())
|
|
|
|
# Can see even though grace period hasn't expired, because have no more
|
|
# attempts.
|
|
still_in_grace = CapaFactory.create(showanswer='finished',
|
|
max_attempts="1",
|
|
attempts="1",
|
|
due=self.yesterday_str,
|
|
graceperiod=self.two_day_delta_str)
|
|
self.assertTrue(still_in_grace.answer_available())
|
|
|
|
def test_showanswer_answered(self):
|
|
"""
|
|
Tests that with showanswer="answered" should show answer after the problem is correctly answered.
|
|
It should *NOT* show answer if the answer is incorrect.
|
|
"""
|
|
# Can not see "Show Answer" when student answer is wrong
|
|
answer_wrong = CapaFactory.create(
|
|
showanswer=SHOWANSWER.ANSWERED,
|
|
max_attempts="1",
|
|
attempts="0",
|
|
due=self.tomorrow_str,
|
|
correct=False
|
|
)
|
|
self.assertFalse(answer_wrong.answer_available())
|
|
|
|
# Expect to see "Show Answer" when answer is correct
|
|
answer_correct = CapaFactory.create(
|
|
showanswer=SHOWANSWER.ANSWERED,
|
|
max_attempts="1",
|
|
attempts="0",
|
|
due=self.tomorrow_str,
|
|
correct=True
|
|
)
|
|
self.assertTrue(answer_correct.answer_available())
|
|
|
|
@ddt.data('', 'other-value')
|
|
def test_show_correctness_other(self, show_correctness):
|
|
"""
|
|
Test that correctness is visible if show_correctness is not set to one of the values
|
|
from SHOW_CORRECTNESS constant.
|
|
"""
|
|
problem = CapaFactory.create(show_correctness=show_correctness)
|
|
self.assertTrue(problem.correctness_available())
|
|
|
|
def test_show_correctness_default(self):
|
|
"""
|
|
Test that correctness is visible by default.
|
|
"""
|
|
problem = CapaFactory.create()
|
|
self.assertTrue(problem.correctness_available())
|
|
|
|
def test_show_correctness_never(self):
|
|
"""
|
|
Test that correctness is hidden when show_correctness turned off.
|
|
"""
|
|
problem = CapaFactory.create(show_correctness='never')
|
|
self.assertFalse(problem.correctness_available())
|
|
|
|
@ddt.data(
|
|
# Correctness not visible if due date in the future, even after using up all attempts
|
|
({
|
|
'show_correctness': 'past_due',
|
|
'max_attempts': '1',
|
|
'attempts': '1',
|
|
'due': 'tomorrow_str',
|
|
}, False),
|
|
# Correctness visible if due date in the past
|
|
({
|
|
'show_correctness': 'past_due',
|
|
'max_attempts': '1',
|
|
'attempts': '0',
|
|
'due': 'yesterday_str',
|
|
}, True),
|
|
# Correctness not visible if due date in the future
|
|
({
|
|
'show_correctness': 'past_due',
|
|
'max_attempts': '1',
|
|
'attempts': '0',
|
|
'due': 'tomorrow_str',
|
|
}, False),
|
|
# Correctness not visible because grace period hasn't expired,
|
|
# even after using up all attempts
|
|
({
|
|
'show_correctness': 'past_due',
|
|
'max_attempts': '1',
|
|
'attempts': '1',
|
|
'due': 'yesterday_str',
|
|
'graceperiod': 'two_day_delta_str',
|
|
}, False),
|
|
)
|
|
@ddt.unpack
|
|
def test_show_correctness_past_due(self, problem_data, expected_result):
|
|
"""
|
|
Test that with show_correctness="past_due", correctness will only be visible
|
|
after the problem is closed for everyone--e.g. after due date + grace period.
|
|
"""
|
|
problem_data['due'] = getattr(self, problem_data['due'])
|
|
if 'graceperiod' in problem_data:
|
|
problem_data['graceperiod'] = getattr(self, problem_data['graceperiod'])
|
|
problem = CapaFactory.create(**problem_data)
|
|
self.assertEqual(problem.correctness_available(), expected_result)
|
|
|
|
def test_closed(self):
|
|
|
|
# Attempts < Max attempts --> NOT closed
|
|
module = CapaFactory.create(max_attempts="1", attempts="0")
|
|
self.assertFalse(module.closed())
|
|
|
|
# Attempts < Max attempts --> NOT closed
|
|
module = CapaFactory.create(max_attempts="2", attempts="1")
|
|
self.assertFalse(module.closed())
|
|
|
|
# Attempts = Max attempts --> closed
|
|
module = CapaFactory.create(max_attempts="1", attempts="1")
|
|
self.assertTrue(module.closed())
|
|
|
|
# Attempts > Max attempts --> closed
|
|
module = CapaFactory.create(max_attempts="1", attempts="2")
|
|
self.assertTrue(module.closed())
|
|
|
|
# Max attempts = 0 --> closed
|
|
module = CapaFactory.create(max_attempts="0", attempts="2")
|
|
self.assertTrue(module.closed())
|
|
|
|
# Past due --> closed
|
|
module = CapaFactory.create(max_attempts="1", attempts="0",
|
|
due=self.yesterday_str)
|
|
self.assertTrue(module.closed())
|
|
|
|
def test_parse_get_params(self):
|
|
|
|
# Valid GET param dict
|
|
# 'input_5' intentionally left unset,
|
|
valid_get_dict = MultiDict({
|
|
'input_1': 'test',
|
|
'input_1_2': 'test',
|
|
'input_1_2_3': 'test',
|
|
'input_[]_3': 'test',
|
|
'input_4': None,
|
|
'input_6': 5
|
|
})
|
|
|
|
result = ProblemBlock.make_dict_of_responses(valid_get_dict)
|
|
|
|
# Expect that we get a dict with "input" stripped from key names
|
|
# and that we get the same values back
|
|
for key in result.keys():
|
|
original_key = "input_" + key
|
|
self.assertIn(original_key, valid_get_dict, "Output dict should have key %s" % original_key)
|
|
self.assertEqual(valid_get_dict[original_key], result[key])
|
|
|
|
# Valid GET param dict with list keys
|
|
# Each tuple represents a single parameter in the query string
|
|
valid_get_dict = MultiDict((('input_2[]', 'test1'), ('input_2[]', 'test2')))
|
|
result = ProblemBlock.make_dict_of_responses(valid_get_dict)
|
|
self.assertIn('2', result)
|
|
self.assertEqual(['test1', 'test2'], result['2'])
|
|
|
|
# If we use [] at the end of a key name, we should always
|
|
# get a list, even if there's just one value
|
|
valid_get_dict = MultiDict({'input_1[]': 'test'})
|
|
result = ProblemBlock.make_dict_of_responses(valid_get_dict)
|
|
self.assertEqual(result['1'], ['test'])
|
|
|
|
# If we have no underscores in the name, then the key is invalid
|
|
invalid_get_dict = MultiDict({'input': 'test'})
|
|
with self.assertRaises(ValueError):
|
|
result = ProblemBlock.make_dict_of_responses(invalid_get_dict)
|
|
|
|
# Two equivalent names (one list, one non-list)
|
|
# One of the values would overwrite the other, so detect this
|
|
# and raise an exception
|
|
invalid_get_dict = MultiDict({'input_1[]': 'test 1',
|
|
'input_1': 'test 2'})
|
|
with self.assertRaises(ValueError):
|
|
result = ProblemBlock.make_dict_of_responses(invalid_get_dict)
|
|
|
|
def test_submit_problem_correct(self):
|
|
|
|
module = CapaFactory.create(attempts=1)
|
|
|
|
# Simulate that all answers are marked correct, no matter
|
|
# what the input is, by patching CorrectMap.is_correct()
|
|
# Also simulate rendering the HTML
|
|
with patch('capa.correctmap.CorrectMap.is_correct') as mock_is_correct:
|
|
with patch('xmodule.capa_module.ProblemBlock.get_problem_html') as mock_html:
|
|
mock_is_correct.return_value = True
|
|
mock_html.return_value = "Test HTML"
|
|
|
|
# Check the problem
|
|
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
|
result = module.submit_problem(get_request_dict)
|
|
|
|
# Expect that the problem is marked correct
|
|
self.assertEqual(result['success'], 'correct')
|
|
|
|
# Expect that we get the (mocked) HTML
|
|
self.assertEqual(result['contents'], 'Test HTML')
|
|
|
|
# Expect that the number of attempts is incremented by 1
|
|
self.assertEqual(module.attempts, 2)
|
|
# and that this was considered attempt number 2 for grading purposes
|
|
self.assertEqual(module.lcp.context['attempt'], 2)
|
|
|
|
def test_submit_problem_incorrect(self):
|
|
|
|
module = CapaFactory.create(attempts=0)
|
|
|
|
# Simulate marking the input incorrect
|
|
with patch('capa.correctmap.CorrectMap.is_correct') as mock_is_correct:
|
|
mock_is_correct.return_value = False
|
|
|
|
# Check the problem
|
|
get_request_dict = {CapaFactory.input_key(): '0'}
|
|
result = module.submit_problem(get_request_dict)
|
|
|
|
# Expect that the problem is marked correct
|
|
self.assertEqual(result['success'], 'incorrect')
|
|
|
|
# Expect that the number of attempts is incremented by 1
|
|
self.assertEqual(module.attempts, 1)
|
|
# and that this is considered the first attempt
|
|
self.assertEqual(module.lcp.context['attempt'], 1)
|
|
|
|
def test_submit_problem_closed(self):
|
|
module = CapaFactory.create(attempts=3)
|
|
|
|
# Problem closed -- cannot submit
|
|
# Simulate that ProblemBlock.closed() always returns True
|
|
with patch('xmodule.capa_module.ProblemBlock.closed') as mock_closed:
|
|
mock_closed.return_value = True
|
|
with self.assertRaises(xmodule.exceptions.NotFoundError):
|
|
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
|
module.submit_problem(get_request_dict)
|
|
|
|
# Expect that number of attempts NOT incremented
|
|
self.assertEqual(module.attempts, 3)
|
|
|
|
@ddt.data(
|
|
RANDOMIZATION.ALWAYS,
|
|
'true'
|
|
)
|
|
def test_submit_problem_resubmitted_with_randomize(self, rerandomize):
|
|
# Randomize turned on
|
|
module = CapaFactory.create(rerandomize=rerandomize, attempts=0)
|
|
|
|
# Simulate that the problem is completed
|
|
module.done = True
|
|
|
|
# Expect that we cannot submit
|
|
with self.assertRaises(xmodule.exceptions.NotFoundError):
|
|
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
|
module.submit_problem(get_request_dict)
|
|
|
|
# Expect that number of attempts NOT incremented
|
|
self.assertEqual(module.attempts, 0)
|
|
|
|
@ddt.data(
|
|
RANDOMIZATION.NEVER,
|
|
'false',
|
|
RANDOMIZATION.PER_STUDENT
|
|
)
|
|
def test_submit_problem_resubmitted_no_randomize(self, rerandomize):
|
|
# Randomize turned off
|
|
module = CapaFactory.create(rerandomize=rerandomize, attempts=0, done=True)
|
|
|
|
# Expect that we can submit successfully
|
|
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
|
result = module.submit_problem(get_request_dict)
|
|
|
|
self.assertEqual(result['success'], 'correct')
|
|
|
|
# Expect that number of attempts IS incremented, still same attempt
|
|
self.assertEqual(module.attempts, 1)
|
|
self.assertEqual(module.lcp.context['attempt'], 1)
|
|
|
|
def test_submit_problem_queued(self):
|
|
module = CapaFactory.create(attempts=1)
|
|
|
|
# Simulate that the problem is queued
|
|
multipatch = patch.multiple(
|
|
'capa.capa_problem.LoncapaProblem',
|
|
is_queued=DEFAULT,
|
|
get_recentmost_queuetime=DEFAULT
|
|
)
|
|
with multipatch as values:
|
|
values['is_queued'].return_value = True
|
|
values['get_recentmost_queuetime'].return_value = datetime.datetime.now(UTC)
|
|
|
|
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
|
result = module.submit_problem(get_request_dict)
|
|
|
|
# Expect an AJAX alert message in 'success'
|
|
self.assertIn('You must wait', result['success'])
|
|
|
|
# Expect that the number of attempts is NOT incremented
|
|
self.assertEqual(module.attempts, 1)
|
|
|
|
def test_submit_problem_with_files(self):
|
|
# Check a problem with uploaded files, using the submit_problem API.
|
|
# pylint: disable=protected-access
|
|
|
|
# The files we'll be uploading.
|
|
fnames = ["prog1.py", "prog2.py", "prog3.py"]
|
|
fpaths = [os.path.join(DATA_DIR, "capa", fname) for fname in fnames]
|
|
fileobjs = [open(fpath) for fpath in fpaths]
|
|
for fileobj in fileobjs:
|
|
self.addCleanup(fileobj.close)
|
|
|
|
module = CapaFactoryWithFiles.create()
|
|
|
|
# Mock the XQueueInterface.
|
|
xqueue_interface = XQueueInterface("http://example.com/xqueue", Mock())
|
|
xqueue_interface._http_post = Mock(return_value=(0, "ok"))
|
|
module.system.xqueue['interface'] = xqueue_interface
|
|
|
|
# Create a request dictionary for submit_problem.
|
|
get_request_dict = {
|
|
CapaFactoryWithFiles.input_key(response_num=2): fileobjs,
|
|
CapaFactoryWithFiles.input_key(response_num=3): 'None',
|
|
}
|
|
|
|
module.submit_problem(get_request_dict)
|
|
|
|
# pylint: disable=line-too-long
|
|
# _http_post is called like this:
|
|
# _http_post(
|
|
# 'http://example.com/xqueue/xqueue/submit/',
|
|
# {
|
|
# 'xqueue_header': '{"lms_key": "df34fb702620d7ae892866ba57572491", "lms_callback_url": "/", "queue_name": "BerkeleyX-cs188x"}',
|
|
# 'xqueue_body': '{"student_info": "{\\"anonymous_student_id\\": \\"student\\", \\"submission_time\\": \\"20131117183318\\"}", "grader_payload": "{\\"project\\": \\"p3\\"}", "student_response": ""}',
|
|
# },
|
|
# files={
|
|
# path(u'/home/ned/edx/edx-platform/common/test/data/uploads/asset.html'):
|
|
# <open file u'/home/ned/edx/edx-platform/common/test/data/uploads/asset.html', mode 'r' at 0x49c5f60>,
|
|
# path(u'/home/ned/edx/edx-platform/common/test/data/uploads/image.jpg'):
|
|
# <open file u'/home/ned/edx/edx-platform/common/test/data/uploads/image.jpg', mode 'r' at 0x49c56f0>,
|
|
# path(u'/home/ned/edx/edx-platform/common/test/data/uploads/textbook.pdf'):
|
|
# <open file u'/home/ned/edx/edx-platform/common/test/data/uploads/textbook.pdf', mode 'r' at 0x49c5a50>,
|
|
# },
|
|
# )
|
|
# pylint: enable=line-too-long
|
|
|
|
self.assertEqual(xqueue_interface._http_post.call_count, 1)
|
|
_, kwargs = xqueue_interface._http_post.call_args # pylint: disable=unpacking-non-sequence
|
|
self.assertItemsEqual(fpaths, kwargs['files'].keys())
|
|
for fpath, fileobj in kwargs['files'].iteritems():
|
|
self.assertEqual(fpath, fileobj.name)
|
|
|
|
def test_submit_problem_with_files_as_xblock(self):
|
|
# Check a problem with uploaded files, using the XBlock API.
|
|
# pylint: disable=protected-access
|
|
|
|
# The files we'll be uploading.
|
|
fnames = ["prog1.py", "prog2.py", "prog3.py"]
|
|
fpaths = [os.path.join(DATA_DIR, "capa", fname) for fname in fnames]
|
|
fileobjs = [open(fpath) for fpath in fpaths]
|
|
for fileobj in fileobjs:
|
|
self.addCleanup(fileobj.close)
|
|
|
|
module = CapaFactoryWithFiles.create()
|
|
|
|
# Mock the XQueueInterface.
|
|
xqueue_interface = XQueueInterface("http://example.com/xqueue", Mock())
|
|
xqueue_interface._http_post = Mock(return_value=(0, "ok"))
|
|
module.system.xqueue['interface'] = xqueue_interface
|
|
|
|
# Create a webob Request with the files uploaded.
|
|
post_data = []
|
|
for fname, fileobj in zip(fnames, fileobjs):
|
|
post_data.append((CapaFactoryWithFiles.input_key(response_num=2), (fname, fileobj)))
|
|
post_data.append((CapaFactoryWithFiles.input_key(response_num=3), 'None'))
|
|
request = webob.Request.blank("/some/fake/url", POST=post_data, content_type='multipart/form-data')
|
|
|
|
module.handle('xmodule_handler', request, 'problem_check')
|
|
|
|
self.assertEqual(xqueue_interface._http_post.call_count, 1)
|
|
_, kwargs = xqueue_interface._http_post.call_args # pylint: disable=unpacking-non-sequence
|
|
self.assertItemsEqual(fnames, kwargs['files'].keys())
|
|
for fpath, fileobj in kwargs['files'].iteritems():
|
|
self.assertEqual(fpath, fileobj.name)
|
|
|
|
def test_submit_problem_error(self):
|
|
|
|
# Try each exception that capa_module should handle
|
|
exception_classes = [StudentInputError,
|
|
LoncapaProblemError,
|
|
ResponseError]
|
|
for exception_class in exception_classes:
|
|
|
|
# Create the module
|
|
module = CapaFactory.create(attempts=1, user_is_staff=False)
|
|
|
|
# Simulate answering a problem that raises the exception
|
|
with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade:
|
|
mock_grade.side_effect = exception_class('test error')
|
|
|
|
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
|
result = module.submit_problem(get_request_dict)
|
|
|
|
# Expect an AJAX alert message in 'success'
|
|
expected_msg = 'test error'
|
|
|
|
self.assertEqual(expected_msg, result['success'])
|
|
|
|
# Expect that the number of attempts is NOT incremented
|
|
self.assertEqual(module.attempts, 1)
|
|
# but that this was considered attempt number 2 for grading purposes
|
|
self.assertEqual(module.lcp.context['attempt'], 2)
|
|
|
|
def test_submit_problem_error_with_codejail_exception(self):
|
|
|
|
# Try each exception that capa_module should handle
|
|
exception_classes = [StudentInputError,
|
|
LoncapaProblemError,
|
|
ResponseError]
|
|
for exception_class in exception_classes:
|
|
|
|
# Create the module
|
|
module = CapaFactory.create(attempts=1, user_is_staff=False)
|
|
|
|
# Simulate a codejail exception "Exception: Couldn't execute jailed code"
|
|
with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade:
|
|
try:
|
|
raise ResponseError(
|
|
'Couldn\'t execute jailed code: stdout: \'\', '
|
|
'stderr: \'Traceback (most recent call last):\\n'
|
|
' File "jailed_code", line 15, in <module>\\n'
|
|
' exec code in g_dict\\n File "<string>", line 67, in <module>\\n'
|
|
' File "<string>", line 65, in check_func\\n'
|
|
'Exception: Couldn\'t execute jailed code\\n\' with status code: 1',)
|
|
except ResponseError as err:
|
|
mock_grade.side_effect = exception_class(six.text_type(err))
|
|
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
|
result = module.submit_problem(get_request_dict)
|
|
|
|
# Expect an AJAX alert message in 'success' without the text of the stack trace
|
|
expected_msg = 'Couldn\'t execute jailed code'
|
|
self.assertEqual(expected_msg, result['success'])
|
|
|
|
# Expect that the number of attempts is NOT incremented
|
|
self.assertEqual(module.attempts, 1)
|
|
# but that this was considered the second attempt for grading purposes
|
|
self.assertEqual(module.lcp.context['attempt'], 2)
|
|
|
|
def test_submit_problem_other_errors(self):
|
|
"""
|
|
Test that errors other than the expected kinds give an appropriate message.
|
|
|
|
See also `test_submit_problem_error` for the "expected kinds" or errors.
|
|
"""
|
|
# Create the module
|
|
module = CapaFactory.create(attempts=1, user_is_staff=False)
|
|
|
|
# Ensure that DEBUG is on
|
|
module.system.DEBUG = True
|
|
|
|
# Simulate answering a problem that raises the exception
|
|
with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade:
|
|
error_msg = u"Superterrible error happened: ☠"
|
|
mock_grade.side_effect = Exception(error_msg)
|
|
|
|
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
|
result = module.submit_problem(get_request_dict)
|
|
|
|
# Expect an AJAX alert message in 'success'
|
|
self.assertIn(error_msg, result['success'])
|
|
|
|
def test_submit_problem_zero_max_grade(self):
|
|
"""
|
|
Test that a capa problem with a max grade of zero doesn't generate an error.
|
|
"""
|
|
# Create the module
|
|
module = CapaFactory.create(attempts=1)
|
|
|
|
# Override the problem score to have a total of zero.
|
|
module.lcp.get_score = lambda: {'score': 0, 'total': 0}
|
|
|
|
# Check the problem
|
|
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
|
module.submit_problem(get_request_dict)
|
|
|
|
def test_submit_problem_error_nonascii(self):
|
|
|
|
# Try each exception that capa_module should handle
|
|
exception_classes = [StudentInputError,
|
|
LoncapaProblemError,
|
|
ResponseError]
|
|
for exception_class in exception_classes:
|
|
|
|
# Create the module
|
|
module = CapaFactory.create(attempts=1, user_is_staff=False)
|
|
|
|
# Simulate answering a problem that raises the exception
|
|
with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade:
|
|
mock_grade.side_effect = exception_class(u"ȧƈƈḗƞŧḗḓ ŧḗẋŧ ƒǿř ŧḗşŧīƞɠ")
|
|
|
|
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
|
result = module.submit_problem(get_request_dict)
|
|
|
|
# Expect an AJAX alert message in 'success'
|
|
expected_msg = u'ȧƈƈḗƞŧḗḓ ŧḗẋŧ ƒǿř ŧḗşŧīƞɠ'
|
|
|
|
self.assertEqual(expected_msg, result['success'])
|
|
|
|
# Expect that the number of attempts is NOT incremented
|
|
self.assertEqual(module.attempts, 1)
|
|
# but that this was considered the second attempt for grading purposes
|
|
self.assertEqual(module.lcp.context['attempt'], 2)
|
|
|
|
def test_submit_problem_error_with_staff_user(self):
|
|
|
|
# Try each exception that capa module should handle
|
|
for exception_class in [StudentInputError,
|
|
LoncapaProblemError,
|
|
ResponseError]:
|
|
|
|
# Create the module
|
|
module = CapaFactory.create(attempts=1, user_is_staff=True)
|
|
|
|
# Simulate answering a problem that raises an exception
|
|
with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade:
|
|
mock_grade.side_effect = exception_class('test error')
|
|
|
|
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
|
result = module.submit_problem(get_request_dict)
|
|
|
|
# Expect an AJAX alert message in 'success'
|
|
self.assertIn('test error', result['success'])
|
|
|
|
# We DO include traceback information for staff users
|
|
self.assertIn('Traceback', result['success'])
|
|
|
|
# Expect that the number of attempts is NOT incremented
|
|
self.assertEqual(module.attempts, 1)
|
|
# but that it was considered the second attempt for grading purposes
|
|
self.assertEqual(module.lcp.context['attempt'], 2)
|
|
|
|
@ddt.data(
|
|
("never", True, None, 'submitted'),
|
|
("never", False, None, 'submitted'),
|
|
("past_due", True, None, 'submitted'),
|
|
("past_due", False, None, 'submitted'),
|
|
("always", True, 1, 'correct'),
|
|
("always", False, 0, 'incorrect'),
|
|
)
|
|
@ddt.unpack
|
|
def test_handle_ajax_show_correctness(self, show_correctness, is_correct, expected_score, expected_success):
|
|
module = CapaFactory.create(show_correctness=show_correctness,
|
|
due=self.tomorrow_str,
|
|
correct=is_correct)
|
|
|
|
# Simulate marking the input correct/incorrect
|
|
with patch('capa.correctmap.CorrectMap.is_correct') as mock_is_correct:
|
|
mock_is_correct.return_value = is_correct
|
|
|
|
# Check the problem
|
|
get_request_dict = {CapaFactory.input_key(): '0'}
|
|
json_result = module.handle_ajax('problem_check', get_request_dict)
|
|
result = json.loads(json_result)
|
|
|
|
# Expect that the AJAX result withholds correctness and score
|
|
self.assertEqual(result['current_score'], expected_score)
|
|
self.assertEqual(result['success'], expected_success)
|
|
|
|
# Expect that the number of attempts is incremented by 1
|
|
self.assertEqual(module.attempts, 1)
|
|
self.assertEqual(module.lcp.context['attempt'], 1)
|
|
|
|
def test_reset_problem(self):
|
|
module = CapaFactory.create(done=True)
|
|
module.new_lcp = Mock(wraps=module.new_lcp)
|
|
module.choose_new_seed = Mock(wraps=module.choose_new_seed)
|
|
|
|
# Stub out HTML rendering
|
|
with patch('xmodule.capa_module.ProblemBlock.get_problem_html') as mock_html:
|
|
mock_html.return_value = "<div>Test HTML</div>"
|
|
|
|
# Reset the problem
|
|
get_request_dict = {}
|
|
result = module.reset_problem(get_request_dict)
|
|
|
|
# Expect that the request was successful
|
|
self.assertTrue('success' in result and result['success'])
|
|
|
|
# Expect that the problem HTML is retrieved
|
|
self.assertIn('html', result)
|
|
self.assertEqual(result['html'], "<div>Test HTML</div>")
|
|
|
|
# Expect that the problem was reset
|
|
module.new_lcp.assert_called_once_with(None)
|
|
|
|
def test_reset_problem_closed(self):
|
|
# pre studio default
|
|
module = CapaFactory.create(rerandomize=RANDOMIZATION.ALWAYS)
|
|
|
|
# Simulate that the problem is closed
|
|
with patch('xmodule.capa_module.ProblemBlock.closed') as mock_closed:
|
|
mock_closed.return_value = True
|
|
|
|
# Try to reset the problem
|
|
get_request_dict = {}
|
|
result = module.reset_problem(get_request_dict)
|
|
|
|
# Expect that the problem was NOT reset
|
|
self.assertTrue('success' in result and not result['success'])
|
|
|
|
def test_reset_problem_not_done(self):
|
|
# Simulate that the problem is NOT done
|
|
module = CapaFactory.create(done=False)
|
|
|
|
# Try to reset the problem
|
|
get_request_dict = {}
|
|
result = module.reset_problem(get_request_dict)
|
|
|
|
# Expect that the problem was NOT reset
|
|
self.assertTrue('success' in result and not result['success'])
|
|
|
|
def test_rescore_problem_correct(self):
|
|
|
|
module = CapaFactory.create(attempts=0, done=True)
|
|
|
|
# Simulate that all answers are marked correct, no matter
|
|
# what the input is, by patching LoncapaResponse.evaluate_answers()
|
|
with patch('capa.responsetypes.LoncapaResponse.evaluate_answers') as mock_evaluate_answers:
|
|
mock_evaluate_answers.return_value = CorrectMap(
|
|
answer_id=CapaFactory.answer_key(),
|
|
correctness='correct',
|
|
npoints=1,
|
|
)
|
|
with patch('capa.correctmap.CorrectMap.is_correct') as mock_is_correct:
|
|
mock_is_correct.return_value = True
|
|
|
|
# Check the problem
|
|
get_request_dict = {CapaFactory.input_key(): '1'}
|
|
module.submit_problem(get_request_dict)
|
|
module.rescore(only_if_higher=False)
|
|
|
|
# Expect that the problem is marked correct
|
|
self.assertEqual(module.is_correct(), True)
|
|
|
|
# Expect that the number of attempts is not incremented
|
|
self.assertEqual(module.attempts, 1)
|
|
# and that this was considered attempt number 1 for grading purposes
|
|
self.assertEqual(module.lcp.context['attempt'], 1)
|
|
|
|
def test_rescore_problem_additional_correct(self):
|
|
# make sure it also works when new correct answer has been added
|
|
module = CapaFactory.create(attempts=0)
|
|
answer_id = CapaFactory.answer_key()
|
|
|
|
# Check the problem
|
|
get_request_dict = {CapaFactory.input_key(): '1'}
|
|
result = module.submit_problem(get_request_dict)
|
|
|
|
# Expect that the problem is marked incorrect and user didn't earn score
|
|
self.assertEqual(result['success'], 'incorrect')
|
|
self.assertEqual(module.get_score(), (0, 1))
|
|
self.assertEqual(module.correct_map[answer_id]['correctness'], 'incorrect')
|
|
|
|
# Expect that the number of attempts has incremented to 1
|
|
self.assertEqual(module.attempts, 1)
|
|
self.assertEqual(module.lcp.context['attempt'], 1)
|
|
|
|
# Simulate that after making an incorrect answer to the correct answer
|
|
# the new calculated score is (1,1)
|
|
# by patching CorrectMap.is_correct() and NumericalResponse.get_staff_ans()
|
|
# In case of rescore with only_if_higher=True it should update score of module
|
|
# if previous score was lower
|
|
|
|
with patch('capa.correctmap.CorrectMap.is_correct') as mock_is_correct:
|
|
mock_is_correct.return_value = True
|
|
module.set_score(module.score_from_lcp(module.lcp))
|
|
with patch('capa.responsetypes.NumericalResponse.get_staff_ans') as get_staff_ans:
|
|
get_staff_ans.return_value = 1 + 0j
|
|
module.rescore(only_if_higher=True)
|
|
|
|
# Expect that the problem is marked correct and user earned the score
|
|
self.assertEqual(module.get_score(), (1, 1))
|
|
self.assertEqual(module.correct_map[answer_id]['correctness'], 'correct')
|
|
# Expect that the number of attempts is not incremented
|
|
self.assertEqual(module.attempts, 1)
|
|
# and hence that this was still considered the first attempt for grading purposes
|
|
self.assertEqual(module.lcp.context['attempt'], 1)
|
|
|
|
def test_rescore_problem_incorrect(self):
|
|
# make sure it also works when attempts have been reset,
|
|
# so add this to the test:
|
|
module = CapaFactory.create(attempts=0, done=True)
|
|
|
|
# Simulate that all answers are marked incorrect, no matter
|
|
# what the input is, by patching LoncapaResponse.evaluate_answers()
|
|
with patch('capa.responsetypes.LoncapaResponse.evaluate_answers') as mock_evaluate_answers:
|
|
mock_evaluate_answers.return_value = CorrectMap(CapaFactory.answer_key(), 'incorrect')
|
|
module.rescore(only_if_higher=False)
|
|
|
|
# Expect that the problem is marked incorrect
|
|
self.assertEqual(module.is_correct(), False)
|
|
|
|
# Expect that the number of attempts is not incremented
|
|
self.assertEqual(module.attempts, 0)
|
|
# and that this is treated as the first attempt for grading purposes
|
|
self.assertEqual(module.lcp.context['attempt'], 1)
|
|
|
|
def test_rescore_problem_not_done(self):
|
|
# Simulate that the problem is NOT done
|
|
module = CapaFactory.create(done=False)
|
|
|
|
# Try to rescore the problem, and get exception
|
|
with self.assertRaises(xmodule.exceptions.NotFoundError):
|
|
module.rescore(only_if_higher=False)
|
|
|
|
def test_rescore_problem_not_supported(self):
|
|
module = CapaFactory.create(done=True)
|
|
|
|
# Try to rescore the problem, and get exception
|
|
with patch('capa.capa_problem.LoncapaProblem.supports_rescoring') as mock_supports_rescoring:
|
|
mock_supports_rescoring.return_value = False
|
|
with self.assertRaises(NotImplementedError):
|
|
module.rescore(only_if_higher=False)
|
|
|
|
def _rescore_problem_error_helper(self, exception_class):
|
|
"""Helper to allow testing all errors that rescoring might return."""
|
|
# Create the module
|
|
module = CapaFactory.create(attempts=1, done=True)
|
|
|
|
# Simulate answering a problem that raises the exception
|
|
with patch('capa.capa_problem.LoncapaProblem.get_grade_from_current_answers') as mock_rescore:
|
|
mock_rescore.side_effect = exception_class(u'test error \u03a9')
|
|
with self.assertRaises(exception_class):
|
|
module.rescore(only_if_higher=False)
|
|
|
|
# Expect that the number of attempts is NOT incremented
|
|
self.assertEqual(module.attempts, 1)
|
|
# and that this was considered the first attempt for grading purposes
|
|
self.assertEqual(module.lcp.context['attempt'], 1)
|
|
|
|
def test_rescore_problem_student_input_error(self):
|
|
self._rescore_problem_error_helper(StudentInputError)
|
|
|
|
def test_rescore_problem_problem_error(self):
|
|
self._rescore_problem_error_helper(LoncapaProblemError)
|
|
|
|
def test_rescore_problem_response_error(self):
|
|
self._rescore_problem_error_helper(ResponseError)
|
|
|
|
def test_save_problem(self):
|
|
module = CapaFactory.create(done=False)
|
|
|
|
# Save the problem
|
|
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
|
result = module.save_problem(get_request_dict)
|
|
|
|
# Expect that answers are saved to the problem
|
|
expected_answers = {CapaFactory.answer_key(): '3.14'}
|
|
self.assertEqual(module.lcp.student_answers, expected_answers)
|
|
|
|
# Expect that the result is success
|
|
self.assertTrue('success' in result and result['success'])
|
|
|
|
def test_save_problem_closed(self):
|
|
module = CapaFactory.create(done=False)
|
|
|
|
# Simulate that the problem is closed
|
|
with patch('xmodule.capa_module.ProblemBlock.closed') as mock_closed:
|
|
mock_closed.return_value = True
|
|
|
|
# Try to save the problem
|
|
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
|
result = module.save_problem(get_request_dict)
|
|
|
|
# Expect that the result is failure
|
|
self.assertTrue('success' in result and not result['success'])
|
|
|
|
@ddt.data(
|
|
RANDOMIZATION.ALWAYS,
|
|
'true'
|
|
)
|
|
def test_save_problem_submitted_with_randomize(self, rerandomize):
|
|
# Capa XModule treats 'always' and 'true' equivalently
|
|
module = CapaFactory.create(rerandomize=rerandomize, done=True)
|
|
|
|
# Try to save
|
|
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
|
result = module.save_problem(get_request_dict)
|
|
|
|
# Expect that we cannot save
|
|
self.assertTrue('success' in result and not result['success'])
|
|
|
|
@ddt.data(
|
|
RANDOMIZATION.NEVER,
|
|
'false',
|
|
RANDOMIZATION.PER_STUDENT
|
|
)
|
|
def test_save_problem_submitted_no_randomize(self, rerandomize):
|
|
# Capa XModule treats 'false' and 'per_student' equivalently
|
|
module = CapaFactory.create(rerandomize=rerandomize, done=True)
|
|
|
|
# Try to save
|
|
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
|
result = module.save_problem(get_request_dict)
|
|
|
|
# Expect that we succeed
|
|
self.assertTrue('success' in result and result['success'])
|
|
|
|
def test_submit_button_name(self):
|
|
module = CapaFactory.create(attempts=0)
|
|
self.assertEqual(module.submit_button_name(), "Submit")
|
|
|
|
def test_submit_button_submitting_name(self):
|
|
module = CapaFactory.create(attempts=1, max_attempts=10)
|
|
self.assertEqual(module.submit_button_submitting_name(), "Submitting")
|
|
|
|
def test_should_enable_submit_button(self):
|
|
|
|
attempts = random.randint(1, 10)
|
|
|
|
# If we're after the deadline, disable the submit button
|
|
module = CapaFactory.create(due=self.yesterday_str)
|
|
self.assertFalse(module.should_enable_submit_button())
|
|
|
|
# If user is out of attempts, disable the submit button
|
|
module = CapaFactory.create(attempts=attempts, max_attempts=attempts)
|
|
self.assertFalse(module.should_enable_submit_button())
|
|
|
|
# If survey question (max_attempts = 0), disable the submit button
|
|
module = CapaFactory.create(max_attempts=0)
|
|
self.assertFalse(module.should_enable_submit_button())
|
|
|
|
# If user submitted a problem but hasn't reset,
|
|
# disable the submit button
|
|
# Note: we can only reset when rerandomize="always" or "true"
|
|
module = CapaFactory.create(rerandomize=RANDOMIZATION.ALWAYS, done=True)
|
|
self.assertFalse(module.should_enable_submit_button())
|
|
|
|
module = CapaFactory.create(rerandomize="true", done=True)
|
|
self.assertFalse(module.should_enable_submit_button())
|
|
|
|
# Otherwise, enable the submit button
|
|
module = CapaFactory.create()
|
|
self.assertTrue(module.should_enable_submit_button())
|
|
|
|
# If the user has submitted the problem
|
|
# and we do NOT have a reset button, then we can enable the submit button
|
|
# Setting rerandomize to "never" or "false" ensures that the reset button
|
|
# is not shown
|
|
module = CapaFactory.create(rerandomize=RANDOMIZATION.NEVER, done=True)
|
|
self.assertTrue(module.should_enable_submit_button())
|
|
|
|
module = CapaFactory.create(rerandomize="false", done=True)
|
|
self.assertTrue(module.should_enable_submit_button())
|
|
|
|
module = CapaFactory.create(rerandomize=RANDOMIZATION.PER_STUDENT, done=True)
|
|
self.assertTrue(module.should_enable_submit_button())
|
|
|
|
def test_should_show_reset_button(self):
|
|
|
|
attempts = random.randint(1, 10)
|
|
|
|
# If we're after the deadline, do NOT show the reset button
|
|
module = CapaFactory.create(due=self.yesterday_str, done=True)
|
|
self.assertFalse(module.should_show_reset_button())
|
|
|
|
# If the user is out of attempts, do NOT show the reset button
|
|
module = CapaFactory.create(attempts=attempts, max_attempts=attempts, done=True)
|
|
self.assertFalse(module.should_show_reset_button())
|
|
|
|
# pre studio default value, DO show the reset button
|
|
module = CapaFactory.create(rerandomize=RANDOMIZATION.ALWAYS, done=True)
|
|
self.assertTrue(module.should_show_reset_button())
|
|
|
|
# If survey question for capa (max_attempts = 0),
|
|
# DO show the reset button
|
|
module = CapaFactory.create(rerandomize=RANDOMIZATION.ALWAYS, max_attempts=0, done=True)
|
|
self.assertTrue(module.should_show_reset_button())
|
|
|
|
# If the question is not correct
|
|
# DO show the reset button
|
|
module = CapaFactory.create(rerandomize=RANDOMIZATION.ALWAYS, max_attempts=0, done=True, correct=False)
|
|
self.assertTrue(module.should_show_reset_button())
|
|
|
|
# If the question is correct and randomization is never
|
|
# DO not show the reset button
|
|
module = CapaFactory.create(rerandomize=RANDOMIZATION.NEVER, max_attempts=0, done=True, correct=True)
|
|
self.assertFalse(module.should_show_reset_button())
|
|
|
|
# If the question is correct and randomization is always
|
|
# Show the reset button
|
|
module = CapaFactory.create(rerandomize=RANDOMIZATION.ALWAYS, max_attempts=0, done=True, correct=True)
|
|
self.assertTrue(module.should_show_reset_button())
|
|
|
|
# Don't show reset button if randomization is turned on and the question is not done
|
|
module = CapaFactory.create(rerandomize=RANDOMIZATION.ALWAYS, show_reset_button=False, done=False)
|
|
self.assertFalse(module.should_show_reset_button())
|
|
|
|
# Show reset button if randomization is turned on and the problem is done
|
|
module = CapaFactory.create(rerandomize=RANDOMIZATION.ALWAYS, show_reset_button=False, done=True)
|
|
self.assertTrue(module.should_show_reset_button())
|
|
|
|
def test_should_show_save_button(self):
|
|
|
|
attempts = random.randint(1, 10)
|
|
|
|
# If we're after the deadline, do NOT show the save button
|
|
module = CapaFactory.create(due=self.yesterday_str, done=True)
|
|
self.assertFalse(module.should_show_save_button())
|
|
|
|
# If the user is out of attempts, do NOT show the save button
|
|
module = CapaFactory.create(attempts=attempts, max_attempts=attempts, done=True)
|
|
self.assertFalse(module.should_show_save_button())
|
|
|
|
# If user submitted a problem but hasn't reset, do NOT show the save button
|
|
module = CapaFactory.create(rerandomize=RANDOMIZATION.ALWAYS, done=True)
|
|
self.assertFalse(module.should_show_save_button())
|
|
|
|
module = CapaFactory.create(rerandomize="true", done=True)
|
|
self.assertFalse(module.should_show_save_button())
|
|
|
|
# If the user has unlimited attempts and we are not randomizing,
|
|
# then do NOT show a save button
|
|
# because they can keep using "Check"
|
|
module = CapaFactory.create(max_attempts=None, rerandomize=RANDOMIZATION.NEVER, done=False)
|
|
self.assertFalse(module.should_show_save_button())
|
|
|
|
module = CapaFactory.create(max_attempts=None, rerandomize="false", done=True)
|
|
self.assertFalse(module.should_show_save_button())
|
|
|
|
module = CapaFactory.create(max_attempts=None, rerandomize=RANDOMIZATION.PER_STUDENT, done=True)
|
|
self.assertFalse(module.should_show_save_button())
|
|
|
|
# pre-studio default, DO show the save button
|
|
module = CapaFactory.create(rerandomize=RANDOMIZATION.ALWAYS, done=False)
|
|
self.assertTrue(module.should_show_save_button())
|
|
|
|
# If we're not randomizing and we have limited attempts, then we can save
|
|
module = CapaFactory.create(rerandomize=RANDOMIZATION.NEVER, max_attempts=2, done=True)
|
|
self.assertTrue(module.should_show_save_button())
|
|
|
|
module = CapaFactory.create(rerandomize="false", max_attempts=2, done=True)
|
|
self.assertTrue(module.should_show_save_button())
|
|
|
|
module = CapaFactory.create(rerandomize=RANDOMIZATION.PER_STUDENT, max_attempts=2, done=True)
|
|
self.assertTrue(module.should_show_save_button())
|
|
|
|
# If survey question for capa (max_attempts = 0),
|
|
# DO show the save button
|
|
module = CapaFactory.create(max_attempts=0, done=False)
|
|
self.assertTrue(module.should_show_save_button())
|
|
|
|
def test_should_show_save_button_force_save_button(self):
|
|
# If we're after the deadline, do NOT show the save button
|
|
# even though we're forcing a save
|
|
module = CapaFactory.create(due=self.yesterday_str,
|
|
force_save_button="true",
|
|
done=True)
|
|
self.assertFalse(module.should_show_save_button())
|
|
|
|
# If the user is out of attempts, do NOT show the save button
|
|
attempts = random.randint(1, 10)
|
|
module = CapaFactory.create(attempts=attempts,
|
|
max_attempts=attempts,
|
|
force_save_button="true",
|
|
done=True)
|
|
self.assertFalse(module.should_show_save_button())
|
|
|
|
# Otherwise, if we force the save button,
|
|
# then show it even if we would ordinarily
|
|
# require a reset first
|
|
module = CapaFactory.create(force_save_button="true",
|
|
rerandomize=RANDOMIZATION.ALWAYS,
|
|
done=True)
|
|
self.assertTrue(module.should_show_save_button())
|
|
|
|
module = CapaFactory.create(force_save_button="true",
|
|
rerandomize="true",
|
|
done=True)
|
|
self.assertTrue(module.should_show_save_button())
|
|
|
|
def test_no_max_attempts(self):
|
|
module = CapaFactory.create(max_attempts='')
|
|
html = module.get_problem_html()
|
|
self.assertIsNotNone(html)
|
|
# assert that we got here without exploding
|
|
|
|
def test_get_problem_html(self):
|
|
module = CapaFactory.create()
|
|
|
|
# We've tested the show/hide button logic in other tests,
|
|
# so here we hard-wire the values
|
|
enable_submit_button = bool(random.randint(0, 1) % 2)
|
|
show_reset_button = bool(random.randint(0, 1) % 2)
|
|
show_save_button = bool(random.randint(0, 1) % 2)
|
|
|
|
module.should_enable_submit_button = Mock(return_value=enable_submit_button)
|
|
module.should_show_reset_button = Mock(return_value=show_reset_button)
|
|
module.should_show_save_button = Mock(return_value=show_save_button)
|
|
|
|
# Mock the system rendering function
|
|
module.system.render_template = Mock(return_value="<div>Test Template HTML</div>")
|
|
|
|
# Patch the capa problem's HTML rendering
|
|
with patch('capa.capa_problem.LoncapaProblem.get_html') as mock_html:
|
|
mock_html.return_value = "<div>Test Problem HTML</div>"
|
|
|
|
# Render the problem HTML
|
|
html = module.get_problem_html(encapsulate=False)
|
|
|
|
# Also render the problem encapsulated in a <div>
|
|
html_encapsulated = module.get_problem_html(encapsulate=True)
|
|
|
|
# Expect that we get the rendered template back
|
|
self.assertEqual(html, "<div>Test Template HTML</div>")
|
|
|
|
# Check the rendering context
|
|
render_args, _ = module.system.render_template.call_args
|
|
self.assertEqual(len(render_args), 2)
|
|
|
|
template_name = render_args[0]
|
|
self.assertEqual(template_name, "problem.html")
|
|
|
|
context = render_args[1]
|
|
self.assertEqual(context['problem']['html'], "<div>Test Problem HTML</div>")
|
|
self.assertEqual(bool(context['should_enable_submit_button']), enable_submit_button)
|
|
self.assertEqual(bool(context['reset_button']), show_reset_button)
|
|
self.assertEqual(bool(context['save_button']), show_save_button)
|
|
self.assertFalse(context['demand_hint_possible'])
|
|
|
|
# Assert that the encapsulated html contains the original html
|
|
self.assertIn(html, html_encapsulated)
|
|
|
|
demand_xml = """
|
|
<problem>
|
|
<p>That is the question</p>
|
|
<multiplechoiceresponse>
|
|
<choicegroup type="MultipleChoice">
|
|
<choice correct="false">Alpha <choicehint>A hint</choicehint>
|
|
</choice>
|
|
<choice correct="true">Beta</choice>
|
|
</choicegroup>
|
|
</multiplechoiceresponse>
|
|
<demandhint>
|
|
<hint>Demand 1</hint>
|
|
<hint>Demand 2</hint>
|
|
</demandhint>
|
|
</problem>"""
|
|
|
|
def test_demand_hint(self):
|
|
# HTML generation is mocked out to be meaningless here, so instead we check
|
|
# the context dict passed into HTML generation.
|
|
module = CapaFactory.create(xml=self.demand_xml)
|
|
module.get_problem_html() # ignoring html result
|
|
context = module.system.render_template.call_args[0][1]
|
|
self.assertTrue(context['demand_hint_possible'])
|
|
self.assertTrue(context['should_enable_next_hint'])
|
|
|
|
# Check the AJAX call that gets the hint by index
|
|
result = module.get_demand_hint(0)
|
|
self.assertEqual(result['hint_index'], 0)
|
|
self.assertTrue(result['should_enable_next_hint'])
|
|
|
|
result = module.get_demand_hint(1)
|
|
self.assertEqual(result['hint_index'], 1)
|
|
self.assertFalse(result['should_enable_next_hint'])
|
|
|
|
result = module.get_demand_hint(2) # here the server wraps around to index 0
|
|
self.assertEqual(result['hint_index'], 0)
|
|
self.assertTrue(result['should_enable_next_hint'])
|
|
|
|
def test_single_demand_hint(self):
|
|
"""
|
|
Test the hint button enabled state when there is just a single hint.
|
|
"""
|
|
test_xml = """
|
|
<problem>
|
|
<p>That is the question</p>
|
|
<multiplechoiceresponse>
|
|
<choicegroup type="MultipleChoice">
|
|
<choice correct="false">Alpha <choicehint>A hint</choicehint>
|
|
</choice>
|
|
<choice correct="true">Beta</choice>
|
|
</choicegroup>
|
|
</multiplechoiceresponse>
|
|
<demandhint>
|
|
<hint>Only demand hint</hint>
|
|
</demandhint>
|
|
</problem>"""
|
|
module = CapaFactory.create(xml=test_xml)
|
|
module.get_problem_html() # ignoring html result
|
|
context = module.system.render_template.call_args[0][1]
|
|
self.assertTrue(context['demand_hint_possible'])
|
|
self.assertTrue(context['should_enable_next_hint'])
|
|
|
|
# Check the AJAX call that gets the hint by index
|
|
result = module.get_demand_hint(0)
|
|
self.assertEqual(result['hint_index'], 0)
|
|
self.assertFalse(result['should_enable_next_hint'])
|
|
|
|
def test_demand_hint_logging(self):
|
|
def mock_location_text(self):
|
|
"""
|
|
Mock implementation of __unicode__ or __str__ for the module's location.
|
|
"""
|
|
return u'i4x://edX/capa_test/problem/meh'
|
|
|
|
module = CapaFactory.create(xml=self.demand_xml)
|
|
# Re-mock the module_id to a fixed string, so we can check the logging
|
|
module.location = Mock(module.location)
|
|
if six.PY2:
|
|
module.location.__unicode__ = mock_location_text
|
|
else:
|
|
module.location.__str__ = mock_location_text
|
|
|
|
with patch.object(module.runtime, 'publish') as mock_track_function:
|
|
module.get_problem_html()
|
|
module.get_demand_hint(0)
|
|
mock_track_function.assert_called_with(
|
|
module, 'edx.problem.hint.demandhint_displayed',
|
|
{'hint_index': 0, 'module_id': u'i4x://edX/capa_test/problem/meh',
|
|
'hint_text': 'Demand 1', 'hint_len': 2}
|
|
)
|
|
|
|
def test_input_state_consistency(self):
|
|
module1 = CapaFactory.create()
|
|
module2 = CapaFactory.create()
|
|
|
|
# check to make sure that the input_state and the keys have the same values
|
|
module1.set_state_from_lcp()
|
|
self.assertEqual(module1.lcp.inputs.keys(), module1.input_state.keys())
|
|
|
|
module2.set_state_from_lcp()
|
|
|
|
intersection = set(module2.input_state.keys()).intersection(set(module1.input_state.keys()))
|
|
self.assertEqual(len(intersection), 0)
|
|
|
|
def test_get_problem_html_error(self):
|
|
"""
|
|
In production, when an error occurs with the problem HTML
|
|
rendering, a "dummy" problem is created with an error
|
|
message to display to the user.
|
|
"""
|
|
module = CapaFactory.create()
|
|
|
|
# Save the original problem so we can compare it later
|
|
original_problem = module.lcp
|
|
|
|
# Simulate throwing an exception when the capa problem
|
|
# is asked to render itself as HTML
|
|
module.lcp.get_html = Mock(side_effect=Exception("Test"))
|
|
|
|
# Stub out the get_test_system rendering function
|
|
module.system.render_template = Mock(return_value="<div>Test Template HTML</div>")
|
|
|
|
# Turn off DEBUG
|
|
module.system.DEBUG = False
|
|
|
|
# Try to render the module with DEBUG turned off
|
|
html = module.get_problem_html()
|
|
|
|
self.assertIsNotNone(html)
|
|
|
|
# Check the rendering context
|
|
render_args, _ = module.system.render_template.call_args
|
|
context = render_args[1]
|
|
self.assertIn("error", context['problem']['html'])
|
|
|
|
# Expect that the module has created a new dummy problem with the error
|
|
self.assertNotEqual(original_problem, module.lcp)
|
|
|
|
def test_get_problem_html_error_w_debug(self):
|
|
"""
|
|
Test the html response when an error occurs with DEBUG on
|
|
"""
|
|
module = CapaFactory.create()
|
|
|
|
# Simulate throwing an exception when the capa problem
|
|
# is asked to render itself as HTML
|
|
error_msg = u"Superterrible error happened: ☠"
|
|
module.lcp.get_html = Mock(side_effect=Exception(error_msg))
|
|
|
|
# Stub out the get_test_system rendering function
|
|
module.system.render_template = Mock(return_value="<div>Test Template HTML</div>")
|
|
|
|
# Make sure DEBUG is on
|
|
module.system.DEBUG = True
|
|
|
|
# Try to render the module with DEBUG turned on
|
|
html = module.get_problem_html()
|
|
|
|
self.assertIsNotNone(html)
|
|
|
|
# Check the rendering context
|
|
render_args, _ = module.system.render_template.call_args
|
|
context = render_args[1]
|
|
self.assertIn(error_msg, context['problem']['html'])
|
|
|
|
@ddt.data(
|
|
'false',
|
|
'true',
|
|
RANDOMIZATION.NEVER,
|
|
RANDOMIZATION.PER_STUDENT,
|
|
RANDOMIZATION.ALWAYS,
|
|
RANDOMIZATION.ONRESET
|
|
)
|
|
def test_random_seed_no_change(self, rerandomize):
|
|
|
|
# Run the test for each possible rerandomize value
|
|
|
|
module = CapaFactory.create(rerandomize=rerandomize)
|
|
|
|
# Get the seed
|
|
# By this point, the module should have persisted the seed
|
|
seed = module.seed
|
|
self.assertIsNotNone(seed)
|
|
|
|
# If we're not rerandomizing, the seed is always set
|
|
# to the same value (1)
|
|
if rerandomize == RANDOMIZATION.NEVER:
|
|
self.assertEqual(seed, 1,
|
|
msg="Seed should always be 1 when rerandomize='%s'" % rerandomize)
|
|
|
|
# Check the problem
|
|
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
|
module.submit_problem(get_request_dict)
|
|
|
|
# Expect that the seed is the same
|
|
self.assertEqual(seed, module.seed)
|
|
|
|
# Save the problem
|
|
module.save_problem(get_request_dict)
|
|
|
|
# Expect that the seed is the same
|
|
self.assertEqual(seed, module.seed)
|
|
|
|
@ddt.data(
|
|
'false',
|
|
'true',
|
|
RANDOMIZATION.NEVER,
|
|
RANDOMIZATION.PER_STUDENT,
|
|
RANDOMIZATION.ALWAYS,
|
|
RANDOMIZATION.ONRESET
|
|
)
|
|
def test_random_seed_with_reset(self, rerandomize):
|
|
"""
|
|
Run the test for each possible rerandomize value
|
|
"""
|
|
|
|
def _reset_and_get_seed(module):
|
|
"""
|
|
Reset the XModule and return the module's seed
|
|
"""
|
|
|
|
# Simulate submitting an attempt
|
|
# We need to do this, or reset_problem() will
|
|
# fail because it won't re-randomize until the problem has been submitted
|
|
# the problem yet.
|
|
module.done = True
|
|
|
|
# Reset the problem
|
|
module.reset_problem({})
|
|
|
|
# Return the seed
|
|
return module.seed
|
|
|
|
def _retry_and_check(num_tries, test_func):
|
|
'''
|
|
Returns True if *test_func* was successful
|
|
(returned True) within *num_tries* attempts
|
|
|
|
*test_func* must be a function
|
|
of the form test_func() -> bool
|
|
'''
|
|
success = False
|
|
for __ in range(num_tries):
|
|
if test_func() is True:
|
|
success = True
|
|
break
|
|
return success
|
|
|
|
module = CapaFactory.create(rerandomize=rerandomize, done=True)
|
|
|
|
# Get the seed
|
|
# By this point, the module should have persisted the seed
|
|
seed = module.seed
|
|
self.assertIsNotNone(seed)
|
|
|
|
# We do NOT want the seed to reset if rerandomize
|
|
# is set to 'never' -- it should still be 1
|
|
# The seed also stays the same if we're randomizing
|
|
# 'per_student': the same student should see the same problem
|
|
if rerandomize in [RANDOMIZATION.NEVER,
|
|
'false',
|
|
RANDOMIZATION.PER_STUDENT]:
|
|
self.assertEqual(seed, _reset_and_get_seed(module))
|
|
|
|
# Otherwise, we expect the seed to change
|
|
# to another valid seed
|
|
else:
|
|
|
|
# Since there's a small chance (expected) we might get the
|
|
# same seed again, give it 10 chances
|
|
# to generate a different seed
|
|
success = _retry_and_check(10, lambda: _reset_and_get_seed(module) != seed)
|
|
|
|
self.assertIsNotNone(module.seed)
|
|
msg = 'Could not get a new seed from reset after 10 tries'
|
|
self.assertTrue(success, msg)
|
|
|
|
@ddt.data(
|
|
'false',
|
|
'true',
|
|
RANDOMIZATION.NEVER,
|
|
RANDOMIZATION.PER_STUDENT,
|
|
RANDOMIZATION.ALWAYS,
|
|
RANDOMIZATION.ONRESET
|
|
)
|
|
def test_random_seed_with_reset_question_unsubmitted(self, rerandomize):
|
|
"""
|
|
Run the test for each possible rerandomize value
|
|
"""
|
|
def _reset_and_get_seed(module):
|
|
"""
|
|
Reset the XModule and return the module's seed
|
|
"""
|
|
|
|
# Reset the problem
|
|
# By default, the problem is instantiated as unsubmitted
|
|
module.reset_problem({})
|
|
|
|
# Return the seed
|
|
return module.seed
|
|
|
|
module = CapaFactory.create(rerandomize=rerandomize, done=False)
|
|
|
|
# Get the seed
|
|
# By this point, the module should have persisted the seed
|
|
seed = module.seed
|
|
self.assertIsNotNone(seed)
|
|
|
|
#the seed should never change because the student hasn't finished the problem
|
|
self.assertEqual(seed, _reset_and_get_seed(module))
|
|
|
|
@ddt.data(
|
|
RANDOMIZATION.ALWAYS,
|
|
RANDOMIZATION.PER_STUDENT,
|
|
'true',
|
|
RANDOMIZATION.ONRESET
|
|
)
|
|
def test_random_seed_bins(self, rerandomize):
|
|
# Assert that we are limiting the number of possible seeds.
|
|
# Get a bunch of seeds, they should all be in 0-999.
|
|
i = 200
|
|
while i > 0:
|
|
module = CapaFactory.create(rerandomize=rerandomize)
|
|
assert 0 <= module.seed < 1000
|
|
i -= 1
|
|
|
|
@patch('xmodule.capa_base.log')
|
|
@patch('xmodule.capa_base.Progress')
|
|
def test_get_progress_error(self, mock_progress, mock_log):
|
|
"""
|
|
Check that an exception given in `Progress` produces a `log.exception` call.
|
|
"""
|
|
error_types = [TypeError, ValueError]
|
|
for error_type in error_types:
|
|
mock_progress.side_effect = error_type
|
|
module = CapaFactory.create()
|
|
self.assertIsNone(module.get_progress())
|
|
mock_log.exception.assert_called_once_with('Got bad progress')
|
|
mock_log.reset_mock()
|
|
|
|
@patch('xmodule.capa_base.Progress')
|
|
def test_get_progress_no_error_if_weight_zero(self, mock_progress):
|
|
"""
|
|
Check that if the weight is 0 get_progress does not try to create a Progress object.
|
|
"""
|
|
mock_progress.return_value = True
|
|
module = CapaFactory.create()
|
|
module.weight = 0
|
|
progress = module.get_progress()
|
|
self.assertIsNone(progress)
|
|
self.assertFalse(mock_progress.called)
|
|
|
|
@patch('xmodule.capa_base.Progress')
|
|
def test_get_progress_calculate_progress_fraction(self, mock_progress):
|
|
"""
|
|
Check that score and total are calculated correctly for the progress fraction.
|
|
"""
|
|
module = CapaFactory.create()
|
|
module.weight = 1
|
|
module.get_progress()
|
|
mock_progress.assert_called_with(0, 1)
|
|
|
|
other_module = CapaFactory.create(correct=True)
|
|
other_module.weight = 1
|
|
other_module.get_progress()
|
|
mock_progress.assert_called_with(1, 1)
|
|
|
|
@ddt.data(
|
|
("never", True, None),
|
|
("never", False, None),
|
|
("past_due", True, None),
|
|
("past_due", False, None),
|
|
("always", True, 1),
|
|
("always", False, 0),
|
|
)
|
|
@ddt.unpack
|
|
def test_get_display_progress_show_correctness(self, show_correctness, is_correct, expected_score):
|
|
"""
|
|
Check that score and total are calculated correctly for the progress fraction.
|
|
"""
|
|
module = CapaFactory.create(correct=is_correct,
|
|
show_correctness=show_correctness,
|
|
due=self.tomorrow_str)
|
|
module.weight = 1
|
|
score, total = module.get_display_progress()
|
|
self.assertEqual(score, expected_score)
|
|
self.assertEqual(total, 1)
|
|
|
|
def test_get_html(self):
|
|
"""
|
|
Check that get_html() calls get_progress() with no arguments.
|
|
"""
|
|
module = CapaFactory.create()
|
|
module.get_progress = Mock(wraps=module.get_progress)
|
|
module.get_html()
|
|
module.get_progress.assert_called_with()
|
|
|
|
def test_get_problem(self):
|
|
"""
|
|
Check that get_problem() returns the expected dictionary.
|
|
"""
|
|
module = CapaFactory.create()
|
|
self.assertEquals(module.get_problem("data"), {'html': module.get_problem_html(encapsulate=False)})
|
|
|
|
# Standard question with shuffle="true" used by a few tests
|
|
common_shuffle_xml = textwrap.dedent("""
|
|
<problem>
|
|
<multiplechoiceresponse>
|
|
<choicegroup type="MultipleChoice" shuffle="true">
|
|
<choice correct="false">Apple</choice>
|
|
<choice correct="false">Banana</choice>
|
|
<choice correct="false">Chocolate</choice>
|
|
<choice correct ="true">Donut</choice>
|
|
</choicegroup>
|
|
</multiplechoiceresponse>
|
|
</problem>
|
|
""")
|
|
|
|
def test_check_unmask(self):
|
|
"""
|
|
Check that shuffle unmasking is plumbed through: when submit_problem is called,
|
|
unmasked names should appear in the track_function event_info.
|
|
"""
|
|
module = CapaFactory.create(xml=self.common_shuffle_xml)
|
|
with patch.object(module.runtime, 'publish') as mock_track_function:
|
|
get_request_dict = {CapaFactory.input_key(): 'choice_3'} # the correct choice
|
|
module.submit_problem(get_request_dict)
|
|
mock_call = mock_track_function.mock_calls[1]
|
|
event_info = mock_call[1][2]
|
|
self.assertEqual(event_info['answers'][CapaFactory.answer_key()], 'choice_3')
|
|
# 'permutation' key added to record how problem was shown
|
|
self.assertEquals(event_info['permutation'][CapaFactory.answer_key()],
|
|
('shuffle', ['choice_3', 'choice_1', 'choice_2', 'choice_0']))
|
|
self.assertEquals(event_info['success'], 'correct')
|
|
|
|
@unittest.skip("masking temporarily disabled")
|
|
def test_save_unmask(self):
|
|
"""On problem save, unmasked data should appear on track_function."""
|
|
module = CapaFactory.create(xml=self.common_shuffle_xml)
|
|
with patch.object(module.runtime, 'track_function') as mock_track_function:
|
|
get_request_dict = {CapaFactory.input_key(): 'mask_0'}
|
|
module.save_problem(get_request_dict)
|
|
mock_call = mock_track_function.mock_calls[0]
|
|
event_info = mock_call[1][1]
|
|
self.assertEquals(event_info['answers'][CapaFactory.answer_key()], 'choice_2')
|
|
self.assertIsNotNone(event_info['permutation'][CapaFactory.answer_key()])
|
|
|
|
@unittest.skip("masking temporarily disabled")
|
|
def test_reset_unmask(self):
|
|
"""On problem reset, unmask names should appear track_function."""
|
|
module = CapaFactory.create(xml=self.common_shuffle_xml)
|
|
get_request_dict = {CapaFactory.input_key(): 'mask_0'}
|
|
module.submit_problem(get_request_dict)
|
|
# On reset, 'old_state' should use unmasked names
|
|
with patch.object(module.runtime, 'track_function') as mock_track_function:
|
|
module.reset_problem(None)
|
|
mock_call = mock_track_function.mock_calls[0]
|
|
event_info = mock_call[1][1]
|
|
self.assertEquals(mock_call[1][0], 'reset_problem')
|
|
self.assertEquals(event_info['old_state']['student_answers'][CapaFactory.answer_key()], 'choice_2')
|
|
self.assertIsNotNone(event_info['permutation'][CapaFactory.answer_key()])
|
|
|
|
@unittest.skip("masking temporarily disabled")
|
|
def test_rescore_unmask(self):
|
|
"""On problem rescore, unmasked names should appear on track_function."""
|
|
module = CapaFactory.create(xml=self.common_shuffle_xml)
|
|
get_request_dict = {CapaFactory.input_key(): 'mask_0'}
|
|
module.submit_problem(get_request_dict)
|
|
# On rescore, state/student_answers should use unmasked names
|
|
with patch.object(module.runtime, 'track_function') as mock_track_function:
|
|
module.rescore_problem(only_if_higher=False)
|
|
mock_call = mock_track_function.mock_calls[0]
|
|
event_info = mock_call[1][1]
|
|
self.assertEquals(mock_call[1][0], 'problem_rescore')
|
|
self.assertEquals(event_info['state']['student_answers'][CapaFactory.answer_key()], 'choice_2')
|
|
self.assertIsNotNone(event_info['permutation'][CapaFactory.answer_key()])
|
|
|
|
def test_check_unmask_answerpool(self):
|
|
"""Check answer-pool question track_function uses unmasked names"""
|
|
xml = textwrap.dedent("""
|
|
<problem>
|
|
<multiplechoiceresponse>
|
|
<choicegroup type="MultipleChoice" answer-pool="4">
|
|
<choice correct="false">Apple</choice>
|
|
<choice correct="false">Banana</choice>
|
|
<choice correct="false">Chocolate</choice>
|
|
<choice correct ="true">Donut</choice>
|
|
</choicegroup>
|
|
</multiplechoiceresponse>
|
|
</problem>
|
|
""")
|
|
module = CapaFactory.create(xml=xml)
|
|
with patch.object(module.runtime, 'publish') as mock_track_function:
|
|
get_request_dict = {CapaFactory.input_key(): 'choice_2'} # mask_X form when masking enabled
|
|
module.submit_problem(get_request_dict)
|
|
mock_call = mock_track_function.mock_calls[1]
|
|
event_info = mock_call[1][2]
|
|
self.assertEqual(event_info['answers'][CapaFactory.answer_key()], 'choice_2')
|
|
# 'permutation' key added to record how problem was shown
|
|
self.assertEquals(event_info['permutation'][CapaFactory.answer_key()],
|
|
('answerpool', ['choice_1', 'choice_3', 'choice_2', 'choice_0']))
|
|
self.assertEquals(event_info['success'], 'incorrect')
|
|
|
|
@ddt.unpack
|
|
@ddt.data(
|
|
{'display_name': None, 'expected_display_name': 'problem'},
|
|
{'display_name': '', 'expected_display_name': 'problem'},
|
|
{'display_name': ' ', 'expected_display_name': 'problem'},
|
|
{'display_name': 'CAPA 101', 'expected_display_name': 'CAPA 101'}
|
|
)
|
|
def test_problem_display_name_with_default(self, display_name, expected_display_name):
|
|
"""
|
|
Verify that display_name_with_default works as expected.
|
|
"""
|
|
module = CapaFactory.create(display_name=display_name)
|
|
self.assertEqual(module.display_name_with_default, expected_display_name)
|
|
|
|
@ddt.data(
|
|
'',
|
|
' ',
|
|
)
|
|
def test_problem_no_display_name(self, display_name):
|
|
"""
|
|
Verify that if problem display name is not provided then a default name is used.
|
|
"""
|
|
module = CapaFactory.create(display_name=display_name)
|
|
module.get_problem_html()
|
|
render_args, _ = module.system.render_template.call_args
|
|
context = render_args[1]
|
|
self.assertEqual(context['problem']['name'], module.location.block_type)
|
|
|
|
|
|
@ddt.ddt
|
|
class ProblemBlockXMLTest(unittest.TestCase):
|
|
|
|
sample_checkbox_problem_xml = textwrap.dedent("""
|
|
<problem>
|
|
<p>Title</p>
|
|
|
|
<p>Description</p>
|
|
|
|
<p>Example</p>
|
|
|
|
<p>The following languages are in the Indo-European family:</p>
|
|
<choiceresponse>
|
|
<checkboxgroup>
|
|
<choice correct="true">Urdu</choice>
|
|
<choice correct="false">Finnish</choice>
|
|
<choice correct="true">Marathi</choice>
|
|
<choice correct="true">French</choice>
|
|
<choice correct="false">Hungarian</choice>
|
|
</checkboxgroup>
|
|
</choiceresponse>
|
|
|
|
<p>Note: Make sure you select all of the correct options—there may be more than one!</p>
|
|
|
|
<solution>
|
|
<div class="detailed-solution">
|
|
<p>Explanation</p>
|
|
|
|
<p>Solution for CAPA problem</p>
|
|
|
|
</div>
|
|
</solution>
|
|
|
|
</problem>
|
|
""")
|
|
|
|
sample_dropdown_problem_xml = textwrap.dedent("""
|
|
<problem>
|
|
<p>Dropdown problems allow learners to select only one option from a list of options.</p>
|
|
|
|
<p>Description</p>
|
|
|
|
<p>You can use the following example problem as a model.</p>
|
|
|
|
<p> Which of the following countries celebrates its independence on August 15?</p>
|
|
|
|
|
|
<optionresponse>
|
|
<optioninput options="('India','Spain','China','Bermuda')" correct="India"></optioninput>
|
|
</optionresponse>
|
|
|
|
<solution>
|
|
<div class="detailed-solution">
|
|
<p>Explanation</p>
|
|
|
|
<p> India became an independent nation on August 15, 1947.</p>
|
|
|
|
</div>
|
|
</solution>
|
|
|
|
</problem>
|
|
""")
|
|
|
|
sample_multichoice_problem_xml = textwrap.dedent("""
|
|
<problem>
|
|
<p>Multiple choice problems allow learners to select only one option.</p>
|
|
|
|
<p>When you add the problem, be sure to select Settings to specify a Display Name and other values.</p>
|
|
|
|
<p>You can use the following example problem as a model.</p>
|
|
|
|
<p>Which of the following countries has the largest population?</p>
|
|
<multiplechoiceresponse>
|
|
<choicegroup type="MultipleChoice">
|
|
<choice correct="false">Brazil
|
|
<choicehint>timely feedback -- explain why an almost correct answer is wrong</choicehint>
|
|
</choice>
|
|
<choice correct="false">Germany</choice>
|
|
<choice correct="true">Indonesia</choice>
|
|
<choice correct="false">Russia</choice>
|
|
</choicegroup>
|
|
</multiplechoiceresponse>
|
|
|
|
<solution>
|
|
<div class="detailed-solution">
|
|
<p>Explanation</p>
|
|
|
|
<p>According to September 2014 estimates:</p>
|
|
<p>The population of Indonesia is approximately 250 million.</p>
|
|
<p>The population of Brazil is approximately 200 million.</p>
|
|
<p>The population of Russia is approximately 146 million.</p>
|
|
<p>The population of Germany is approximately 81 million.</p>
|
|
|
|
</div>
|
|
</solution>
|
|
|
|
</problem>
|
|
""")
|
|
|
|
sample_numerical_input_problem_xml = textwrap.dedent("""
|
|
<problem>
|
|
<p>In a numerical input problem, learners enter numbers or a specific and relatively simple mathematical
|
|
expression. Learners enter the response in plain text, and the system then converts the text to a symbolic
|
|
expression that learners can see below the response field.</p>
|
|
|
|
<p>The system can handle several types of characters, including basic operators, fractions, exponents, and
|
|
common constants such as "i". You can refer learners to "Entering Mathematical and Scientific Expressions"
|
|
in the edX Guide for Students for more information.</p>
|
|
|
|
<p>When you add the problem, be sure to select Settings to specify a Display Name and other values that
|
|
apply.</p>
|
|
|
|
<p>You can use the following example problems as models.</p>
|
|
|
|
<p>How many miles away from Earth is the sun? Use scientific notation to answer.</p>
|
|
|
|
<numericalresponse answer="9.3*10^7">
|
|
<formulaequationinput/>
|
|
</numericalresponse>
|
|
|
|
<p>The square of what number is -100?</p>
|
|
|
|
<numericalresponse answer="10*i">
|
|
<formulaequationinput/>
|
|
</numericalresponse>
|
|
|
|
<solution>
|
|
<div class="detailed-solution">
|
|
<p>Explanation</p>
|
|
|
|
<p>The sun is 93,000,000, or 9.3*10^7, miles away from Earth.</p>
|
|
<p>-100 is the square of 10 times the imaginary number, i.</p>
|
|
|
|
</div>
|
|
</solution>
|
|
|
|
</problem>
|
|
""")
|
|
|
|
sample_text_input_problem_xml = textwrap.dedent("""
|
|
<problem>
|
|
<p>In text input problems, also known as "fill-in-the-blank" problems, learners enter text into a response
|
|
field. The text can include letters and characters such as punctuation marks. The text that the learner
|
|
enters must match your specified answer text exactly. You can specify more than one correct answer.
|
|
Learners must enter a response that matches one of the correct answers exactly.</p>
|
|
|
|
<p>When you add the problem, be sure to select Settings to specify a Display Name and other values that
|
|
apply.</p>
|
|
|
|
<p>You can use the following example problem as a model.</p>
|
|
|
|
<p>What was the first post-secondary school in China to allow both male and female students?</p>
|
|
|
|
<stringresponse answer="Nanjing Higher Normal Institute" type="ci" >
|
|
<additional_answer answer="National Central University"></additional_answer>
|
|
<additional_answer answer="Nanjing University"></additional_answer>
|
|
<textline size="20"/>
|
|
</stringresponse>
|
|
|
|
<solution>
|
|
<div class="detailed-solution">
|
|
<p>Explanation</p>
|
|
|
|
<p>Nanjing Higher Normal Institute first admitted female students in 1920.</p>
|
|
|
|
</div>
|
|
</solution>
|
|
|
|
</problem>
|
|
""")
|
|
|
|
sample_checkboxes_with_hints_and_feedback_problem_xml = textwrap.dedent("""
|
|
<problem>
|
|
<p>You can provide feedback for each option in a checkbox problem, with distinct feedback depending on
|
|
whether or not the learner selects that option.</p>
|
|
|
|
<p>You can also provide compound feedback for a specific combination of answers. For example, if you have
|
|
three possible answers in the problem, you can configure specific feedback for when a learner selects each
|
|
combination of possible answers.</p>
|
|
|
|
<p>You can also add hints for learners.</p>
|
|
|
|
<p>Be sure to select Settings to specify a Display Name and other values that apply.</p>
|
|
|
|
<p>Use the following example problem as a model.</p>
|
|
|
|
<p>Which of the following is a fruit? Check all that apply.</p>
|
|
<choiceresponse>
|
|
<checkboxgroup>
|
|
<choice correct="true">apple
|
|
<choicehint selected="true">You are correct that an apple is a fruit because it is the fertilized
|
|
ovary that comes from an apple tree and contains seeds.</choicehint>
|
|
<choicehint selected="false">Remember that an apple is also a fruit.</choicehint></choice>
|
|
<choice correct="true">pumpkin
|
|
<choicehint selected="true">You are correct that a pumpkin is a fruit because it is the fertilized
|
|
ovary of a squash plant and contains seeds.</choicehint>
|
|
<choicehint selected="false">Remember that a pumpkin is also a fruit.</choicehint></choice>
|
|
<choice correct="false">potato
|
|
<choicehint selected="true">A potato is a vegetable, not a fruit, because it does not come from a
|
|
flower and does not contain seeds.</choicehint>
|
|
<choicehint selected="false">You are correct that a potato is a vegetable because it is an edible
|
|
part of a plant in tuber form.</choicehint></choice>
|
|
<choice correct="true">tomato
|
|
<choicehint selected="true">You are correct that a tomato is a fruit because it is the fertilized
|
|
ovary of a tomato plant and contains seeds.</choicehint>
|
|
<choicehint selected="false">Many people mistakenly think a tomato is a vegetable. However, because
|
|
a tomato is the fertilized ovary of a tomato plant and contains seeds, it is a fruit.</choicehint>
|
|
</choice>
|
|
<compoundhint value="A B D">An apple, pumpkin, and tomato are all fruits as they all are fertilized
|
|
ovaries of a plant and contain seeds.</compoundhint>
|
|
<compoundhint value="A B C D">You are correct that an apple, pumpkin, and tomato are all fruits as they
|
|
all are fertilized ovaries of a plant and contain seeds. However, a potato is not a fruit as it is an
|
|
edible part of a plant in tuber form and is a vegetable.</compoundhint>
|
|
</checkboxgroup>
|
|
</choiceresponse>
|
|
|
|
|
|
<demandhint>
|
|
<hint>A fruit is the fertilized ovary from a flower.</hint>
|
|
<hint>A fruit contains seeds of the plant.</hint>
|
|
</demandhint>
|
|
</problem>
|
|
""")
|
|
|
|
sample_dropdown_with_hints_and_feedback_problem_xml = textwrap.dedent("""
|
|
<problem>
|
|
<p>You can provide feedback for each available option in a dropdown problem.</p>
|
|
|
|
<p>You can also add hints for learners.</p>
|
|
|
|
<p>Be sure to select Settings to specify a Display Name and other values that apply.</p>
|
|
|
|
<p>Use the following example problem as a model.</p>
|
|
|
|
<p> A/an ________ is a vegetable.</p>
|
|
<optionresponse>
|
|
<optioninput>
|
|
<option correct="False">apple <optionhint>An apple is the fertilized ovary that comes from an apple
|
|
tree and contains seeds, meaning it is a fruit.</optionhint></option>
|
|
<option correct="False">pumpkin <optionhint>A pumpkin is the fertilized ovary of a squash plant and
|
|
contains seeds, meaning it is a fruit.</optionhint></option>
|
|
<option correct="True">potato <optionhint>A potato is an edible part of a plant in tuber form and is a
|
|
vegetable.</optionhint></option>
|
|
<option correct="False">tomato <optionhint>Many people mistakenly think a tomato is a vegetable.
|
|
However, because a tomato is the fertilized ovary of a tomato plant and contains seeds, it is a fruit.
|
|
</optionhint></option>
|
|
</optioninput>
|
|
</optionresponse>
|
|
|
|
<demandhint>
|
|
<hint>A fruit is the fertilized ovary from a flower.</hint>
|
|
<hint>A fruit contains seeds of the plant.</hint>
|
|
</demandhint>
|
|
</problem>
|
|
""")
|
|
|
|
sample_multichoice_with_hints_and_feedback_problem_xml = textwrap.dedent("""
|
|
<problem>
|
|
<p>You can provide feedback for each option in a multiple choice problem.</p>
|
|
|
|
<p>You can also add hints for learners.</p>
|
|
|
|
<p>Be sure to select Settings to specify a Display Name and other values that apply.</p>
|
|
|
|
<p>Use the following example problem as a model.</p>
|
|
|
|
<p>Which of the following is a vegetable?</p>
|
|
<multiplechoiceresponse>
|
|
<choicegroup type="MultipleChoice">
|
|
<choice correct="false">apple <choicehint>An apple is the fertilized ovary that comes from an apple
|
|
tree and contains seeds, meaning it is a fruit.</choicehint></choice>
|
|
<choice correct="false">pumpkin <choicehint>A pumpkin is the fertilized ovary of a squash plant and
|
|
contains seeds, meaning it is a fruit.</choicehint></choice>
|
|
<choice correct="true">potato <choicehint>A potato is an edible part of a plant in tuber form and is a
|
|
vegetable.</choicehint></choice>
|
|
<choice correct="false">tomato <choicehint>Many people mistakenly think a tomato is a vegetable.
|
|
However, because a tomato is the fertilized ovary of a tomato plant and contains seeds, it is a fruit.
|
|
</choicehint></choice>
|
|
</choicegroup>
|
|
</multiplechoiceresponse>
|
|
|
|
|
|
<demandhint>
|
|
<hint>A fruit is the fertilized ovary from a flower.</hint>
|
|
<hint>A fruit contains seeds of the plant.</hint>
|
|
</demandhint>
|
|
</problem>
|
|
""")
|
|
|
|
sample_numerical_input_with_hints_and_feedback_problem_xml = textwrap.dedent("""
|
|
<problem>
|
|
<p>You can provide feedback for correct answers in numerical input problems. You cannot provide feedback
|
|
for incorrect answers.</p>
|
|
|
|
<p>Use feedback for the correct answer to reinforce the process for arriving at the numerical value.</p>
|
|
|
|
<p>You can also add hints for learners.</p>
|
|
|
|
<p>Be sure to select Settings to specify a Display Name and other values that apply.</p>
|
|
|
|
<p>Use the following example problem as a model.</p>
|
|
|
|
<p>What is the arithmetic mean for the following set of numbers? (1, 5, 6, 3, 5)</p>
|
|
|
|
<numericalresponse answer="4">
|
|
<formulaequationinput/>
|
|
<correcthint>The mean for this set of numbers is 20 / 5, which equals 4.</correcthint>
|
|
</numericalresponse>
|
|
<solution>
|
|
<div class="detailed-solution">
|
|
<p>Explanation</p>
|
|
|
|
<p>The mean is calculated by summing the set of numbers and dividing by n. In this case:
|
|
(1 + 5 + 6 + 3 + 5) / 5 = 20 / 5 = 4.</p>
|
|
|
|
</div>
|
|
</solution>
|
|
|
|
<demandhint>
|
|
<hint>The mean is calculated by summing the set of numbers and dividing by n.</hint>
|
|
<hint>n is the count of items in the set.</hint>
|
|
</demandhint>
|
|
</problem>
|
|
""")
|
|
|
|
sample_text_input_with_hints_and_feedback_problem_xml = textwrap.dedent("""
|
|
<problem>
|
|
<p>You can provide feedback for the correct answer in text input problems, as well as for specific
|
|
incorrect answers.</p>
|
|
|
|
<p>Use feedback on expected incorrect answers to address common misconceptions and to provide guidance on
|
|
how to arrive at the correct answer.</p>
|
|
|
|
<p>Be sure to select Settings to specify a Display Name and other values that apply.</p>
|
|
|
|
<p>Use the following example problem as a model.</p>
|
|
|
|
<p>Which U.S. state has the largest land area?</p>
|
|
|
|
<stringresponse answer="Alaska" type="ci" >
|
|
<correcthint>Alaska is 576,400 square miles, more than double the land area of the second largest state,
|
|
Texas.</correcthint>
|
|
<stringequalhint answer="Texas">While many people think Texas is the largest state, it is actually the
|
|
second largest, with 261,797 square miles.</stringequalhint>
|
|
<stringequalhint answer="California">California is the third largest state, with 155,959 square miles.
|
|
</stringequalhint>
|
|
<textline size="20"/>
|
|
</stringresponse>
|
|
|
|
<demandhint>
|
|
<hint>Consider the square miles, not population.</hint>
|
|
<hint>Consider all 50 states, not just the continental United States.</hint>
|
|
</demandhint>
|
|
</problem>
|
|
""")
|
|
|
|
def _create_descriptor(self, xml, name=None):
|
|
""" Creates a ProblemBlock to run test against """
|
|
descriptor = CapaFactory.create()
|
|
descriptor.data = xml
|
|
if name:
|
|
descriptor.display_name = name
|
|
return descriptor
|
|
|
|
@ddt.data(*responsetypes.registry.registered_tags())
|
|
def test_all_response_types(self, response_tag):
|
|
""" Tests that every registered response tag is correctly returned """
|
|
xml = "<problem><{response_tag}></{response_tag}></problem>".format(response_tag=response_tag)
|
|
name = "Some Capa Problem"
|
|
descriptor = self._create_descriptor(xml, name=name)
|
|
self.assertEquals(descriptor.problem_types, {response_tag})
|
|
self.assertEquals(descriptor.index_dictionary(), {
|
|
'content_type': ProblemBlock.INDEX_CONTENT_TYPE,
|
|
'problem_types': [response_tag],
|
|
'content': {
|
|
'display_name': name,
|
|
'capa_content': ''
|
|
}
|
|
})
|
|
|
|
def test_response_types_ignores_non_response_tags(self):
|
|
xml = textwrap.dedent("""
|
|
<problem>
|
|
<p>Label</p>
|
|
<div>Some comment</div>
|
|
<multiplechoiceresponse>
|
|
<choicegroup type="MultipleChoice" answer-pool="4">
|
|
<choice correct="false">Apple</choice>
|
|
<choice correct="false">Banana</choice>
|
|
<choice correct="false">Chocolate</choice>
|
|
<choice correct ="true">Donut</choice>
|
|
</choicegroup>
|
|
</multiplechoiceresponse>
|
|
</problem>
|
|
""")
|
|
name = "Test Capa Problem"
|
|
descriptor = self._create_descriptor(xml, name=name)
|
|
self.assertEquals(descriptor.problem_types, {"multiplechoiceresponse"})
|
|
self.assertEquals(descriptor.index_dictionary(), {
|
|
'content_type': ProblemBlock.INDEX_CONTENT_TYPE,
|
|
'problem_types': ["multiplechoiceresponse"],
|
|
'content': {
|
|
'display_name': name,
|
|
'capa_content': ' Label Some comment Apple Banana Chocolate Donut '
|
|
}
|
|
})
|
|
|
|
def test_response_types_multiple_tags(self):
|
|
xml = textwrap.dedent("""
|
|
<problem>
|
|
<p>Label</p>
|
|
<div>Some comment</div>
|
|
<multiplechoiceresponse>
|
|
<choicegroup type="MultipleChoice" answer-pool="1">
|
|
<choice correct ="true">Donut</choice>
|
|
</choicegroup>
|
|
</multiplechoiceresponse>
|
|
<multiplechoiceresponse>
|
|
<choicegroup type="MultipleChoice" answer-pool="1">
|
|
<choice correct ="true">Buggy</choice>
|
|
</choicegroup>
|
|
</multiplechoiceresponse>
|
|
<optionresponse>
|
|
<optioninput options="('1','2')" correct="2"></optioninput>
|
|
</optionresponse>
|
|
</problem>
|
|
""")
|
|
name = "Other Test Capa Problem"
|
|
descriptor = self._create_descriptor(xml, name=name)
|
|
self.assertEquals(descriptor.problem_types, {"multiplechoiceresponse", "optionresponse"})
|
|
self.assertEquals(
|
|
descriptor.index_dictionary(), {
|
|
'content_type': ProblemBlock.INDEX_CONTENT_TYPE,
|
|
'problem_types': ["optionresponse", "multiplechoiceresponse"],
|
|
'content': {
|
|
'display_name': name,
|
|
'capa_content': ' Label Some comment Donut Buggy '
|
|
}
|
|
}
|
|
)
|
|
|
|
def test_solutions_not_indexed(self):
|
|
xml = textwrap.dedent("""
|
|
<problem>
|
|
<solution>
|
|
<div class="detailed-solution">
|
|
<p>Explanation</p>
|
|
|
|
<p>This is what the 1st solution.</p>
|
|
|
|
</div>
|
|
</solution>
|
|
|
|
<solution>
|
|
<div class="detailed-solution">
|
|
<p>Explanation</p>
|
|
|
|
<p>This is the 2nd solution.</p>
|
|
|
|
</div>
|
|
</solution>
|
|
|
|
|
|
</problem>
|
|
""")
|
|
name = "Blank Common Capa Problem"
|
|
descriptor = self._create_descriptor(xml, name=name)
|
|
self.assertEquals(
|
|
descriptor.index_dictionary(), {
|
|
'content_type': ProblemBlock.INDEX_CONTENT_TYPE,
|
|
'problem_types': [],
|
|
'content': {
|
|
'display_name': name,
|
|
'capa_content': ' '
|
|
}
|
|
}
|
|
)
|
|
|
|
def test_indexing_checkboxes(self):
|
|
name = "Checkboxes"
|
|
descriptor = self._create_descriptor(self.sample_checkbox_problem_xml, name=name)
|
|
capa_content = textwrap.dedent(u"""
|
|
Title
|
|
Description
|
|
Example
|
|
The following languages are in the Indo-European family:
|
|
Urdu
|
|
Finnish
|
|
Marathi
|
|
French
|
|
Hungarian
|
|
Note: Make sure you select all of the correct options—there may be more than one!
|
|
""")
|
|
self.assertEquals(descriptor.problem_types, {"choiceresponse"})
|
|
self.assertEquals(
|
|
descriptor.index_dictionary(),
|
|
{
|
|
'content_type': ProblemBlock.INDEX_CONTENT_TYPE,
|
|
'problem_types': ["choiceresponse"],
|
|
'content': {
|
|
'display_name': name,
|
|
'capa_content': capa_content.replace("\n", " ")
|
|
}
|
|
}
|
|
)
|
|
|
|
def test_indexing_dropdown(self):
|
|
name = "Dropdown"
|
|
descriptor = self._create_descriptor(self.sample_dropdown_problem_xml, name=name)
|
|
capa_content = textwrap.dedent("""
|
|
Dropdown problems allow learners to select only one option from a list of options.
|
|
Description
|
|
You can use the following example problem as a model.
|
|
Which of the following countries celebrates its independence on August 15?
|
|
""")
|
|
self.assertEquals(descriptor.problem_types, {"optionresponse"})
|
|
self.assertEquals(
|
|
descriptor.index_dictionary(), {
|
|
'content_type': ProblemBlock.INDEX_CONTENT_TYPE,
|
|
'problem_types': ["optionresponse"],
|
|
'content': {
|
|
'display_name': name,
|
|
'capa_content': capa_content.replace("\n", " ")
|
|
}
|
|
}
|
|
)
|
|
|
|
def test_indexing_multiple_choice(self):
|
|
name = "Multiple Choice"
|
|
descriptor = self._create_descriptor(self.sample_multichoice_problem_xml, name=name)
|
|
capa_content = textwrap.dedent("""
|
|
Multiple choice problems allow learners to select only one option.
|
|
When you add the problem, be sure to select Settings to specify a Display Name and other values.
|
|
You can use the following example problem as a model.
|
|
Which of the following countries has the largest population?
|
|
Brazil
|
|
Germany
|
|
Indonesia
|
|
Russia
|
|
""")
|
|
self.assertEquals(descriptor.problem_types, {"multiplechoiceresponse"})
|
|
self.assertEquals(
|
|
descriptor.index_dictionary(), {
|
|
'content_type': ProblemBlock.INDEX_CONTENT_TYPE,
|
|
'problem_types': ["multiplechoiceresponse"],
|
|
'content': {
|
|
'display_name': name,
|
|
'capa_content': capa_content.replace("\n", " ")
|
|
}
|
|
}
|
|
)
|
|
|
|
def test_indexing_numerical_input(self):
|
|
name = "Numerical Input"
|
|
descriptor = self._create_descriptor(self.sample_numerical_input_problem_xml, name=name)
|
|
capa_content = textwrap.dedent("""
|
|
In a numerical input problem, learners enter numbers or a specific and relatively simple mathematical
|
|
expression. Learners enter the response in plain text, and the system then converts the text to a symbolic
|
|
expression that learners can see below the response field.
|
|
The system can handle several types of characters, including basic operators, fractions, exponents, and
|
|
common constants such as "i". You can refer learners to "Entering Mathematical and Scientific Expressions"
|
|
in the edX Guide for Students for more information.
|
|
When you add the problem, be sure to select Settings to specify a Display Name and other values that
|
|
apply.
|
|
You can use the following example problems as models.
|
|
How many miles away from Earth is the sun? Use scientific notation to answer.
|
|
The square of what number is -100?
|
|
""")
|
|
self.assertEquals(descriptor.problem_types, {"numericalresponse"})
|
|
self.assertEquals(
|
|
descriptor.index_dictionary(), {
|
|
'content_type': ProblemBlock.INDEX_CONTENT_TYPE,
|
|
'problem_types': ["numericalresponse"],
|
|
'content': {
|
|
'display_name': name,
|
|
'capa_content': capa_content.replace("\n", " ")
|
|
}
|
|
}
|
|
)
|
|
|
|
def test_indexing_text_input(self):
|
|
name = "Text Input"
|
|
descriptor = self._create_descriptor(self.sample_text_input_problem_xml, name=name)
|
|
capa_content = textwrap.dedent("""
|
|
In text input problems, also known as "fill-in-the-blank" problems, learners enter text into a response
|
|
field. The text can include letters and characters such as punctuation marks. The text that the learner
|
|
enters must match your specified answer text exactly. You can specify more than one correct answer.
|
|
Learners must enter a response that matches one of the correct answers exactly.
|
|
When you add the problem, be sure to select Settings to specify a Display Name and other values that
|
|
apply.
|
|
You can use the following example problem as a model.
|
|
What was the first post-secondary school in China to allow both male and female students?
|
|
""")
|
|
self.assertEquals(descriptor.problem_types, {"stringresponse"})
|
|
self.assertEquals(
|
|
descriptor.index_dictionary(), {
|
|
'content_type': ProblemBlock.INDEX_CONTENT_TYPE,
|
|
'problem_types': ["stringresponse"],
|
|
'content': {
|
|
'display_name': name,
|
|
'capa_content': capa_content.replace("\n", " ")
|
|
}
|
|
}
|
|
)
|
|
|
|
def test_indexing_non_latin_problem(self):
|
|
sample_text_input_problem_xml = textwrap.dedent("""
|
|
<problem>
|
|
<script type="text/python">FX1_VAL='Καλημέρα'</script>
|
|
<p>Δοκιμή με μεταβλητές με Ελληνικούς χαρακτήρες μέσα σε python: $FX1_VAL</p>
|
|
</problem>
|
|
""")
|
|
name = "Non latin Input"
|
|
descriptor = self._create_descriptor(sample_text_input_problem_xml, name=name)
|
|
capa_content = " FX1_VAL='Καλημέρα' Δοκιμή με μεταβλητές με Ελληνικούς χαρακτήρες μέσα σε python: $FX1_VAL "
|
|
|
|
descriptor_dict = descriptor.index_dictionary()
|
|
self.assertEquals(
|
|
descriptor_dict['content']['capa_content'], smart_text(capa_content)
|
|
)
|
|
|
|
def test_indexing_checkboxes_with_hints_and_feedback(self):
|
|
name = "Checkboxes with Hints and Feedback"
|
|
descriptor = self._create_descriptor(self.sample_checkboxes_with_hints_and_feedback_problem_xml, name=name)
|
|
capa_content = textwrap.dedent("""
|
|
You can provide feedback for each option in a checkbox problem, with distinct feedback depending on
|
|
whether or not the learner selects that option.
|
|
You can also provide compound feedback for a specific combination of answers. For example, if you have
|
|
three possible answers in the problem, you can configure specific feedback for when a learner selects each
|
|
combination of possible answers.
|
|
You can also add hints for learners.
|
|
Be sure to select Settings to specify a Display Name and other values that apply.
|
|
Use the following example problem as a model.
|
|
Which of the following is a fruit? Check all that apply.
|
|
apple
|
|
pumpkin
|
|
potato
|
|
tomato
|
|
""")
|
|
self.assertEquals(descriptor.problem_types, {"choiceresponse"})
|
|
self.assertEquals(
|
|
descriptor.index_dictionary(), {
|
|
'content_type': ProblemBlock.INDEX_CONTENT_TYPE,
|
|
'problem_types': ["choiceresponse"],
|
|
'content': {
|
|
'display_name': name,
|
|
'capa_content': capa_content.replace("\n", " ")
|
|
}
|
|
}
|
|
)
|
|
|
|
def test_indexing_dropdown_with_hints_and_feedback(self):
|
|
name = "Dropdown with Hints and Feedback"
|
|
descriptor = self._create_descriptor(self.sample_dropdown_with_hints_and_feedback_problem_xml, name=name)
|
|
capa_content = textwrap.dedent("""
|
|
You can provide feedback for each available option in a dropdown problem.
|
|
You can also add hints for learners.
|
|
Be sure to select Settings to specify a Display Name and other values that apply.
|
|
Use the following example problem as a model.
|
|
A/an ________ is a vegetable.
|
|
apple
|
|
pumpkin
|
|
potato
|
|
tomato
|
|
""")
|
|
self.assertEquals(descriptor.problem_types, {"optionresponse"})
|
|
self.assertEquals(
|
|
descriptor.index_dictionary(), {
|
|
'content_type': ProblemBlock.INDEX_CONTENT_TYPE,
|
|
'problem_types': ["optionresponse"],
|
|
'content': {
|
|
'display_name': name,
|
|
'capa_content': capa_content.replace("\n", " ")
|
|
}
|
|
}
|
|
)
|
|
|
|
def test_indexing_multiple_choice_with_hints_and_feedback(self):
|
|
name = "Multiple Choice with Hints and Feedback"
|
|
descriptor = self._create_descriptor(self.sample_multichoice_with_hints_and_feedback_problem_xml, name=name)
|
|
capa_content = textwrap.dedent("""
|
|
You can provide feedback for each option in a multiple choice problem.
|
|
You can also add hints for learners.
|
|
Be sure to select Settings to specify a Display Name and other values that apply.
|
|
Use the following example problem as a model.
|
|
Which of the following is a vegetable?
|
|
apple
|
|
pumpkin
|
|
potato
|
|
tomato
|
|
""")
|
|
self.assertEquals(descriptor.problem_types, {"multiplechoiceresponse"})
|
|
self.assertEquals(
|
|
descriptor.index_dictionary(), {
|
|
'content_type': ProblemBlock.INDEX_CONTENT_TYPE,
|
|
'problem_types': ["multiplechoiceresponse"],
|
|
'content': {
|
|
'display_name': name,
|
|
'capa_content': capa_content.replace("\n", " ")
|
|
}
|
|
}
|
|
)
|
|
|
|
def test_indexing_numerical_input_with_hints_and_feedback(self):
|
|
name = "Numerical Input with Hints and Feedback"
|
|
descriptor = self._create_descriptor(self.sample_numerical_input_with_hints_and_feedback_problem_xml, name=name)
|
|
capa_content = textwrap.dedent("""
|
|
You can provide feedback for correct answers in numerical input problems. You cannot provide feedback
|
|
for incorrect answers.
|
|
Use feedback for the correct answer to reinforce the process for arriving at the numerical value.
|
|
You can also add hints for learners.
|
|
Be sure to select Settings to specify a Display Name and other values that apply.
|
|
Use the following example problem as a model.
|
|
What is the arithmetic mean for the following set of numbers? (1, 5, 6, 3, 5)
|
|
""")
|
|
self.assertEquals(descriptor.problem_types, {"numericalresponse"})
|
|
self.assertEquals(
|
|
descriptor.index_dictionary(), {
|
|
'content_type': ProblemBlock.INDEX_CONTENT_TYPE,
|
|
'problem_types': ["numericalresponse"],
|
|
'content': {
|
|
'display_name': name,
|
|
'capa_content': capa_content.replace("\n", " ")
|
|
}
|
|
}
|
|
)
|
|
|
|
def test_indexing_text_input_with_hints_and_feedback(self):
|
|
name = "Text Input with Hints and Feedback"
|
|
descriptor = self._create_descriptor(self.sample_text_input_with_hints_and_feedback_problem_xml, name=name)
|
|
capa_content = textwrap.dedent("""
|
|
You can provide feedback for the correct answer in text input problems, as well as for specific
|
|
incorrect answers.
|
|
Use feedback on expected incorrect answers to address common misconceptions and to provide guidance on
|
|
how to arrive at the correct answer.
|
|
Be sure to select Settings to specify a Display Name and other values that apply.
|
|
Use the following example problem as a model.
|
|
Which U.S. state has the largest land area?
|
|
""")
|
|
self.assertEquals(descriptor.problem_types, {"stringresponse"})
|
|
self.assertEquals(
|
|
descriptor.index_dictionary(), {
|
|
'content_type': ProblemBlock.INDEX_CONTENT_TYPE,
|
|
'problem_types': ["stringresponse"],
|
|
'content': {
|
|
'display_name': name,
|
|
'capa_content': capa_content.replace("\n", " ")
|
|
}
|
|
}
|
|
)
|
|
|
|
def test_indexing_problem_with_html_tags(self):
|
|
sample_problem_xml = textwrap.dedent("""
|
|
<problem>
|
|
<style>p {left: 10px;}</style>
|
|
<!-- Beginning of the html -->
|
|
<p>This has HTML comment in it.<!-- Commenting Content --></p>
|
|
<!-- Here comes CDATA -->
|
|
<![CDATA[This is just a CDATA!]]>
|
|
<p>HTML end.</p>
|
|
<!-- Script that makes everything alive! -->
|
|
<script>
|
|
var alive;
|
|
</script>
|
|
</problem>
|
|
""")
|
|
name = "Mixed business"
|
|
descriptor = self._create_descriptor(sample_problem_xml, name=name)
|
|
capa_content = textwrap.dedent("""
|
|
This has HTML comment in it.
|
|
HTML end.
|
|
""")
|
|
self.assertEquals(
|
|
descriptor.index_dictionary(), {
|
|
'content_type': ProblemBlock.INDEX_CONTENT_TYPE,
|
|
'problem_types': [],
|
|
'content': {
|
|
'display_name': name,
|
|
'capa_content': capa_content.replace("\n", " ")
|
|
}
|
|
}
|
|
)
|
|
|
|
def test_invalid_xml_handling(self):
|
|
"""
|
|
Tests to confirm that invalid XML throws errors during xblock creation,
|
|
so as not to allow bad data into modulestore.
|
|
"""
|
|
sample_invalid_xml = textwrap.dedent("""
|
|
<problem>
|
|
</proble-oh no my finger broke and I can't close the problem tag properly...
|
|
""")
|
|
with self.assertRaises(etree.XMLSyntaxError):
|
|
self._create_descriptor(sample_invalid_xml, name="Invalid XML")
|
|
|
|
|
|
class ComplexEncoderTest(unittest.TestCase):
|
|
|
|
def test_default(self):
|
|
"""
|
|
Check that complex numbers can be encoded into JSON.
|
|
"""
|
|
complex_num = 1 - 1j
|
|
expected_str = '1-1*j'
|
|
json_str = json.dumps(complex_num, cls=ComplexEncoder)
|
|
self.assertEqual(expected_str, json_str[1:-1]) # ignore quotes
|
|
|
|
|
|
class ProblemCheckTrackingTest(unittest.TestCase):
|
|
"""
|
|
Ensure correct tracking information is included in events emitted during problem checks.
|
|
"""
|
|
|
|
def setUp(self):
|
|
super(ProblemCheckTrackingTest, self).setUp()
|
|
self.maxDiff = None
|
|
|
|
def test_choice_answer_text(self):
|
|
xml = """\
|
|
<problem display_name="Multiple Choice Questions">
|
|
<optionresponse>
|
|
<label>What color is the open ocean on a sunny day?</label>
|
|
<optioninput options="('yellow','blue','green')" correct="blue"/>
|
|
</optionresponse>
|
|
|
|
<multiplechoiceresponse>
|
|
<label>Which piece of furniture is built for sitting?</label>
|
|
<choicegroup type="MultipleChoice">
|
|
<choice correct="false"><text>a table</text></choice>
|
|
<choice correct="false"><text>a desk</text></choice>
|
|
<choice correct="true"><text>a chair</text></choice>
|
|
<choice correct="false"><text>a bookshelf</text></choice>
|
|
</choicegroup>
|
|
</multiplechoiceresponse>
|
|
|
|
<choiceresponse>
|
|
<label>Which of the following are musical instruments?</label>
|
|
<checkboxgroup>
|
|
<choice correct="true">a piano</choice>
|
|
<choice correct="false">a tree</choice>
|
|
<choice correct="true">a guitar</choice>
|
|
<choice correct="false">a window</choice>
|
|
</checkboxgroup>
|
|
</choiceresponse>
|
|
</problem>
|
|
"""
|
|
|
|
# Whitespace screws up comparisons
|
|
xml = ''.join(line.strip() for line in xml.split('\n'))
|
|
factory = self.capa_factory_for_problem_xml(xml)
|
|
module = factory.create()
|
|
|
|
answer_input_dict = {
|
|
factory.input_key(2): 'blue',
|
|
factory.input_key(3): 'choice_0',
|
|
factory.input_key(4): ['choice_0', 'choice_1'],
|
|
}
|
|
event = self.get_event_for_answers(module, answer_input_dict)
|
|
|
|
self.assertEquals(event['submission'], {
|
|
factory.answer_key(2): {
|
|
'question': 'What color is the open ocean on a sunny day?',
|
|
'answer': 'blue',
|
|
'response_type': 'optionresponse',
|
|
'input_type': 'optioninput',
|
|
'correct': True,
|
|
'group_label': '',
|
|
'variant': '',
|
|
},
|
|
factory.answer_key(3): {
|
|
'question': 'Which piece of furniture is built for sitting?',
|
|
'answer': u'<text>a table</text>',
|
|
'response_type': 'multiplechoiceresponse',
|
|
'input_type': 'choicegroup',
|
|
'correct': False,
|
|
'group_label': '',
|
|
'variant': '',
|
|
},
|
|
factory.answer_key(4): {
|
|
'question': 'Which of the following are musical instruments?',
|
|
'answer': [u'a piano', u'a tree'],
|
|
'response_type': 'choiceresponse',
|
|
'input_type': 'checkboxgroup',
|
|
'correct': False,
|
|
'group_label': '',
|
|
'variant': '',
|
|
},
|
|
})
|
|
|
|
def capa_factory_for_problem_xml(self, xml):
|
|
class CustomCapaFactory(CapaFactory):
|
|
"""
|
|
A factory for creating a Capa problem with arbitrary xml.
|
|
"""
|
|
sample_problem_xml = textwrap.dedent(xml)
|
|
|
|
return CustomCapaFactory
|
|
|
|
def get_event_for_answers(self, module, answer_input_dict):
|
|
with patch.object(module.runtime, 'publish') as mock_track_function:
|
|
module.submit_problem(answer_input_dict)
|
|
|
|
self.assertGreaterEqual(len(mock_track_function.mock_calls), 2)
|
|
# There are potentially 2 track logs: answers and hint. [-1]=answers.
|
|
mock_call = mock_track_function.mock_calls[-1]
|
|
event = mock_call[1][2]
|
|
|
|
return event
|
|
|
|
def test_numerical_textline(self):
|
|
factory = CapaFactory
|
|
module = factory.create()
|
|
|
|
answer_input_dict = {
|
|
factory.input_key(2): '3.14'
|
|
}
|
|
|
|
event = self.get_event_for_answers(module, answer_input_dict)
|
|
self.assertEquals(event['submission'], {
|
|
factory.answer_key(2): {
|
|
'question': '',
|
|
'answer': '3.14',
|
|
'response_type': 'numericalresponse',
|
|
'input_type': 'textline',
|
|
'correct': True,
|
|
'group_label': '',
|
|
'variant': '',
|
|
}
|
|
})
|
|
|
|
def test_multiple_inputs(self):
|
|
group_label = 'Choose the correct color'
|
|
input1_label = 'What color is the sky?'
|
|
input2_label = 'What color are pine needles?'
|
|
factory = self.capa_factory_for_problem_xml("""\
|
|
<problem display_name="Multiple Inputs">
|
|
<optionresponse>
|
|
<label>{}</label>
|
|
<optioninput options="('yellow','blue','green')" correct="blue" label="{}"/>
|
|
<optioninput options="('yellow','blue','green')" correct="green" label="{}"/>
|
|
</optionresponse>
|
|
</problem>
|
|
""".format(group_label, input1_label, input2_label))
|
|
module = factory.create()
|
|
answer_input_dict = {
|
|
factory.input_key(2, 1): 'blue',
|
|
factory.input_key(2, 2): 'yellow',
|
|
}
|
|
|
|
event = self.get_event_for_answers(module, answer_input_dict)
|
|
self.assertEquals(event['submission'], {
|
|
factory.answer_key(2, 1): {
|
|
'group_label': group_label,
|
|
'question': input1_label,
|
|
'answer': 'blue',
|
|
'response_type': 'optionresponse',
|
|
'input_type': 'optioninput',
|
|
'correct': True,
|
|
'variant': '',
|
|
},
|
|
factory.answer_key(2, 2): {
|
|
'group_label': group_label,
|
|
'question': input2_label,
|
|
'answer': 'yellow',
|
|
'response_type': 'optionresponse',
|
|
'input_type': 'optioninput',
|
|
'correct': False,
|
|
'variant': '',
|
|
},
|
|
})
|
|
|
|
def test_optioninput_extended_xml(self):
|
|
"""Test the new XML form of writing with <option> tag instead of options= attribute."""
|
|
group_label = 'Are you the Gatekeeper?'
|
|
input1_label = 'input 1 label'
|
|
input2_label = 'input 2 label'
|
|
factory = self.capa_factory_for_problem_xml("""\
|
|
<problem display_name="Woo Hoo">
|
|
<optionresponse>
|
|
<label>{}</label>
|
|
<optioninput label="{}">
|
|
<option correct="True" label="Good Job">
|
|
apple
|
|
<optionhint>
|
|
banana
|
|
</optionhint>
|
|
</option>
|
|
<option correct="False" label="blorp">
|
|
cucumber
|
|
<optionhint>
|
|
donut
|
|
</optionhint>
|
|
</option>
|
|
</optioninput>
|
|
|
|
<optioninput label="{}">
|
|
<option correct="True">
|
|
apple
|
|
<optionhint>
|
|
banana
|
|
</optionhint>
|
|
</option>
|
|
<option correct="False">
|
|
cucumber
|
|
<optionhint>
|
|
donut
|
|
</optionhint>
|
|
</option>
|
|
</optioninput>
|
|
</optionresponse>
|
|
</problem>
|
|
""".format(group_label, input1_label, input2_label))
|
|
module = factory.create()
|
|
|
|
answer_input_dict = {
|
|
factory.input_key(2, 1): 'apple',
|
|
factory.input_key(2, 2): 'cucumber',
|
|
}
|
|
|
|
event = self.get_event_for_answers(module, answer_input_dict)
|
|
self.assertEquals(event['submission'], {
|
|
factory.answer_key(2, 1): {
|
|
'group_label': group_label,
|
|
'question': input1_label,
|
|
'answer': 'apple',
|
|
'response_type': 'optionresponse',
|
|
'input_type': 'optioninput',
|
|
'correct': True,
|
|
'variant': '',
|
|
},
|
|
factory.answer_key(2, 2): {
|
|
'group_label': group_label,
|
|
'question': input2_label,
|
|
'answer': 'cucumber',
|
|
'response_type': 'optionresponse',
|
|
'input_type': 'optioninput',
|
|
'correct': False,
|
|
'variant': '',
|
|
},
|
|
})
|
|
|
|
def test_rerandomized_inputs(self):
|
|
factory = CapaFactory
|
|
module = factory.create(rerandomize=RANDOMIZATION.ALWAYS)
|
|
|
|
answer_input_dict = {
|
|
factory.input_key(2): '3.14'
|
|
}
|
|
|
|
event = self.get_event_for_answers(module, answer_input_dict)
|
|
self.assertEquals(event['submission'], {
|
|
factory.answer_key(2): {
|
|
'question': '',
|
|
'answer': '3.14',
|
|
'response_type': 'numericalresponse',
|
|
'input_type': 'textline',
|
|
'correct': True,
|
|
'group_label': '',
|
|
'variant': module.seed,
|
|
}
|
|
})
|
|
|
|
def test_file_inputs(self):
|
|
fnames = ["prog1.py", "prog2.py", "prog3.py"]
|
|
fpaths = [os.path.join(DATA_DIR, "capa", fname) for fname in fnames]
|
|
fileobjs = [open(fpath) for fpath in fpaths]
|
|
for fileobj in fileobjs:
|
|
self.addCleanup(fileobj.close)
|
|
|
|
factory = CapaFactoryWithFiles
|
|
module = factory.create()
|
|
|
|
# Mock the XQueueInterface.
|
|
xqueue_interface = XQueueInterface("http://example.com/xqueue", Mock())
|
|
xqueue_interface._http_post = Mock(return_value=(0, "ok")) # pylint: disable=protected-access
|
|
module.system.xqueue['interface'] = xqueue_interface
|
|
|
|
answer_input_dict = {
|
|
CapaFactoryWithFiles.input_key(response_num=2): fileobjs,
|
|
CapaFactoryWithFiles.input_key(response_num=3): 'None',
|
|
}
|
|
|
|
event = self.get_event_for_answers(module, answer_input_dict)
|
|
self.assertEquals(event['submission'], {
|
|
factory.answer_key(2): {
|
|
'question': '',
|
|
'answer': fpaths,
|
|
'response_type': 'coderesponse',
|
|
'input_type': 'filesubmission',
|
|
'correct': False,
|
|
'group_label': '',
|
|
'variant': '',
|
|
},
|
|
factory.answer_key(3): {
|
|
'answer': 'None',
|
|
'correct': True,
|
|
'group_label': '',
|
|
'question': '',
|
|
'response_type': 'customresponse',
|
|
'input_type': 'textline',
|
|
'variant': ''
|
|
}
|
|
})
|
|
|
|
def test_get_answer_with_jump_to_id_urls(self):
|
|
"""
|
|
Make sure replace_jump_to_id_urls() is called in get_answer.
|
|
"""
|
|
problem_xml = textwrap.dedent("""
|
|
<problem>
|
|
<p>What is 1+4?</p>
|
|
<numericalresponse answer="5">
|
|
<formulaequationinput />
|
|
</numericalresponse>
|
|
|
|
<solution>
|
|
<div class="detailed-solution">
|
|
<p>Explanation</p>
|
|
<a href="/jump_to_id/c0f8d54964bc44a4a1deb8ecce561ecd">here's the same link to the hint page.</a>
|
|
</div>
|
|
</solution>
|
|
</problem>
|
|
""")
|
|
|
|
data = dict()
|
|
problem = CapaFactory.create(showanswer='always', xml=problem_xml)
|
|
problem.runtime.replace_jump_to_id_urls = Mock()
|
|
problem.get_answer(data)
|
|
self.assertTrue(problem.runtime.replace_jump_to_id_urls.called)
|
|
|
|
|
|
class ProblemBlockReportGenerationTest(unittest.TestCase):
|
|
"""
|
|
Ensure that Capa report generation works correctly
|
|
"""
|
|
|
|
def setUp(self):
|
|
self.find_question_label_patcher = patch(
|
|
'capa.capa_problem.LoncapaProblem.find_question_label',
|
|
lambda self, answer_id: answer_id
|
|
)
|
|
self.find_answer_text_patcher = patch(
|
|
'capa.capa_problem.LoncapaProblem.find_answer_text',
|
|
lambda self, answer_id, current_answer: current_answer
|
|
)
|
|
self.find_question_label_patcher.start()
|
|
self.find_answer_text_patcher.start()
|
|
self.addCleanup(self.find_question_label_patcher.stop)
|
|
self.addCleanup(self.find_answer_text_patcher.stop)
|
|
|
|
def _mock_user_state_generator(self, user_count=1, response_count=10):
|
|
for uid in range(user_count):
|
|
yield self._user_state(username='user{}'.format(uid), response_count=response_count)
|
|
|
|
def _user_state(self, username='testuser', response_count=10, suffix=''):
|
|
return XBlockUserState(
|
|
username=username,
|
|
state={
|
|
'student_answers': {
|
|
'{}_answerid_{}{}'.format(username, aid, suffix): '{}_answer_{}'.format(username, aid)
|
|
for aid in range(response_count)
|
|
},
|
|
'seed': 1,
|
|
'correct_map': {},
|
|
},
|
|
block_key=None,
|
|
updated=None,
|
|
scope=None,
|
|
)
|
|
|
|
def _get_descriptor(self):
|
|
scope_ids = Mock(block_type='problem')
|
|
descriptor = ProblemBlock(get_test_system(), scope_ids=scope_ids)
|
|
descriptor.runtime = Mock()
|
|
descriptor.data = '<problem/>'
|
|
return descriptor
|
|
|
|
def test_generate_report_data_not_implemented(self):
|
|
scope_ids = Mock(block_type='noproblem')
|
|
descriptor = ProblemBlock(get_test_system(), scope_ids=scope_ids)
|
|
with self.assertRaises(NotImplementedError):
|
|
next(descriptor.generate_report_data(iter([])))
|
|
|
|
def test_generate_report_data_limit_responses(self):
|
|
descriptor = self._get_descriptor()
|
|
report_data = list(descriptor.generate_report_data(self._mock_user_state_generator(), 2))
|
|
self.assertEquals(2, len(report_data))
|
|
|
|
def test_generate_report_data_dont_limit_responses(self):
|
|
descriptor = self._get_descriptor()
|
|
user_count = 5
|
|
response_count = 10
|
|
report_data = list(descriptor.generate_report_data(
|
|
self._mock_user_state_generator(
|
|
user_count=user_count,
|
|
response_count=response_count,
|
|
)
|
|
))
|
|
self.assertEquals(user_count * response_count, len(report_data))
|
|
|
|
def test_generate_report_data_skip_dynamath(self):
|
|
descriptor = self._get_descriptor()
|
|
iterator = iter([self._user_state(suffix='_dynamath')])
|
|
report_data = list(descriptor.generate_report_data(iterator))
|
|
self.assertEquals(0, len(report_data))
|