Merge pull request #26357 from edx/bseverino/proctored-exam-email

[MST-636] Add additional check for proctoring requirements
This commit is contained in:
Bianca Severino
2021-02-04 09:09:12 -05:00
committed by GitHub
4 changed files with 83 additions and 13 deletions

View File

@@ -9,6 +9,7 @@ from django.conf import settings
from lms.djangoapps.verify_student.services import IDVerificationService
from openedx.core.djangoapps.ace_common.template_context import get_base_template_context
from openedx.core.djangoapps.enrollments.api import is_enrollment_valid_for_proctoring
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from xmodule.modulestore.django import modulestore
@@ -51,3 +52,26 @@ def generate_proctoring_requirements_email_context(user, course_id):
'proctoring_requirements_url': settings.PROCTORING_SETTINGS.get('LINK_URLS', {}).get('faq', ''),
'id_verification_url': IDVerificationService.get_verify_location(),
}
def should_send_proctoring_requirements_email(username, course_id):
"""
Returns a boolean whether a proctoring requirements email should be sent.
Arguments:
* username (str): The user associated with the enrollment.
* course_id (str): The course id associated with the enrollment.
"""
if not is_enrollment_valid_for_proctoring(username, course_id):
return False
# Only send if a proctored exam is found in the course
timed_exams = modulestore().get_items(
course_id,
qualifiers={'category': 'sequential'},
settings={'is_time_limited': True}
)
has_proctored_exam = any([exam.is_proctored_exam for exam in timed_exams])
return has_proctored_exam

View File

@@ -59,7 +59,10 @@ from user_util import user_util
import openedx.core.djangoapps.django_comment_common.comment_client as cc
from common.djangoapps.course_modes.models import CourseMode, get_cosmetic_verified_display_price
from common.djangoapps.student.emails import send_proctoring_requirements_email
from common.djangoapps.student.email_helpers import generate_proctoring_requirements_email_context
from common.djangoapps.student.email_helpers import (
generate_proctoring_requirements_email_context,
should_send_proctoring_requirements_email
)
from common.djangoapps.student.signals import ENROLL_STATUS_CHANGE, ENROLLMENT_TRACK_UPDATED, UNENROLL_DONE
from common.djangoapps.track import contexts, segment
from common.djangoapps.util.model_utils import emit_field_changed_events, get_changed_fields_dict
@@ -76,7 +79,6 @@ from openedx.core.djangoapps.content.course_overviews.models import CourseOvervi
from openedx.core.djangoapps.enrollments.api import (
_default_course_mode,
get_enrollment_attributes,
is_enrollment_valid_for_proctoring,
set_enrollment_attributes,
)
from openedx.core.djangoapps.signals.signals import USER_ACCOUNT_ACTIVATED
@@ -1461,7 +1463,7 @@ class CourseEnrollment(models.Model):
if mode_changed:
if COURSEWARE_PROCTORING_IMPROVEMENTS.is_enabled(self.course_id):
# If mode changed to one that requires proctoring, send proctoring requirements email
if is_enrollment_valid_for_proctoring(self.user.username, self.course_id):
if should_send_proctoring_requirements_email(self.user.username, self.course_id):
email_context = generate_proctoring_requirements_email_context(self.user, self.course_id)
send_proctoring_requirements_email(context=email_context)

View File

@@ -26,7 +26,7 @@ from common.djangoapps.student.tests.factories import CourseEnrollmentAllowedFac
from common.djangoapps.util.testing import UrlResetMixin
from lms.djangoapps.courseware.toggles import COURSEWARE_PROCTORING_IMPROVEMENTS
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
@ddt.ddt
@@ -48,7 +48,12 @@ class EnrollmentTest(UrlResetMixin, SharedModuleStoreTestCase):
super(EnrollmentTest, cls).setUpClass()
cls.course = CourseFactory.create()
cls.course_limited = CourseFactory.create()
cls.proctored_course = CourseFactory(enable_proctored_exams=True)
cls.proctored_course = CourseFactory(
enable_proctored_exams=True, enable_timed_exams=True
)
cls.proctored_course_no_exam = CourseFactory(
enable_proctored_exams=True, enable_timed_exams=True
)
@patch.dict(settings.FEATURES, {'EMBARGO': True})
def setUp(self):
@@ -61,6 +66,21 @@ class EnrollmentTest(UrlResetMixin, SharedModuleStoreTestCase):
self.urls = [
reverse('course_modes_choose', kwargs={'course_id': six.text_type(self.course.id)})
]
# Set up proctored exam
self._create_proctored_exam(self.proctored_course)
def _create_proctored_exam(self, course):
"""
Helper function to create a proctored exam for a given course
"""
chapter = ItemFactory.create(
parent=course, category='chapter', display_name='Test Section', publish_item=True
)
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, publish_item=True
)
@ddt.data(
# Default (no course modes in the database)
@@ -197,10 +217,24 @@ class EnrollmentTest(UrlResetMixin, SharedModuleStoreTestCase):
CourseEnrollment.enroll(self.user, self.proctored_course.id, mode) # pylint: disable=no-member
self.assertEqual(email_sent, mock_send_email.called)
def test_enroll_in_proctored_course_no_exam(self):
"""
If a verified learner enrolls in a course that has proctoring enabled, but does not have
any proctored exams, they should not receive a proctoring requirements email.
"""
with patch(
'common.djangoapps.student.models.send_proctoring_requirements_email',
return_value=None
) as mock_send_email:
CourseEnrollment.enroll(
self.user, self.proctored_course_no_exam.id, 'verified' # pylint: disable=no-member
)
self.assertFalse(mock_send_email.called)
@ddt.data('verified', 'masters', 'professional', 'executive-education')
def test_upgrade_proctoring_enrollment(self, mode):
"""
When upgrading from audit in a proctoring-enabled course, an email with proctoring requirements
When upgrading from audit in a course with proctored exams, an email with proctoring requirements
should be sent.
"""
with patch(
@@ -219,15 +253,18 @@ class EnrollmentTest(UrlResetMixin, SharedModuleStoreTestCase):
def test_enroll_in_proctored_course_honor_mode_allowed(self):
"""
If the proctoring provider allows honor mode, send proctoring requirements email when learners
enroll in honor mode for a proctoring-enabled course.
enroll in honor mode for a course with proctored exams.
"""
with patch(
'common.djangoapps.student.models.send_proctoring_requirements_email',
return_value=None
) as mock_send_email:
course_honor_mode = CourseFactory(
enable_proctored_exams=True, proctoring_provider='test_provider_honor_mode'
enable_proctored_exams=True,
enable_timed_exams=True,
proctoring_provider='test_provider_honor_mode',
)
self._create_proctored_exam(course_honor_mode)
CourseEnrollment.enroll(self.user, course_honor_mode.id, 'honor') # pylint: disable=no-member
self.assertTrue(mock_send_email.called)

View File

@@ -497,25 +497,32 @@ def serialize_enrollments(enrollments):
def is_enrollment_valid_for_proctoring(username, course_id):
"""
Returns a boolean value regarding whether user's course enrollment is eligible for proctoring. Returns
False if the enrollment is not active, special exams aren't enabled, proctored exams aren't enabled
for the course, or if the course mode is audit.
Returns a boolean value regarding whether user's course enrollment is eligible for proctoring.
Returns false if:
* special exams aren't enabled
* the enrollment is not active
* proctored exams aren't enabled for the course
* the course mode is audit
Arguments:
username: The user associated with the enrollment.
course_id (str): The course id associated with the enrollment.
* username (str): The user associated with the enrollment.
* course_id (str): The course id associated with the enrollment.
"""
if not settings.FEATURES.get('ENABLE_SPECIAL_EXAMS'):
return False
# Verify that the learner's enrollment is active
enrollment = _data_api().get_course_enrollment(username, str(course_id))
if not enrollment['is_active']:
return False
# Check that the course has proctored exams enabled
course_module = modulestore().get_course(course_id)
if not course_module.enable_proctored_exams:
return False
# Only allow verified modes
appropriate_modes = [
CourseMode.VERIFIED, CourseMode.MASTERS, CourseMode.PROFESSIONAL, CourseMode.EXECUTIVE_EDUCATION
]