From 452433a7316a8a11eb162552b58f31d36f8861f2 Mon Sep 17 00:00:00 2001 From: Nathan Sprenkle Date: Fri, 7 Jul 2023 10:42:58 -0400 Subject: [PATCH] feat: configurable roll-forward of flex grading (#32584) Add ability to roll-forward ORA flex peer grading feature. Where enabled for an Organization or course, flex peer grading will be turned on at the course level for new course runs and course reruns. Where disabled, a new course or course rerun will preserve existing / default setting value. --- .../contentstore/tests/test_contentstore.py | 34 ++++++++++++ .../tests/test_course_create_rerun.py | 52 +++++++++++++++++++ cms/djangoapps/contentstore/toggles.py | 23 ++++++++ cms/djangoapps/contentstore/views/course.py | 13 ++++- 4 files changed, 121 insertions(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index f7046cc01ab7..9162598645b9 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -1080,6 +1080,36 @@ def test_create_course(self): """Test new course creation - happy path""" self.assert_created_course() + @ddt.data(True, False) + @mock.patch( + 'cms.djangoapps.contentstore.views.course.default_enable_flexible_peer_openassessments' + ) + def test_create_course__default_enable_flexible_peer_openassessments( + self, + mock_toggle_state, + mock_default_enable_flexible_peer_openassessments + ): + """ + Test that flex peer grading is forced on, when enabled + """ + # Given a new course run + test_course_data = {} + test_course_data.update(self.course_data) + course_key = _get_course_id(self.store, test_course_data) + + # ... with org configured to / not to enable flex grading + mock_default_enable_flexible_peer_openassessments.return_value = mock_toggle_state + + # When I create a new course + new_course_data = _create_course(self, course_key, test_course_data) + + # Then the process completes successfully + new_course_key = CourseKey.from_string(new_course_data['course_key']) + new_course = self.store.get_course(new_course_key) + + # ... and our setting got toggled appropriately on the course + self.assertEqual(new_course.force_on_flexible_peer_openassessments, mock_toggle_state) + @override_settings(DEFAULT_COURSE_LANGUAGE='hr') def test_create_course_default_language(self): """Test new course creation and verify default language""" @@ -2104,6 +2134,8 @@ def test_accessibility(self): def _create_course(test, course_key, course_data): """ Creates a course via an AJAX request and verifies the URL returned in the response. + + Returns the data of the POST response """ course_url = get_url('course_handler', course_key, 'course_key_string') response = test.client.ajax_post(course_url, course_data) @@ -2112,6 +2144,8 @@ def _create_course(test, course_key, course_data): test.assertNotIn('ErrMsg', data) test.assertEqual(data['url'], course_url) + return data + def _get_course_id(store, course_data): """Returns the course ID.""" diff --git a/cms/djangoapps/contentstore/tests/test_course_create_rerun.py b/cms/djangoapps/contentstore/tests/test_course_create_rerun.py index 1d1b1177d0dc..264d6de8ff2f 100644 --- a/cms/djangoapps/contentstore/tests/test_course_create_rerun.py +++ b/cms/djangoapps/contentstore/tests/test_course_create_rerun.py @@ -4,6 +4,7 @@ import datetime +from itertools import product from unittest import mock import ddt @@ -317,3 +318,54 @@ def test_course_creation_without_permission_for_specific_organization(self): 'run': '2021_T1' }) self.assertEqual(response.status_code, 403) + + @ddt.data(*product([True, False], [True, False])) + @ddt.unpack + @mock.patch( + 'cms.djangoapps.contentstore.views.course.default_enable_flexible_peer_openassessments' + ) + def test_default_enable_flexible_peer_openassessments_on_rerun( + self, + mock_toggle_state, + mock_original_course_setting, + mock_default_enable_flexible_peer_openassessments + ): + """ + Test that flex peer grading is forced on, when enabled + """ + # Given a valid course to rerun + add_organization({ + 'name': 'Test Flex Grading', + 'short_name': self.source_course_key.org, + 'description': 'Test roll-forward of flex grading setting', + }) + source_course = self.store.get_course(self.source_course_key) + source_course.force_on_flexible_peer_openassessments = mock_original_course_setting + self.store.update_item(source_course, self.user.id) + mock_default_enable_flexible_peer_openassessments.return_value = mock_toggle_state + + # When I create a new course + response = self.client.ajax_post(self.course_create_rerun_url, { + 'source_course_key': str(self.source_course_key), + 'org': self.source_course_key.org, + 'course': self.source_course_key.course, + 'run': 'copy', + 'display_name': 'New, exciting course!', + }) + + # Then the process completes successfully + self.assertEqual(response.status_code, 200) + + data = parse_json(response) + dest_course_key = CourseKey.from_string(data['destination_course_key']) + dest_course = self.store.get_course(dest_course_key) + + # ... and our setting got enabled appropriately on our new course + if mock_toggle_state: + self.assertTrue(dest_course.force_on_flexible_peer_openassessments) + # ... or preserved if the default enable setting is not on + else: + self.assertEqual( + source_course.force_on_flexible_peer_openassessments, + dest_course.force_on_flexible_peer_openassessments + ) diff --git a/cms/djangoapps/contentstore/toggles.py b/cms/djangoapps/contentstore/toggles.py index 34b2ae783ae4..438f99f97b01 100644 --- a/cms/djangoapps/contentstore/toggles.py +++ b/cms/djangoapps/contentstore/toggles.py @@ -476,3 +476,26 @@ def use_new_course_team_page(course_key): Returns a boolean if new studio course team mfe is enabled """ return ENABLE_NEW_STUDIO_COURSE_TEAM_PAGE.is_enabled(course_key) + + +# .. toggle_name: contentstore.default_enable_flexible_peer_openassessments +# .. toggle_implementation: CourseWaffleFlag +# .. toggle_default: False +# .. toggle_description: This flag turns on the force_on_flexible_peer_openassessments +# setting for course reruns or new courses, where enabled. +# .. toggle_use_cases: temporary +# .. toggle_creation_date: 2023-06-27 +# .. toggle_target_removal_date: 2024-01-27 +# .. toggle_tickets: AU-1289 +# .. toggle_warning: +DEFAULT_ENABLE_FLEXIBLE_PEER_OPENASSESSMENTS = CourseWaffleFlag( + f'{CONTENTSTORE_NAMESPACE}.default_enable_flexible_peer_openassessments', __name__) + + +def default_enable_flexible_peer_openassessments(course_key): + """ + Returns a boolean if ORA flexible peer grading should be toggled on for a + course rerun or new course. We expect this to be set at the organization + level to opt in/out of rolling forward this feature. + """ + return DEFAULT_ENABLE_FLEXIBLE_PEER_OPENASSESSMENTS.is_enabled(course_key) diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index 4ddac587f002..0b33525c99ce 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -86,13 +86,14 @@ from ..courseware_index import CoursewareSearchIndexer, SearchIndexingError from ..tasks import rerun_course as rerun_course_task from ..toggles import ( + default_enable_flexible_peer_openassessments, split_library_view_on_dashboard, use_new_course_outline_page, use_new_home_page, use_new_updates_page, use_new_advanced_settings_page, use_new_grading_page, - use_new_schedule_details_page, + use_new_schedule_details_page ) from ..utils import ( add_instructor, @@ -993,6 +994,12 @@ def create_new_course(user, org, number, run, fields): new_course = create_new_course_in_store(store_for_new_course, user, org, number, run, fields) add_organization_course(org_data, new_course.id) update_course_discussions_settings(new_course.id) + + # Enable certain fields rolling forward, where configured + if default_enable_flexible_peer_openassessments(new_course.id): + new_course.force_on_flexible_peer_openassessments = True + modulestore().update_item(new_course, new_course.published_by) + return new_course @@ -1056,6 +1063,10 @@ def rerun_course(user, source_course_key, org, number, run, fields, background=T fields['enrollment_end'] = None fields['video_upload_pipeline'] = {} + # Enable certain fields rolling forward, where configured + if default_enable_flexible_peer_openassessments(source_course_key): + fields['force_on_flexible_peer_openassessments'] = True + json_fields = json.dumps(fields, cls=EdxJSONEncoder) args = [str(source_course_key), str(destination_course_key), user.id, json_fields]