From e3b03a1f24f6307993357b32e68ad2caea10c3c3 Mon Sep 17 00:00:00 2001 From: Zach Hancock Date: Fri, 16 Sep 2022 16:48:18 -0400 Subject: [PATCH 1/5] feat: sync with exam service on course publish --- cms/djangoapps/contentstore/exams.py | 129 +++++++++++++ cms/djangoapps/contentstore/proctoring.py | 3 + cms/djangoapps/contentstore/tasks.py | 9 +- .../contentstore/tests/test_exams.py | 173 ++++++++++++++++++ .../contentstore/tests/test_tasks.py | 34 +++- cms/envs/common.py | 3 + cms/envs/production.py | 4 + lms/envs/devstack.py | 3 + lms/envs/test.py | 3 + 9 files changed, 358 insertions(+), 3 deletions(-) create mode 100644 cms/djangoapps/contentstore/exams.py create mode 100644 cms/djangoapps/contentstore/tests/test_exams.py diff --git a/cms/djangoapps/contentstore/exams.py b/cms/djangoapps/contentstore/exams.py new file mode 100644 index 000000000000..2d13ad6673ae --- /dev/null +++ b/cms/djangoapps/contentstore/exams.py @@ -0,0 +1,129 @@ + +""" +Utilities for working with the exam service +""" + +import json +import logging + +import requests +from django.conf import settings +from django.contrib.auth import get_user_model +from edx_rest_api_client.auth import SuppliedJwtAuth + +from openedx.core.djangoapps.course_apps.toggles import exams_ida_enabled +from openedx.core.djangoapps.oauth_dispatch.jwt import create_jwt_for_user +from xmodule.modulestore.django import modulestore +from xmodule.modulestore.exceptions import ItemNotFoundError + +from .views.helpers import is_item_in_course_tree + +log = logging.getLogger(__name__) +User = get_user_model() + + +def register_exams(course_key): + """ + This is typically called on a course published signal. The course is examined for sequences + that are marked as timed exams. Then these are registered with the exams service. + Likewise, if formerly registered exams are unmarked, then those + registered exams are marked as inactive + """ + if not settings.FEATURES.get('ENABLE_SPECIAL_EXAMS') or not exams_ida_enabled(course_key): + # if feature is not enabled then do a quick exit + return + + course = modulestore().get_course(course_key) + if course is None: + raise ItemNotFoundError("Course {} does not exist", str(course_key)) # lint-amnesty, pylint: disable=raising-format-tuple + + if not course.enable_proctored_exams and not course.enable_timed_exams: + # TODO: I'M NOT SO SURE WE ACTUALLY WANT THIS + # likewise if course does not have these features turned on + # then quickly exit + return + + # get all sequences, since they can be marked as timed/proctored exams + _timed_exams = modulestore().get_items( + course_key, + qualifiers={ + 'category': 'sequential', + }, + settings={ + 'is_time_limited': True, + } + ) + + # filter out any potential dangling sequences + timed_exams = [ + timed_exam + for timed_exam in _timed_exams + if is_item_in_course_tree(timed_exam) + ] + + exams_list = [] + locations = [] + for timed_exam in timed_exams: + location = str(timed_exam.location) + msg = ( + 'Found {location} as an exam in course structure.'.format( + location=location + ) + ) + log.info(msg) + locations.append(location) + + if timed_exam.is_proctored_exam: + if timed_exam.is_onboarding_exam: + exam_type = 'onboarding' + elif timed_exam.is_practice_exam: + exam_type = 'practice_proctored' + else: + exam_type = 'proctored' + else: + exam_type = 'timed' + + exams_list.append({ + 'course_id': str(course_key), + 'content_id': str(timed_exam.location), + 'exam_name': timed_exam.display_name, + 'time_limit_mins': timed_exam.default_time_limit_minutes, + 'due_date': timed_exam.due if not course.self_paced else None, + 'exam_type': exam_type, + 'is_active': True, + 'hide_after_due': timed_exam.hide_after_due, + # backend is only required for edx-proctoring support edx-exams will maintain LTI backends + 'backend': course.proctoring_provider, + }) + + try: + _patch_course_exams(exams_list, str(course_key)) + log.info(f'Successfully registered {locations} with exam service') + # pylint: disable=broad-except + except Exception: + log.exception('Failed to register exams with exam API', exc_info=True) + + +def _get_exams_api_client(): + """ + Returns an API client which can be used to make Exams API requests. + """ + user = User.objects.get(username=settings.EXAMS_SERVICE_USERNAME) + jwt = create_jwt_for_user(user) + client = requests.Session() + client.auth = SuppliedJwtAuth(jwt) + + return client + + +def _patch_course_exams(exams_list, course_id): + """ + Make a PATCH request to update course exams + """ + url = f'{settings.EXAMS_SERVICE_URL}/exams/course_id/{course_id}/' + api_client = _get_exams_api_client() + + response = api_client.patch(url, data=json.dumps(exams_list)) + response.raise_for_status() + response = response.json() + return response diff --git a/cms/djangoapps/contentstore/proctoring.py b/cms/djangoapps/contentstore/proctoring.py index cf0704259b49..d265232f83e3 100644 --- a/cms/djangoapps/contentstore/proctoring.py +++ b/cms/djangoapps/contentstore/proctoring.py @@ -1,5 +1,8 @@ """ Code related to the handling of Proctored Exams in Studio +using the edx-proctoring plugin. + +Courses using the exam IDA are handled by ./exams.py """ diff --git a/cms/djangoapps/contentstore/tasks.py b/cms/djangoapps/contentstore/tasks.py index eea41c3a4342..3c1171d16daf 100644 --- a/cms/djangoapps/contentstore/tasks.py +++ b/cms/djangoapps/contentstore/tasks.py @@ -51,6 +51,7 @@ from common.djangoapps.student.auth import has_course_author_access from common.djangoapps.util.monitoring import monitor_import_failure from openedx.core.djangoapps.content.learning_sequences.api import key_supports_outlines +from openedx.core.djangoapps.course_apps.toggles import exams_ida_enabled from openedx.core.djangoapps.discussions.tasks import update_unit_discussion_state_from_discussion_blocks from openedx.core.djangoapps.embargo.models import CountryAccessRule, RestrictedCourse from openedx.core.lib.extract_tar import safetar_extractall @@ -242,13 +243,17 @@ def update_special_exams_and_publish(course_key_str): 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 cms.djangoapps.contentstore.exams import register_exams + from cms.djangoapps.contentstore.proctoring import register_special_exams as register_exams_legacy 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) + + # Call the appropriate handler for the exams IDA or the legacy edx-proctoring plugin + register_exams_handler = register_exams if exams_ida_enabled(course_key) else register_exams_legacy try: - register_special_exams(course_key) + register_exams_handler(course_key) LOGGER.info('Successfully registered exams for course %s', course_key_str) # pylint: disable=broad-except except Exception as exception: diff --git a/cms/djangoapps/contentstore/tests/test_exams.py b/cms/djangoapps/contentstore/tests/test_exams.py new file mode 100644 index 000000000000..5a25840bda81 --- /dev/null +++ b/cms/djangoapps/contentstore/tests/test_exams.py @@ -0,0 +1,173 @@ +""" +Test the exams service integration into Studio +""" +from datetime import datetime, timedelta +from unittest.mock import patch + +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_MONGO_AMNESTY_MODULESTORE, ModuleStoreTestCase +from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory + + +@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') +class TestExamService(ModuleStoreTestCase): + """ + Test for syncing exams to the exam service + """ + MODULESTORE = TEST_DATA_MONGO_AMNESTY_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 = ItemFactory.create(parent=self.course, category='chapter', display_name='Test Section') + self.course_key = str(self.course.id) + + # create one non-exam sequence + chapter2 = ItemFactory.create(parent=self.course, category='chapter', display_name='Test Homework') + ItemFactory.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_proctored'), + (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 + + sequence = ItemFactory.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_exam=is_proctored_exam, + is_practice_exam=is_practice_exam, + due=datetime.now(UTC) + timedelta(minutes=default_time_limit_minutes + 1), + 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': sequence.due, + '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 + """ + ItemFactory.create( + parent=self.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, + ) + 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 + """ + ItemFactory.create( + parent=self.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) + mock_patch_course_exams.assert_not_called() + + def test_self_paced_no_due_dates(self, mock_patch_course_exams): + self.course.self_paced = True + self.course = self.update_course(self.course, 1) + ItemFactory.create( + parent=self.chapter, + category='sequential', + display_name='Test Proctored Exam', + graded=True, + is_time_limited=True, + default_time_limit_minutes=60, + is_proctored_exam=False, + is_practice_exam=False, + due=datetime.now(UTC) + timedelta(minutes=60), + 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'] is None + + # now switch to instructor paced + # the exam will be updated with a due date + self.course.self_paced = False + self.course = self.update_course(self.course, 1) + 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'] is not None diff --git a/cms/djangoapps/contentstore/tests/test_tasks.py b/cms/djangoapps/contentstore/tests/test_tasks.py index d037f1d99c2d..74958d02c955 100644 --- a/cms/djangoapps/contentstore/tests/test_tasks.py +++ b/cms/djangoapps/contentstore/tests/test_tasks.py @@ -11,16 +11,18 @@ from django.conf import settings from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user from django.test.utils import override_settings +from edx_toggles.toggles.testutils import override_waffle_flag from opaque_keys.edx.locator import CourseLocator from organizations.models import OrganizationCourse from organizations.tests.factories import OrganizationFactory from user_tasks.models import UserTaskArtifact, UserTaskStatus -from cms.djangoapps.contentstore.tasks import export_olx, rerun_course +from cms.djangoapps.contentstore.tasks import export_olx, update_special_exams_and_publish, rerun_course from cms.djangoapps.contentstore.tests.test_libraries import LibraryTestCase from cms.djangoapps.contentstore.tests.utils import CourseTestCase from common.djangoapps.course_action_state.models import CourseRerunState from common.djangoapps.student.tests.factories import UserFactory +from openedx.core.djangoapps.course_apps.toggles import EXAMS_IDA from openedx.core.djangoapps.embargo.models import Country, CountryAccessRule, RestrictedCourse from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE @@ -167,3 +169,33 @@ def test_success(self): restricted_course=restricted_course, country=restricted_country ) + + +@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE) +class RegisterExamsTaskTestCase(CourseTestCase): + + @mock.patch('cms.djangoapps.contentstore.exams.register_exams') + @mock.patch('cms.djangoapps.contentstore.proctoring.register_special_exams') + def test_exam_service_not_enabled_success(self, _mock_register_exams_proctoring, _mock_register_exams_service): + """ edx-proctoring interface is called if exam service is not enabled """ + update_special_exams_and_publish(str(self.course.id)) + _mock_register_exams_proctoring.assert_called_once_with(self.course.id) + _mock_register_exams_service.assert_not_called() + + @mock.patch('cms.djangoapps.contentstore.exams.register_exams') + @mock.patch('cms.djangoapps.contentstore.proctoring.register_special_exams') + @override_waffle_flag(EXAMS_IDA, active=True) + def test_exam_service_enabled_success(self, _mock_register_exams_proctoring, _mock_register_exams_service): + """ exams service interface is called if exam service is enabled """ + update_special_exams_and_publish(str(self.course.id)) + _mock_register_exams_proctoring.assert_not_called() + _mock_register_exams_service.assert_called_once_with(self.course.id) + + @mock.patch('cms.djangoapps.contentstore.exams.register_exams') + @mock.patch('cms.djangoapps.contentstore.proctoring.register_special_exams') + def test_register_exams_failure(self, _mock_register_exams_proctoring, _mock_register_exams_service): + """ credit requirements update signal fires even if exam registration fails """ + with mock.patch('openedx.core.djangoapps.credit.signals.on_course_publish') as course_publish: + _mock_register_exams_proctoring.side_effect = Exception('boom!') + update_special_exams_and_publish(str(self.course.id)) + course_publish.assert_called() diff --git a/cms/envs/common.py b/cms/envs/common.py index 1e3bfbe4bce8..07647ceb4d13 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -2414,6 +2414,9 @@ COMMENTS_SERVICE_URL = 'http://localhost:18080' COMMENTS_SERVICE_KEY = 'password' +EXAMS_SERVICE_URL = 'http://localhost:8740/api/v1' +EXAMS_SERVICE_USERNAME = 'exams_service_user' + FINANCIAL_REPORTS = { 'STORAGE_TYPE': 'localfs', 'BUCKET': None, diff --git a/cms/envs/production.py b/cms/envs/production.py index ece9b85a178e..6431ad95c15d 100644 --- a/cms/envs/production.py +++ b/cms/envs/production.py @@ -561,6 +561,10 @@ def get_env_setting(setting): RETIREMENT_SERVICE_WORKER_USERNAME ) +############### Settings for Exams #################### +EXAMS_SERVICE_URL = ENV_TOKENS.get('EXAMS_SERVICE_URL', EXAMS_SERVICE_URL) +EXAMS_SERVICE_USERNAME = ENV_TOKENS.get('EXAMS_SERVICE_USERNAME', EXAMS_SERVICE_USERNAME) + ############### Settings for edx-rbac ############### SYSTEM_WIDE_ROLE_CLASSES = ENV_TOKENS.get('SYSTEM_WIDE_ROLE_CLASSES') or SYSTEM_WIDE_ROLE_CLASSES diff --git a/lms/envs/devstack.py b/lms/envs/devstack.py index cdf6011eb81d..d7c66cb20298 100644 --- a/lms/envs/devstack.py +++ b/lms/envs/devstack.py @@ -238,6 +238,9 @@ def should_show_debug_toolbar(request): # lint-amnesty, pylint: disable=missing CREDENTIALS_INTERNAL_SERVICE_URL = 'http://edx.devstack.credentials:18150' CREDENTIALS_PUBLIC_SERVICE_URL = 'http://localhost:18150' +############## Exams CONFIGURATION SETTINGS #################### +EXAMS_SERVICE_URL = 'http://localhost:8740/api/v1' + ############################### BLOCKSTORE ##################################### BLOCKSTORE_API_URL = "http://edx.devstack.blockstore:18250/api/v1/" diff --git a/lms/envs/test.py b/lms/envs/test.py index 57de824ad0e4..ad9372e09be1 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -611,6 +611,9 @@ # (ref MST-637) PROCTORING_USER_OBFUSCATION_KEY = '85920908f28904ed733fe576320db18cabd7b6cd' +############## Exams CONFIGURATION SETTINGS #################### +EXAMS_SERVICE_URL = 'http://exams.example.com/api/v1' + ############### Settings for Django Rate limit ##################### RATELIMIT_RATE = '2/m' From 5f74f0f5077eb976b327fe0ab53183b8aab8d9f6 Mon Sep 17 00:00:00 2001 From: Zach Hancock Date: Mon, 19 Sep 2022 11:00:55 -0400 Subject: [PATCH 2/5] style: self-review tweaks --- cms/djangoapps/contentstore/exams.py | 18 ++++++------------ cms/djangoapps/contentstore/tasks.py | 2 +- cms/envs/devstack-experimental.yml | 1 + 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/cms/djangoapps/contentstore/exams.py b/cms/djangoapps/contentstore/exams.py index 2d13ad6673ae..89eb228fa5f2 100644 --- a/cms/djangoapps/contentstore/exams.py +++ b/cms/djangoapps/contentstore/exams.py @@ -1,6 +1,5 @@ - """ -Utilities for working with the exam service +Code related to working with the exam service """ import json @@ -26,8 +25,8 @@ def register_exams(course_key): """ This is typically called on a course published signal. The course is examined for sequences that are marked as timed exams. Then these are registered with the exams service. - Likewise, if formerly registered exams are unmarked, then those - registered exams are marked as inactive + Likewise, if formerly registered exams are not included in the payload they will + be marked inactive by the exam service. """ if not settings.FEATURES.get('ENABLE_SPECIAL_EXAMS') or not exams_ida_enabled(course_key): # if feature is not enabled then do a quick exit @@ -37,12 +36,6 @@ def register_exams(course_key): if course is None: raise ItemNotFoundError("Course {} does not exist", str(course_key)) # lint-amnesty, pylint: disable=raising-format-tuple - if not course.enable_proctored_exams and not course.enable_timed_exams: - # TODO: I'M NOT SO SURE WE ACTUALLY WANT THIS - # likewise if course does not have these features turned on - # then quickly exit - return - # get all sequences, since they can be marked as timed/proctored exams _timed_exams = modulestore().get_items( course_key, @@ -92,7 +85,7 @@ def register_exams(course_key): 'exam_type': exam_type, 'is_active': True, 'hide_after_due': timed_exam.hide_after_due, - # backend is only required for edx-proctoring support edx-exams will maintain LTI backends + # backend is only required for continued edx-proctoring support 'backend': course.proctoring_provider, }) @@ -100,8 +93,9 @@ def register_exams(course_key): _patch_course_exams(exams_list, str(course_key)) log.info(f'Successfully registered {locations} with exam service') # pylint: disable=broad-except - except Exception: + except Exception as ex: log.exception('Failed to register exams with exam API', exc_info=True) + raise ex def _get_exams_api_client(): diff --git a/cms/djangoapps/contentstore/tasks.py b/cms/djangoapps/contentstore/tasks.py index 3c1171d16daf..aaee20fbb159 100644 --- a/cms/djangoapps/contentstore/tasks.py +++ b/cms/djangoapps/contentstore/tasks.py @@ -250,7 +250,7 @@ def update_special_exams_and_publish(course_key_str): course_key = CourseKey.from_string(course_key_str) LOGGER.info('Attempting to register exams for course %s', course_key_str) - # Call the appropriate handler for the exams IDA or the legacy edx-proctoring plugin + # Call the appropriate handler for either the exams IDA or the edx-proctoring plugin register_exams_handler = register_exams if exams_ida_enabled(course_key) else register_exams_legacy try: register_exams_handler(course_key) diff --git a/cms/envs/devstack-experimental.yml b/cms/envs/devstack-experimental.yml index 9a96454a61ea..daa1225ee852 100644 --- a/cms/envs/devstack-experimental.yml +++ b/cms/envs/devstack-experimental.yml @@ -258,6 +258,7 @@ ENTERPRISE_API_URL: http://edx.devstack.lms:18000/enterprise/api/v1 ENTERPRISE_MARKETING_FOOTER_QUERY_PARAMS: {} ENTERPRISE_SERVICE_WORKER_USERNAME: enterprise_worker EVENT_TRACKING_SEGMENTIO_EMIT_WHITELIST: [] +EXAMS_API_URL: http://localhost:8740/api/v1 EXTRA_MIDDLEWARE_CLASSES: [] FACEBOOK_API_VERSION: v2.1 FACEBOOK_APP_ID: FACEBOOK_APP_ID From c1e4d52f245a0d6d5e187af15596c17d9e37c816 Mon Sep 17 00:00:00 2001 From: Zach Hancock Date: Mon, 19 Sep 2022 11:25:00 -0400 Subject: [PATCH 3/5] test: fixup --- cms/djangoapps/contentstore/tests/test_tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/tests/test_tasks.py b/cms/djangoapps/contentstore/tests/test_tasks.py index 74958d02c955..d0fcd78210c3 100644 --- a/cms/djangoapps/contentstore/tests/test_tasks.py +++ b/cms/djangoapps/contentstore/tests/test_tasks.py @@ -172,7 +172,7 @@ def test_success(self): @override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE) -class RegisterExamsTaskTestCase(CourseTestCase): +class RegisterExamsTaskTestCase(CourseTestCase): # pylint: disable=missing-class-docstring @mock.patch('cms.djangoapps.contentstore.exams.register_exams') @mock.patch('cms.djangoapps.contentstore.proctoring.register_special_exams') From d946dfaee433490c5c30abe12d26db668a133c44 Mon Sep 17 00:00:00 2001 From: Zach Hancock Date: Mon, 19 Sep 2022 14:48:21 -0400 Subject: [PATCH 4/5] feat: update exams worker name --- cms/envs/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/envs/common.py b/cms/envs/common.py index 07647ceb4d13..27e16e5c430c 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -2415,7 +2415,7 @@ COMMENTS_SERVICE_KEY = 'password' EXAMS_SERVICE_URL = 'http://localhost:8740/api/v1' -EXAMS_SERVICE_USERNAME = 'exams_service_user' +EXAMS_SERVICE_USERNAME = 'edx_exams_worker' FINANCIAL_REPORTS = { 'STORAGE_TYPE': 'localfs', From 379b9e4f7d959a61c79a79b29e7ed1081c50a6fc Mon Sep 17 00:00:00 2001 From: Zach Hancock Date: Tue, 20 Sep 2022 10:43:15 -0400 Subject: [PATCH 5/5] style: move exam type translation to function --- cms/djangoapps/contentstore/exams.py | 33 +++++++++++++++++++--------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/cms/djangoapps/contentstore/exams.py b/cms/djangoapps/contentstore/exams.py index 89eb228fa5f2..7dc2680d0a81 100644 --- a/cms/djangoapps/contentstore/exams.py +++ b/cms/djangoapps/contentstore/exams.py @@ -66,16 +66,11 @@ def register_exams(course_key): log.info(msg) locations.append(location) - if timed_exam.is_proctored_exam: - if timed_exam.is_onboarding_exam: - exam_type = 'onboarding' - elif timed_exam.is_practice_exam: - exam_type = 'practice_proctored' - else: - exam_type = 'proctored' - else: - exam_type = 'timed' - + exam_type = get_exam_type( + timed_exam.is_proctored_exam, + timed_exam.is_practice_exam, + timed_exam.is_onboarding_exam + ) exams_list.append({ 'course_id': str(course_key), 'content_id': str(timed_exam.location), @@ -98,6 +93,24 @@ def register_exams(course_key): raise ex +def get_exam_type(is_proctored, is_practice, is_onboarding): + """ + Get the exam type string based on the proctored, practice and onboarding + attributes. + """ + if is_proctored: + if is_onboarding: + exam_type = 'onboarding' + elif is_practice: + exam_type = 'practice_proctored' + else: + exam_type = 'proctored' + else: + exam_type = 'timed' + + return exam_type + + def _get_exams_api_client(): """ Returns an API client which can be used to make Exams API requests.