Files
edx-platform/xmodule/tests/test_delay_between_attempts.py

307 lines
12 KiB
Python

"""
Tests the logic of problems with a delay between attempt submissions.
Note that this test file is based off of test_capa_block.py and as
such, uses the same CapaFactory problem setup to test the functionality
of the submit_problem method of a capa block when the "delay between quiz
submissions" setting is set to different values
"""
import datetime
import textwrap
import unittest
from unittest.mock import Mock
import pytest
from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator
from pytz import UTC
from xblock.field_data import DictFieldData
from xblock.fields import ScopeIds
from xblock.scorable import Score
import xmodule
from xmodule.capa_block import ProblemBlock
from . import get_test_system
class CapaFactoryWithDelay:
"""
Create problem blocks class, specialized for delay_between_attempts
test cases. This factory seems different enough from the one in
test_capa_block that unifying them is unattractive.
Removed the unused optional arguments.
"""
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):
"""
Return the next cls number
"""
cls.num += 1
return cls.num
@classmethod
def input_key(cls, input_num=2):
"""
Return the input key to use when passing GET parameters
"""
return "input_" + cls.answer_key(input_num)
@classmethod
def answer_key(cls, input_num=2):
"""
Return the key stored in the capa problem answer dict
"""
return (
"%s_%d_1" % (
"-".join(['i4x', 'edX', 'capa_test', 'problem', 'SampleProblem%d' % cls.num]),
input_num,
)
)
@classmethod
def create(
cls,
max_attempts=None,
attempts=None,
correct=False,
last_submission_time=None,
submission_wait_seconds=None
):
"""
Optional parameters here are cut down to what we actually use vs. the regular CapaFactory.
"""
location = BlockUsageLocator(CourseLocator('edX', 'capa_test', 'run', deprecated=True),
'problem', f'SampleProblem{cls.next_num()}', deprecated=True)
field_data = {'data': cls.sample_problem_xml}
if max_attempts is not None:
field_data['max_attempts'] = max_attempts
if last_submission_time is not None:
field_data['last_submission_time'] = last_submission_time
if submission_wait_seconds is not None:
field_data['submission_wait_seconds'] = submission_wait_seconds
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(render_template=Mock(return_value="<div>Test Template HTML</div>"))
block = ProblemBlock(
system,
DictFieldData(field_data),
ScopeIds(None, None, location, location),
)
if correct:
# Could set the internal state formally, but here we just jam in the score.
block.score = Score(raw_earned=1, raw_possible=1)
else:
block.score = Score(raw_earned=0, raw_possible=1)
return block
class XModuleQuizAttemptsDelayTest(unittest.TestCase):
"""
Class to test delay between quiz attempts.
"""
def create_and_check(self,
num_attempts=None,
last_submission_time=None,
submission_wait_seconds=None,
considered_now=None,
skip_submit_problem=False):
"""Unified create and check code for the tests here."""
block = CapaFactoryWithDelay.create(
attempts=num_attempts,
max_attempts=99,
last_submission_time=last_submission_time,
submission_wait_seconds=submission_wait_seconds
)
block.done = False
get_request_dict = {CapaFactoryWithDelay.input_key(): "3.14"}
if skip_submit_problem:
return (block, None)
if considered_now is not None:
result = block.submit_problem(get_request_dict, considered_now)
else:
result = block.submit_problem(get_request_dict)
return (block, result)
def test_first_submission(self):
# Not attempted yet
num_attempts = 0
(block, result) = self.create_and_check(
num_attempts=num_attempts,
last_submission_time=None
)
# Successfully submitted and answered
# Also, the number of attempts should increment by 1
assert result['success'] == 'correct'
assert block.attempts == (num_attempts + 1)
def test_no_wait_time(self):
num_attempts = 1
(block, result) = self.create_and_check(
num_attempts=num_attempts,
last_submission_time=datetime.datetime.now(UTC),
submission_wait_seconds=0
)
# Successfully submitted and answered
# Also, the number of attempts should increment by 1
assert result['success'] == 'correct'
assert block.attempts == (num_attempts + 1)
def test_submit_quiz_in_rapid_succession(self):
# Already attempted once (just now) and thus has a submitted time
num_attempts = 1
(block, result) = self.create_and_check(
num_attempts=num_attempts,
last_submission_time=datetime.datetime.now(UTC),
submission_wait_seconds=123
)
# You should get a dialog that tells you to wait
# Also, the number of attempts should not be incremented
self.assertRegex(result['success'], r"You must wait at least.*")
assert block.attempts == num_attempts
def test_submit_quiz_too_soon(self):
# Already attempted once (just now)
num_attempts = 1
(block, result) = self.create_and_check(
num_attempts=num_attempts,
last_submission_time=datetime.datetime(2013, 12, 6, 0, 17, 36, tzinfo=UTC),
submission_wait_seconds=180,
considered_now=datetime.datetime(2013, 12, 6, 0, 18, 36, tzinfo=UTC)
)
# You should get a dialog that tells you to wait 2 minutes
# Also, the number of attempts should not be incremented
self.assertRegex(result['success'], r"You must wait at least 3 minutes between submissions. 2 minutes remaining\..*") # lint-amnesty, pylint: disable=line-too-long
assert block.attempts == num_attempts
def test_submit_quiz_1_second_too_soon(self):
# Already attempted once (just now)
num_attempts = 1
(block, result) = self.create_and_check(
num_attempts=num_attempts,
last_submission_time=datetime.datetime(2013, 12, 6, 0, 17, 36, tzinfo=UTC),
submission_wait_seconds=180,
considered_now=datetime.datetime(2013, 12, 6, 0, 20, 35, tzinfo=UTC)
)
# You should get a dialog that tells you to wait 2 minutes
# Also, the number of attempts should not be incremented
self.assertRegex(result['success'], r"You must wait at least 3 minutes between submissions. 1 second remaining\..*") # lint-amnesty, pylint: disable=line-too-long
assert block.attempts == num_attempts
def test_submit_quiz_as_soon_as_allowed(self):
# Already attempted once (just now)
num_attempts = 1
(block, result) = self.create_and_check(
num_attempts=num_attempts,
last_submission_time=datetime.datetime(2013, 12, 6, 0, 17, 36, tzinfo=UTC),
submission_wait_seconds=180,
considered_now=datetime.datetime(2013, 12, 6, 0, 20, 36, tzinfo=UTC)
)
# Successfully submitted and answered
# Also, the number of attempts should increment by 1
assert result['success'] == 'correct'
assert block.attempts == (num_attempts + 1)
def test_submit_quiz_after_delay_expired(self):
# Already attempted once (just now)
num_attempts = 1
(block, result) = self.create_and_check(
num_attempts=num_attempts,
last_submission_time=datetime.datetime(2013, 12, 6, 0, 17, 36, tzinfo=UTC),
submission_wait_seconds=180,
considered_now=datetime.datetime(2013, 12, 6, 0, 24, 0, tzinfo=UTC)
)
# Successfully submitted and answered
# Also, the number of attempts should increment by 1
assert result['success'] == 'correct'
assert block.attempts == (num_attempts + 1)
def test_still_cannot_submit_after_max_attempts(self):
# Already attempted once (just now) and thus has a submitted time
num_attempts = 99
# Regular create_and_check should fail
with pytest.raises(xmodule.exceptions.NotFoundError):
(block, unused_result) = self.create_and_check(
num_attempts=num_attempts,
last_submission_time=datetime.datetime(2013, 12, 6, 0, 17, 36, tzinfo=UTC),
submission_wait_seconds=180,
considered_now=datetime.datetime(2013, 12, 6, 0, 24, 0, tzinfo=UTC)
)
# Now try it without the submit_problem
(block, unused_result) = self.create_and_check(
num_attempts=num_attempts,
last_submission_time=datetime.datetime(2013, 12, 6, 0, 17, 36, tzinfo=UTC),
submission_wait_seconds=180,
considered_now=datetime.datetime(2013, 12, 6, 0, 24, 0, tzinfo=UTC),
skip_submit_problem=True
)
# Expect that number of attempts NOT incremented
assert block.attempts == num_attempts
def test_submit_quiz_with_long_delay(self):
# Already attempted once (just now)
num_attempts = 1
(block, result) = self.create_and_check(
num_attempts=num_attempts,
last_submission_time=datetime.datetime(2013, 12, 6, 0, 17, 36, tzinfo=UTC),
submission_wait_seconds=60 * 60 * 2,
considered_now=datetime.datetime(2013, 12, 6, 2, 15, 35, tzinfo=UTC)
)
# You should get a dialog that tells you to wait 2 minutes
# Also, the number of attempts should not be incremented
self.assertRegex(result['success'], r"You must wait at least 2 hours between submissions. 2 minutes 1 second remaining\..*") # lint-amnesty, pylint: disable=line-too-long
assert block.attempts == num_attempts
def test_submit_quiz_with_involved_pretty_print(self):
# Already attempted once (just now)
num_attempts = 1
(block, result) = self.create_and_check(
num_attempts=num_attempts,
last_submission_time=datetime.datetime(2013, 12, 6, 0, 17, 36, tzinfo=UTC),
submission_wait_seconds=60 * 60 * 2 + 63,
considered_now=datetime.datetime(2013, 12, 6, 1, 15, 40, tzinfo=UTC)
)
# You should get a dialog that tells you to wait 2 minutes
# Also, the number of attempts should not be incremented
self.assertRegex(result['success'], r"You must wait at least 2 hours 1 minute 3 seconds between submissions. 1 hour 2 minutes 59 seconds remaining\..*") # lint-amnesty, pylint: disable=line-too-long
assert block.attempts == num_attempts
def test_submit_quiz_with_nonplural_pretty_print(self):
# Already attempted once (just now)
num_attempts = 1
(block, result) = self.create_and_check(
num_attempts=num_attempts,
last_submission_time=datetime.datetime(2013, 12, 6, 0, 17, 36, tzinfo=UTC),
submission_wait_seconds=60,
considered_now=datetime.datetime(2013, 12, 6, 0, 17, 36, tzinfo=UTC)
)
# You should get a dialog that tells you to wait 2 minutes
# Also, the number of attempts should not be incremented
self.assertRegex(result['success'], r"You must wait at least 1 minute between submissions. 1 minute remaining\..*") # lint-amnesty, pylint: disable=line-too-long
assert block.attempts == num_attempts