diff --git a/cms/djangoapps/contentstore/config/waffle.py b/cms/djangoapps/contentstore/config/waffle.py index e9c1bd90cd..540dad49bb 100644 --- a/cms/djangoapps/contentstore/config/waffle.py +++ b/cms/djangoapps/contentstore/config/waffle.py @@ -52,6 +52,22 @@ SHOW_REVIEW_RULES_FLAG = CourseWaffleFlag( # lint-amnesty, pylint: disable=togg module_name=__name__, ) +# Waffle flag to move the registration of special exams to an async celery task +# .. toggle_name: contentstore.async_register_exams +# .. toggle_implementation: CourseWaffleFlag +# .. toggle_default: False +# .. toggle_description: Toggles the asynchronous registration of special exams +# .. toggle_use_cases: temporary, open_edx +# .. toggle_creation_date: 2021-04-21 +# .. toggle_target_removal_date: 2021-05-07 +# .. toggle_warnings: +# .. toggle_tickets: https://openedx.atlassian.net/browse/MST-757 +ENABLE_ASYNC_REGISTER_EXAMS = CourseWaffleFlag( + waffle_namespace=waffle_flags(), + flag_name='async_register_exams', + module_name=__name__, +) + # Waffle flag to redirect to the library authoring MFE. # .. toggle_name: contentstore.library_authoring_mfe # .. toggle_implementation: WaffleFlag diff --git a/cms/djangoapps/contentstore/signals/handlers.py b/cms/djangoapps/contentstore/signals/handlers.py index e7e26cd96b..b7fc7bc48b 100644 --- a/cms/djangoapps/contentstore/signals/handlers.py +++ b/cms/djangoapps/contentstore/signals/handlers.py @@ -24,6 +24,7 @@ from openedx.core.lib.gating import api as gating_api from xmodule.modulestore.django import SignalHandler, modulestore from .signals import GRADING_POLICY_CHANGED +from ..config.waffle import ENABLE_ASYNC_REGISTER_EXAMS log = logging.getLogger(__name__) @@ -51,18 +52,23 @@ def listen_for_course_publish(sender, course_key, **kwargs): # pylint: disable= registering proctored exams, building up credit requirements, and performing search indexing """ + is_enabled = ENABLE_ASYNC_REGISTER_EXAMS.is_enabled(course_key) + if is_enabled: + from cms.djangoapps.contentstore.tasks import update_special_exams_and_publish + course_key_str = str(course_key) + update_special_exams_and_publish.delay(course_key_str) + else: + # first is to registered exams, the credit subsystem will assume that + # all proctored exams have already been registered, so we have to do that first + try: + register_special_exams(course_key) + # pylint: disable=broad-except + except Exception as exception: + log.exception(exception) - # first is to registered exams, the credit subsystem will assume that - # all proctored exams have already been registered, so we have to do that first - try: - register_special_exams(course_key) - # pylint: disable=broad-except - except Exception as exception: - log.exception(exception) - - # then call into the credit subsystem (in /openedx/djangoapps/credit) - # to perform any 'on_publish' workflow - on_course_publish(course_key) + # then call into the credit subsystem (in /openedx/djangoapps/credit) + # to perform any 'on_publish' workflow + on_course_publish(course_key) # import here, because signal is registered at startup, but items in tasks are not yet able to be loaded from cms.djangoapps.contentstore.tasks import update_outline_from_modulestore_task, update_search_index diff --git a/cms/djangoapps/contentstore/tasks.py b/cms/djangoapps/contentstore/tasks.py index de2fd0b347..21b8b02f9b 100644 --- a/cms/djangoapps/contentstore/tasks.py +++ b/cms/djangoapps/contentstore/tasks.py @@ -232,6 +232,31 @@ def update_library_index(library_id, triggered_time_isoformat): LOGGER.debug('Search indexing successful for library %s', library_id) +@shared_task +@set_code_owner_attribute +def update_special_exams_and_publish(course_key_str): + """ + Registers special exams for a given course and calls publishing flow. + + on_course_publish expects that the edx-proctoring subsystem has been refreshed + before being executed, so both functions are called here synchronously. + """ + from cms.djangoapps.contentstore.proctoring import register_special_exams + from openedx.core.djangoapps.credit.signals import on_course_publish + + course_key = CourseKey.from_string(course_key_str) + LOGGER.info('Attempting to register exams for course %s', course_key_str) + try: + register_special_exams(course_key) + LOGGER.info('Successfully registered exams for course %s', course_key_str) + # pylint: disable=broad-except + except Exception as exception: + LOGGER.exception(exception) + + LOGGER.info('Publishing course %s', course_key_str) + on_course_publish(course_key) + + class CourseExportTask(UserTask): # pylint: disable=abstract-method """ Base class for course and library export tasks. diff --git a/cms/djangoapps/contentstore/tests/test_proctoring.py b/cms/djangoapps/contentstore/tests/test_proctoring.py index 37f2e3072b..b7221e31a8 100644 --- a/cms/djangoapps/contentstore/tests/test_proctoring.py +++ b/cms/djangoapps/contentstore/tests/test_proctoring.py @@ -9,9 +9,11 @@ from unittest.mock import patch import ddt from django.conf import settings from edx_proctoring.api import get_all_exams_for_course, get_review_policy_by_exam_id +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 cms.djangoapps.contentstore.config.waffle import ENABLE_ASYNC_REGISTER_EXAMS from common.djangoapps.student.tests.factories import UserFactory from common.lib.xmodule.xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase @@ -100,7 +102,9 @@ class TestProctoredExams(ModuleStoreTestCase): is_onboarding_exam=is_onboarding_exam, ) - listen_for_course_publish(self, self.course.id) + with patch('cms.djangoapps.contentstore.tasks.update_special_exams_and_publish') as mock_task: + listen_for_course_publish(self, self.course.id) + mock_task.delay.assert_not_called() self._verify_exam_data(sequence, True) @@ -314,3 +318,45 @@ class TestProctoredExams(ModuleStoreTestCase): listen_for_course_publish(self, self.course.id) exams = get_all_exams_for_course(str(self.course.id)) assert exams[0]['due_date'] is not None + + @override_waffle_flag(ENABLE_ASYNC_REGISTER_EXAMS, active=True) + def test_async_waffle_flag_publishes(self): + 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, + is_onboarding_exam=False, + exam_review_rules="allow_use_of_paper", + ) + + listen_for_course_publish(self, self.course.id) + + exams = get_all_exams_for_course(str(self.course.id)) + self.assertEqual(len(exams), 1) + self._verify_exam_data(sequence, True) + + @override_waffle_flag(ENABLE_ASYNC_REGISTER_EXAMS, active=True) + def test_async_waffle_flag_task(self): + 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, + is_onboarding_exam=False, + exam_review_rules="allow_use_of_paper", + ) + + with patch('cms.djangoapps.contentstore.tasks.update_special_exams_and_publish') as mock_task: + listen_for_course_publish(self, self.course.id) + mock_task.delay.assert_called()