Files
edx-platform/cms/djangoapps/contentstore/tests/test_exams.py

212 lines
7.9 KiB
Python

"""
Test the exams service integration into Studio
"""
from datetime import datetime, timedelta, timezone
from unittest.mock import patch, Mock
import ddt
from django.conf import settings
from edx_toggles.toggles.testutils import override_waffle_flag
from pytz import UTC
from cms.djangoapps.contentstore.signals.handlers import listen_for_course_publish
from openedx.core.djangoapps.course_apps.toggles import EXAMS_IDA
from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE, ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, BlockFactory
@ddt.ddt
@override_waffle_flag(EXAMS_IDA, active=True)
@patch.dict('django.conf.settings.FEATURES', {'ENABLE_SPECIAL_EXAMS': True})
@patch('cms.djangoapps.contentstore.exams._patch_course_exams')
@patch('cms.djangoapps.contentstore.signals.handlers.transaction.on_commit',
new=Mock(side_effect=lambda func: func()),) # run right away
class TestExamService(ModuleStoreTestCase):
"""
Test for syncing exams to the exam service
"""
MODULESTORE = TEST_DATA_SPLIT_MODULESTORE
def setUp(self):
"""
Initial data setup
"""
super().setUp()
self.course = CourseFactory.create(
org='edX',
course='900',
run='test_run',
enable_proctored_exams=True,
proctoring_provider='null',
)
self.chapter = BlockFactory.create(parent=self.course, category='chapter', display_name='Test Section')
self.course_key = str(self.course.id)
# create one non-exam sequence
chapter2 = BlockFactory.create(parent=self.course, category='chapter', display_name='Test Homework')
BlockFactory.create(
parent=chapter2,
category='sequential',
display_name='Homework 1',
graded=True,
is_time_limited=False,
due=datetime.now(UTC) + timedelta(minutes=60),
)
def _get_exams_url(self, course_id):
return f'{settings.EXAMS_SERVICE_URL}/exams/course_id/{course_id}/'
@ddt.data(
(False, False, False, 'timed'),
(True, False, False, 'proctored'),
(True, True, False, 'practice'),
(True, True, True, 'onboarding'),
)
@ddt.unpack
def test_publishing_exam(self, is_proctored_exam, is_practice_exam,
is_onboarding_exam, expected_type, mock_patch_course_exams):
"""
When a course is published it will register all exams sections with the exams service
"""
default_time_limit_minutes = 10
due_date = datetime.now(UTC) + timedelta(minutes=default_time_limit_minutes + 1)
sequence = BlockFactory.create(
parent=self.chapter,
category='sequential',
display_name='Test Proctored Exam',
graded=True,
is_time_limited=True,
default_time_limit_minutes=default_time_limit_minutes,
is_proctored_enabled=is_proctored_exam,
is_practice_exam=is_practice_exam,
due=due_date,
hide_after_due=True,
is_onboarding_exam=is_onboarding_exam,
)
expected_exams = [{
'course_id': self.course_key,
'content_id': str(sequence.location),
'exam_name': sequence.display_name,
'time_limit_mins': sequence.default_time_limit_minutes,
'due_date': due_date.isoformat(),
'exam_type': expected_type,
'is_active': True,
'hide_after_due': True,
# backend is only required for edx-proctoring support edx-exams will maintain LTI backends
'backend': 'null',
}]
listen_for_course_publish(self, self.course.id)
mock_patch_course_exams.assert_called_once_with(expected_exams, self.course_key)
def test_publish_no_exam(self, mock_patch_course_exams):
"""
Exam service is called with an empty list if there are no exam sections.
This will deactivate any currently active exams
"""
listen_for_course_publish(self, self.course.id)
mock_patch_course_exams.assert_called_once_with([], self.course_key)
def test_dangling_exam(self, mock_patch_course_exams):
"""
Make sure we filter out all dangling items
"""
BlockFactory.create(
parent=self.chapter,
category='sequential',
display_name='Test Proctored Exam',
graded=True,
is_time_limited=True,
default_time_limit_minutes=10,
is_proctored_enabled=True,
hide_after_due=False,
)
self.store.delete_item(self.chapter.location, self.user.id)
listen_for_course_publish(self, self.course.id)
mock_patch_course_exams.assert_called_once_with([], self.course_key)
@patch.dict('django.conf.settings.FEATURES', {'ENABLE_SPECIAL_EXAMS': False})
def test_feature_flag_off(self, mock_patch_course_exams):
"""
Make sure the feature flag is honored
"""
BlockFactory.create(
parent=self.chapter,
category='sequential',
display_name='Test Proctored Exam',
graded=True,
is_time_limited=True,
default_time_limit_minutes=10,
is_proctored_enabled=True,
hide_after_due=False,
)
listen_for_course_publish(self, self.course.id)
mock_patch_course_exams.assert_not_called()
# MODIFY DUE DATE HERE
@ddt.data(
(True, datetime(2035, 1, 1, 0, 0, tzinfo=timezone.utc)),
(False, datetime(2035, 1, 1, 0, 0, tzinfo=timezone.utc)),
(True, None),
(False, None),
)
@ddt.unpack
def test_no_due_dates(self, is_self_paced, course_end_date, mock_patch_course_exams):
"""
Test that the coures end date is registered as the due date when the subsection does not have a due date for
both self-paced and instructor-paced exams.
"""
self.course.self_paced = is_self_paced
self.course.end = course_end_date
self.course = self.update_course(self.course, 1)
BlockFactory.create(
parent=self.chapter,
category='sequential',
display_name='Test Proctored Exam',
graded=True,
is_time_limited=True,
default_time_limit_minutes=60,
is_proctored_enabled=False,
is_practice_exam=False,
due=None,
hide_after_due=True,
is_onboarding_exam=False,
)
listen_for_course_publish(self, self.course.id)
called_exams, called_course = mock_patch_course_exams.call_args[0]
assert called_exams[0]['due_date'] == (course_end_date.isoformat() if course_end_date else None)
@ddt.data(True, False)
def test_subsection_due_date_prioritized(self, is_self_paced, mock_patch_course_exams):
"""
Test that the subsection due date is registered as the due date when both the subsection has a due date and the
course has an end date for both self-paced and instructor-paced exams.
"""
self.course.self_paced = is_self_paced
self.course.end = datetime(2035, 1, 1, 0, 0)
self.course = self.update_course(self.course, 1)
sequential_due_date = datetime.now(UTC) + timedelta(minutes=60)
BlockFactory.create(
parent=self.chapter,
category='sequential',
display_name='Test Proctored Exam',
graded=True,
is_time_limited=True,
default_time_limit_minutes=60,
is_proctored_enabled=False,
is_practice_exam=False,
due=sequential_due_date,
hide_after_due=True,
is_onboarding_exam=False,
)
listen_for_course_publish(self, self.course.id)
called_exams, called_course = mock_patch_course_exams.call_args[0]
assert called_exams[0]['due_date'] == sequential_due_date.isoformat()