From 3c31cdb9ebbf092c1d731e79c79b1290e4d8a032 Mon Sep 17 00:00:00 2001 From: Kyrylo Kholodenko Date: Fri, 19 Sep 2025 15:03:06 +0300 Subject: [PATCH] refactor: rename and refactor functions and view --- .../api/v1/tests/test_utils.py | 35 +++++++- .../api/v1/tests/test_views.py | 21 +++-- .../features/course_experience/api/v1/urls.py | 8 +- .../course_experience/api/v1/utils.py | 80 +++++++++++++++++-- .../course_experience/api/v1/views.py | 24 ++---- 5 files changed, 127 insertions(+), 41 deletions(-) diff --git a/openedx/features/course_experience/api/v1/tests/test_utils.py b/openedx/features/course_experience/api/v1/tests/test_utils.py index 10b5882918..741a2d7658 100644 --- a/openedx/features/course_experience/api/v1/tests/test_utils.py +++ b/openedx/features/course_experience/api/v1/tests/test_utils.py @@ -13,7 +13,11 @@ from common.djangoapps.util.testing import EventTestMixin from lms.djangoapps.course_home_api.tests.utils import BaseCourseHomeTests from lms.djangoapps.courseware.tests.helpers import MasqueradeMixin from openedx.core.djangoapps.schedules.models import Schedule -from openedx.features.course_experience.api.v1.utils import reset_deadlines_for_course +from openedx.features.course_experience.api.v1.utils import ( + reset_deadlines_for_course, + reset_course_deadlines_for_user, + reset_bulk_course_deadlines +) from xmodule.modulestore.tests.factories import CourseFactory @@ -82,3 +86,32 @@ class TestResetDeadlinesForCourse(EventTestMixin, BaseCourseHomeTests, Masquerad org_key=self.course.org, user_id=student_user_id, ) + + def test_reset_course_deadlines_for_user(self): + """Test the reset_course_deadlines_for_user utility function directly""" + enrollment = CourseEnrollment.enroll(self.user, self.course.id, CourseMode.VERIFIED) + enrollment.schedule.start_date = timezone.now() - datetime.timedelta(days=100) + enrollment.schedule.save() + + result = reset_course_deadlines_for_user(self.user, self.course.id) + + assert result is True + assert enrollment.schedule.start_date < Schedule.objects.get(id=enrollment.schedule.id).start_date + + def test_reset_bulk_course_deadlines(self): + """Test the reset_bulk_course_deadlines utility function""" + enrollment = CourseEnrollment.enroll(self.user, self.course.id, CourseMode.VERIFIED) + enrollment.schedule.start_date = timezone.now() - datetime.timedelta(days=100) + enrollment.schedule.save() + + request = APIRequestFactory().post( + reverse("course-experience-reset-all-course-deadlines"), {} + ) + request.user = self.user + + success_keys, failed_keys = reset_bulk_course_deadlines(request, [self.course.id], {}) + + assert len(success_keys) == 1 + assert self.course.id in success_keys + assert len(failed_keys) == 0 + assert enrollment.schedule.start_date < Schedule.objects.get(id=enrollment.schedule.id).start_date diff --git a/openedx/features/course_experience/api/v1/tests/test_views.py b/openedx/features/course_experience/api/v1/tests/test_views.py index d7e8f6cafc..097eb2c18c 100644 --- a/openedx/features/course_experience/api/v1/tests/test_views.py +++ b/openedx/features/course_experience/api/v1/tests/test_views.py @@ -89,30 +89,27 @@ class ResetAllRelativeCourseDeadlinesViewTests(BaseCourseHomeTests, MasqueradeMi self.enrollment.schedule.start_date = timezone.now() - datetime.timedelta(days=100) self.enrollment.schedule.save() - def test_reset_all_relative_course_deadlines(self): + def test_reset_all_course_deadlines(self): """ - Test reset all relative course deadlines endpoint + Test reset all course deadlines endpoint """ response = self.client.post( - reverse("course-experience-reset-all-relative-course-deadlines"), + reverse("course-experience-reset-all-course-deadlines"), {}, ) assert response.status_code == 200 assert self.enrollment.schedule.start_date < Schedule.objects.get(id=self.enrollment.schedule.id).start_date assert str(self.course.id) in response.data.get("success_course_keys") - def test_reset_all_relative_course_deadlines_failure(self): + def test_reset_all_course_deadlines_failure(self): """ - Raise exception on reset_deadlines_for_course and assert if failure course id is returned + Raise exception on reset_bulk_course_deadlines and assert if failure course id is returned """ with mock.patch( - "openedx.features.course_experience.api.v1.views.reset_deadlines_for_course", - side_effect=Exception("Test Exception"), + "openedx.features.course_experience.api.v1.views.reset_bulk_course_deadlines", + return_value=([], [self.course.id]), ): - response = self.client.post( - reverse("course-experience-reset-all-relative-course-deadlines"), - {}, - ) + response = self.client.post(reverse("course-experience-reset-all-course-deadlines"), {}) assert response.status_code == 200 assert str(self.course.id) in response.data.get("failed_course_keys") @@ -123,7 +120,7 @@ class ResetAllRelativeCourseDeadlinesViewTests(BaseCourseHomeTests, MasqueradeMi """ self.client.logout() response = self.client.post( - reverse("course-experience-reset-all-relative-course-deadlines"), + reverse("course-experience-reset-all-course-deadlines"), {}, ) assert response.status_code == 401 diff --git a/openedx/features/course_experience/api/v1/urls.py b/openedx/features/course_experience/api/v1/urls.py index 7e5a0936c2..30d7c55d29 100644 --- a/openedx/features/course_experience/api/v1/urls.py +++ b/openedx/features/course_experience/api/v1/urls.py @@ -8,7 +8,7 @@ from django.urls import re_path from openedx.features.course_experience.api.v1.views import ( reset_course_deadlines, - reset_all_relative_course_deadlines, + reset_all_course_deadlines, CourseDeadlinesMobileView, ) @@ -22,9 +22,9 @@ urlpatterns += [ name='course-experience-reset-course-deadlines' ), re_path( - r'v1/reset_all_relative_course_deadlines/', - reset_all_relative_course_deadlines, - name='course-experience-reset-all-relative-course-deadlines', + r'v1/reset_all_course_deadlines/', + reset_all_course_deadlines, + name='course-experience-reset-all-course-deadlines', ) ] diff --git a/openedx/features/course_experience/api/v1/utils.py b/openedx/features/course_experience/api/v1/utils.py index aabfadb540..8f9205b0f1 100644 --- a/openedx/features/course_experience/api/v1/utils.py +++ b/openedx/features/course_experience/api/v1/utils.py @@ -2,6 +2,7 @@ """ Course Experience API utilities. """ +import logging from eventtracking import tracker from lms.djangoapps.courseware.access import has_access @@ -11,6 +12,76 @@ from openedx.core.djangoapps.schedules.utils import reset_self_paced_schedule from openedx.features.course_experience.utils import dates_banner_should_display +logger = logging.getLogger(__name__) + + +def reset_course_deadlines_for_user(user, course_key): + """ + Core function to reset deadlines for a single course and user. + + Args: + user: The user object + course_key: The course key + + Returns: + bool: True if deadlines were reset, False if gated content prevents reset + """ + # We ignore the missed_deadlines because this util is used in endpoint from the Learning MFE for + # learners who have remaining attempts on a problem and reset their due dates in order to + # submit additional attempts. This can apply for 'completed' (submitted) content that would + # not be marked as past_due + _missed_deadlines, missed_gated_content = dates_banner_should_display(course_key, user) + if not missed_gated_content: + reset_self_paced_schedule(user, course_key) + return True + return False + + +def reset_bulk_course_deadlines(request, course_keys, research_event_data={}): # lint-amnesty, pylint: disable=dangerous-default-value + """ + Reset deadlines for multiple courses for the requesting user. + + Args: + request (Request): The request object + course_keys (list): List of course keys + research_event_data (dict): Any data that should be included in the research tracking event + + Returns: + tuple: (success_course_keys, failed_course_keys) + """ + success_course_keys = [] + failed_course_keys = [] + + for course_key in course_keys: + try: + course_masquerade, user = setup_masquerade( + request, + course_key, + has_access(request.user, 'staff', course_key) + ) + + if reset_course_deadlines_for_user(user, course_key): + success_course_keys.append(course_key) + + course_overview = course_detail(request, user.username, course_key) + + research_event_data.update({ + 'courserun_key': str(course_key), + 'is_masquerading': is_masquerading(user, course_key, course_masquerade), + 'is_staff': has_access(user, 'staff', course_key).has_access, + 'org_key': course_overview.display_org_with_default, + 'user_id': user.id, + }) + tracker.emit('edx.ui.lms.reset_deadlines.clicked', research_event_data) + else: + failed_course_keys.append(course_key) + except Exception: # pylint: disable=broad-exception-caught + logger.exception('Error occurred while trying to reset deadlines!') + failed_course_keys.append(course_key) + + return success_course_keys, failed_course_keys + + def reset_deadlines_for_course(request, course_key, research_event_data={}): # lint-amnesty, pylint: disable=dangerous-default-value """ Set the start_date of a schedule to today, which in turn will adjust due dates for @@ -29,14 +100,7 @@ def reset_deadlines_for_course(request, course_key, research_event_data={}): # has_access(request.user, 'staff', course_key) ) - # We ignore the missed_deadlines because this util is used in endpoint from the Learning MFE for - # learners who have remaining attempts on a problem and reset their due dates in order to - # submit additional attempts. This can apply for 'completed' (submitted) content that would - # not be marked as past_due - _missed_deadlines, missed_gated_content = dates_banner_should_display(course_key, user) - if not missed_gated_content: - reset_self_paced_schedule(user, course_key) - + if reset_course_deadlines_for_user(user, course_key): course_overview = course_detail(request, user.username, course_key) # For context here, research_event_data should already contain `location` indicating # the page/location dates were reset from and could also contain `block_id` if reset diff --git a/openedx/features/course_experience/api/v1/views.py b/openedx/features/course_experience/api/v1/views.py index 10f8b8b4bf..d822fdd65c 100644 --- a/openedx/features/course_experience/api/v1/views.py +++ b/openedx/features/course_experience/api/v1/views.py @@ -23,7 +23,7 @@ from lms.djangoapps.courseware.courses import get_course_with_access from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser from openedx.features.course_experience.api.v1.serializers import CourseDeadlinesMobileSerializer from openedx.features.course_experience.url_helpers import get_learning_mfe_home_url -from openedx.features.course_experience.api.v1.utils import reset_deadlines_for_course +from openedx.features.course_experience.api.v1.utils import reset_deadlines_for_course, reset_bulk_course_deadlines log = logging.getLogger(__name__) @@ -86,7 +86,7 @@ def reset_course_deadlines(request): ) ) @permission_classes((IsAuthenticated,)) -def reset_all_relative_course_deadlines(request): +def reset_all_course_deadlines(request): """ Set the start_date of a schedule to today for all enrolled courses @@ -99,26 +99,18 @@ def reset_all_relative_course_deadlines(request): failed_course_keys: list of course keys for which deadlines could not be reset """ research_event_data = request.data.get("research_event_data", {}) - course_keys = ( + course_keys = list( CourseEnrollment.enrollments_for_user(request.user).select_related("course").values_list("course_id", flat=True) ) - failed_course_keys = [] - success_course_keys = [] - - for course_key in course_keys: - try: - reset_deadlines_for_course(request, course_key, research_event_data) - success_course_keys.append(str(course_key)) - except Exception: # pylint: disable=broad-exception-caught - log.exception(f"Error occurred while trying to reset deadlines for course {course_key}!") - failed_course_keys.append(str(course_key)) - continue + success_course_keys, failed_course_keys = reset_bulk_course_deadlines( + request, course_keys, research_event_data + ) return Response( { - "success_course_keys": success_course_keys, - "failed_course_keys": failed_course_keys, + "success_course_keys": [str(key) for key in success_course_keys], + "failed_course_keys": [str(key) for key in failed_course_keys], } )