Check for an existing proctored exam before sending proctoring requirements email
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user