237 lines
8.1 KiB
Python
237 lines
8.1 KiB
Python
"""
|
|
Tests for the edx_proctoring integration into Studio
|
|
"""
|
|
|
|
from datetime import datetime, timedelta
|
|
|
|
import ddt
|
|
from edx_proctoring.api import get_all_exams_for_course, get_review_policy_by_exam_id
|
|
from mock import patch
|
|
from pytz import UTC
|
|
|
|
from contentstore.signals.handlers import listen_for_course_publish
|
|
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
|
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
|
|
|
|
|
@ddt.ddt
|
|
@patch.dict('django.conf.settings.FEATURES', {'ENABLE_SPECIAL_EXAMS': True})
|
|
class TestProctoredExams(ModuleStoreTestCase):
|
|
"""
|
|
Tests for the publishing of proctored exams
|
|
"""
|
|
|
|
def setUp(self):
|
|
"""
|
|
Initial data setup
|
|
"""
|
|
super(TestProctoredExams, self).setUp()
|
|
|
|
self.course = CourseFactory.create(
|
|
org='edX',
|
|
course='900',
|
|
run='test_run',
|
|
enable_proctored_exams=True
|
|
)
|
|
|
|
def _verify_exam_data(self, sequence, expected_active):
|
|
"""
|
|
Helper method to compare the sequence with the stored exam,
|
|
which should just be a single one
|
|
"""
|
|
exams = get_all_exams_for_course(unicode(self.course.id))
|
|
|
|
self.assertEqual(len(exams), 1)
|
|
|
|
exam = exams[0]
|
|
|
|
if exam['is_proctored'] and not exam['is_practice_exam']:
|
|
# get the review policy object
|
|
exam_review_policy = get_review_policy_by_exam_id(exam['id'])
|
|
self.assertEqual(exam_review_policy['review_policy'], sequence.exam_review_rules)
|
|
|
|
if not exam['is_proctored'] and not exam['is_practice_exam']:
|
|
# the hide after due value only applies to timed exams
|
|
self.assertEqual(exam['hide_after_due'], sequence.hide_after_due)
|
|
|
|
self.assertEqual(exam['course_id'], unicode(self.course.id))
|
|
self.assertEqual(exam['content_id'], unicode(sequence.location))
|
|
self.assertEqual(exam['exam_name'], sequence.display_name)
|
|
self.assertEqual(exam['time_limit_mins'], sequence.default_time_limit_minutes)
|
|
self.assertEqual(exam['is_proctored'], sequence.is_proctored_exam)
|
|
self.assertEqual(exam['is_practice_exam'], sequence.is_practice_exam)
|
|
self.assertEqual(exam['is_active'], expected_active)
|
|
|
|
@ddt.data(
|
|
(True, 10, True, False, True, False, False),
|
|
(True, 10, False, False, True, False, False),
|
|
(True, 10, False, False, True, False, True),
|
|
(True, 10, True, True, True, True, False),
|
|
)
|
|
@ddt.unpack
|
|
def test_publishing_exam(self, is_time_limited, default_time_limit_minutes, is_proctored_exam,
|
|
is_practice_exam, expected_active, republish, hide_after_due):
|
|
"""
|
|
Happy path testing to see that when a course is published which contains
|
|
a proctored exam, it will also put an entry into the exam tables
|
|
"""
|
|
|
|
chapter = ItemFactory.create(parent=self.course, category='chapter', display_name='Test Section')
|
|
sequence = ItemFactory.create(
|
|
parent=chapter,
|
|
category='sequential',
|
|
display_name='Test Proctored Exam',
|
|
graded=True,
|
|
is_time_limited=is_time_limited,
|
|
default_time_limit_minutes=default_time_limit_minutes,
|
|
is_proctored_exam=is_proctored_exam,
|
|
is_practice_exam=is_practice_exam,
|
|
due=datetime.now(UTC) + timedelta(minutes=default_time_limit_minutes + 1),
|
|
exam_review_rules="allow_use_of_paper",
|
|
hide_after_due=hide_after_due,
|
|
)
|
|
|
|
listen_for_course_publish(self, self.course.id)
|
|
|
|
self._verify_exam_data(sequence, expected_active)
|
|
|
|
if republish:
|
|
# update the sequence
|
|
sequence.default_time_limit_minutes += sequence.default_time_limit_minutes
|
|
self.store.update_item(sequence, self.user.id)
|
|
|
|
# simulate a publish
|
|
listen_for_course_publish(self, self.course.id)
|
|
|
|
# reverify
|
|
self._verify_exam_data(sequence, expected_active)
|
|
|
|
def test_unpublishing_proctored_exam(self):
|
|
"""
|
|
Make sure that if we publish and then unpublish a proctored exam,
|
|
the exam record stays, but is marked as is_active=False
|
|
"""
|
|
|
|
chapter = ItemFactory.create(parent=self.course, category='chapter', display_name='Test Section')
|
|
sequence = ItemFactory.create(
|
|
parent=chapter,
|
|
category='sequential',
|
|
display_name='Test Proctored Exam',
|
|
graded=True,
|
|
is_time_limited=True,
|
|
default_time_limit_minutes=10,
|
|
is_proctored_exam=True,
|
|
hide_after_due=False,
|
|
)
|
|
|
|
listen_for_course_publish(self, self.course.id)
|
|
|
|
exams = get_all_exams_for_course(unicode(self.course.id))
|
|
self.assertEqual(len(exams), 1)
|
|
|
|
sequence.is_time_limited = False
|
|
sequence.is_proctored_exam = False
|
|
|
|
self.store.update_item(sequence, self.user.id)
|
|
|
|
listen_for_course_publish(self, self.course.id)
|
|
|
|
self._verify_exam_data(sequence, False)
|
|
|
|
def test_dangling_exam(self):
|
|
"""
|
|
Make sure we filter out all dangling items
|
|
"""
|
|
|
|
chapter = ItemFactory.create(parent=self.course, category='chapter', display_name='Test Section')
|
|
ItemFactory.create(
|
|
parent=chapter,
|
|
category='sequential',
|
|
display_name='Test Proctored Exam',
|
|
graded=True,
|
|
is_time_limited=True,
|
|
default_time_limit_minutes=10,
|
|
is_proctored_exam=True,
|
|
hide_after_due=False,
|
|
)
|
|
|
|
listen_for_course_publish(self, self.course.id)
|
|
|
|
exams = get_all_exams_for_course(unicode(self.course.id))
|
|
self.assertEqual(len(exams), 1)
|
|
|
|
self.store.delete_item(chapter.location, self.user.id)
|
|
|
|
# republish course
|
|
listen_for_course_publish(self, self.course.id)
|
|
|
|
# look through exam table, the dangling exam
|
|
# should be disabled
|
|
exams = get_all_exams_for_course(unicode(self.course.id))
|
|
self.assertEqual(len(exams), 1)
|
|
|
|
exam = exams[0]
|
|
self.assertEqual(exam['is_active'], False)
|
|
|
|
@patch.dict('django.conf.settings.FEATURES', {'ENABLE_SPECIAL_EXAMS': False})
|
|
def test_feature_flag_off(self):
|
|
"""
|
|
Make sure the feature flag is honored
|
|
"""
|
|
|
|
chapter = ItemFactory.create(parent=self.course, category='chapter', display_name='Test Section')
|
|
ItemFactory.create(
|
|
parent=chapter,
|
|
category='sequential',
|
|
display_name='Test Proctored Exam',
|
|
graded=True,
|
|
is_time_limited=True,
|
|
default_time_limit_minutes=10,
|
|
is_proctored_exam=True,
|
|
hide_after_due=False,
|
|
)
|
|
|
|
listen_for_course_publish(self, self.course.id)
|
|
|
|
exams = get_all_exams_for_course(unicode(self.course.id))
|
|
self.assertEqual(len(exams), 0)
|
|
|
|
@ddt.data(
|
|
(True, False, 1),
|
|
(False, True, 1),
|
|
(False, False, 0),
|
|
)
|
|
@ddt.unpack
|
|
def test_advanced_settings(self, enable_timed_exams, enable_proctored_exams, expected_count):
|
|
"""
|
|
Make sure the feature flag is honored
|
|
"""
|
|
|
|
self.course = CourseFactory.create(
|
|
org='edX',
|
|
course='901',
|
|
run='test_run2',
|
|
enable_proctored_exams=enable_proctored_exams,
|
|
enable_timed_exams=enable_timed_exams
|
|
)
|
|
|
|
chapter = ItemFactory.create(parent=self.course, category='chapter', display_name='Test Section')
|
|
ItemFactory.create(
|
|
parent=chapter,
|
|
category='sequential',
|
|
display_name='Test Proctored Exam',
|
|
graded=True,
|
|
is_time_limited=True,
|
|
default_time_limit_minutes=10,
|
|
is_proctored_exam=True,
|
|
exam_review_rules="allow_use_of_paper",
|
|
hide_after_due=False,
|
|
)
|
|
|
|
listen_for_course_publish(self, self.course.id)
|
|
|
|
# there shouldn't be any exams because we haven't enabled that
|
|
# advanced setting flag
|
|
exams = get_all_exams_for_course(unicode(self.course.id))
|
|
self.assertEqual(len(exams), expected_count)
|