""" 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("""\

What is pi, to two decimal places?

""") 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="
Test Template HTML
")) 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