diff --git a/openedx/features/course_experience/__init__.py b/openedx/features/course_experience/__init__.py index 52b870033a26..df8179623593 100644 --- a/openedx/features/course_experience/__init__.py +++ b/openedx/features/course_experience/__init__.py @@ -50,6 +50,16 @@ # .. toggle_tickets: https://openedx.atlassian.net/browse/AA-27 RELATIVE_DATES_FLAG = CourseWaffleFlag(f'{WAFFLE_FLAG_NAMESPACE}.relative_dates', __name__) # lint-amnesty, pylint: disable=toggle-missing-annotation +# .. toggle_name: course_experience.relative_dates_disable_reset +# .. toggle_implementation: CourseWaffleFlag +# .. toggle_default: False +# .. toggle_description: Waffle flag to disable resetting deadlines by learners in self-paced courses. The 'Dates' tab +# will no longer show a banner about missed deadlines. The deadlines banner will also be hidden on unit pages. +# .. toggle_use_cases: open_edx +# .. toggle_creation_date: 2023-04-27 +# .. toggle_warning: For this toggle to have an effect, the RELATIVE_DATES_FLAG toggle must be enabled, too. +RELATIVE_DATES_DISABLE_RESET_FLAG = CourseWaffleFlag(f'{WAFFLE_FLAG_NAMESPACE}.relative_dates_disable_reset', __name__) + # .. toggle_name: course_experience.calendar_sync # .. toggle_implementation: CourseWaffleFlag # .. toggle_default: False 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 f67768c38f76..8cef39053b3e 100644 --- a/openedx/features/course_experience/api/v1/tests/test_views.py +++ b/openedx/features/course_experience/api/v1/tests/test_views.py @@ -2,18 +2,20 @@ Tests for reset deadlines endpoint. """ import datetime -import ddt +import ddt from django.urls import reverse from django.utils import timezone +from edx_toggles.toggles.testutils import override_waffle_flag from common.djangoapps.course_modes.models import CourseMode from common.djangoapps.student.models import CourseEnrollment from common.djangoapps.util.testing import EventTestMixin -from lms.djangoapps.courseware.tests.helpers import MasqueradeMixin 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 xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order +from openedx.features.course_experience import RELATIVE_DATES_DISABLE_RESET_FLAG, RELATIVE_DATES_FLAG +from xmodule.modulestore.tests.factories import CourseFactory @ddt.ddt @@ -25,17 +27,22 @@ def setUp(self): # pylint: disable=arguments-differ # Need to supply tracker name for the EventTestMixin. Also, EventTestMixin needs to come # first in class inheritance so the setUp call here appropriately works super().setUp('openedx.features.course_experience.api.v1.views.tracker') + self.course = CourseFactory.create(self_paced=True, start=timezone.now() - datetime.timedelta(days=1000)) def test_reset_deadlines(self): - CourseEnrollment.enroll(self.user, self.course.id, CourseMode.VERIFIED) + enrollment = CourseEnrollment.enroll(self.user, self.course.id, CourseMode.VERIFIED) + enrollment.schedule.start_date = timezone.now() - datetime.timedelta(days=100) + enrollment.schedule.save() # Test body with incorrect body param (course_key is required) response = self.client.post(reverse('course-experience-reset-course-deadlines'), {'course': self.course.id}) assert response.status_code == 400 + assert enrollment.schedule == Schedule.objects.get(id=enrollment.schedule.id) self.assert_no_events_were_emitted() # Test correct post body response = self.client.post(reverse('course-experience-reset-course-deadlines'), {'course_key': self.course.id}) assert response.status_code == 200 + assert enrollment.schedule.start_date < Schedule.objects.get(id=enrollment.schedule.id).start_date self.assert_event_emitted( 'edx.ui.lms.reset_deadlines.clicked', courserun_key=str(self.course.id), @@ -45,33 +52,44 @@ def test_reset_deadlines(self): user_id=self.user.id, ) + @override_waffle_flag(RELATIVE_DATES_FLAG, active=True) + @override_waffle_flag(RELATIVE_DATES_DISABLE_RESET_FLAG, active=True) + def test_reset_deadlines_disabled(self): + enrollment = CourseEnrollment.enroll(self.user, self.course.id, CourseMode.VERIFIED) + enrollment.schedule.start_date = timezone.now() - datetime.timedelta(days=100) + enrollment.schedule.save() + + response = self.client.post(reverse('course-experience-reset-course-deadlines'), {'course_key': self.course.id}) + assert response.status_code == 200 + assert enrollment.schedule == Schedule.objects.get(id=enrollment.schedule.id) + self.assert_no_events_were_emitted() + def test_reset_deadlines_with_masquerade(self): """ Staff users should be able to masquerade as a learner and reset the learner's schedule """ - course = CourseFactory.create(self_paced=True, start=timezone.now() - datetime.timedelta(days=1)) student_username = self.user.username student_user_id = self.user.id - student_enrollment = CourseEnrollment.enroll(self.user, course.id) + student_enrollment = CourseEnrollment.enroll(self.user, self.course.id) student_enrollment.schedule.start_date = timezone.now() - datetime.timedelta(days=100) student_enrollment.schedule.save() - staff_enrollment = CourseEnrollment.enroll(self.staff_user, course.id) + staff_enrollment = CourseEnrollment.enroll(self.staff_user, self.course.id) staff_enrollment.schedule.start_date = timezone.now() - datetime.timedelta(days=30) staff_enrollment.schedule.save() self.switch_to_staff() - self.update_masquerade(course=course, username=student_username) + self.update_masquerade(course=self.course, username=student_username) - self.client.post(reverse('course-experience-reset-course-deadlines'), {'course_key': course.id}) + self.client.post(reverse('course-experience-reset-course-deadlines'), {'course_key': self.course.id}) updated_schedule = Schedule.objects.get(id=student_enrollment.schedule.id) assert updated_schedule.start_date.date() == datetime.datetime.today().date() updated_staff_schedule = Schedule.objects.get(id=staff_enrollment.schedule.id) assert updated_staff_schedule.start_date == staff_enrollment.schedule.start_date self.assert_event_emitted( 'edx.ui.lms.reset_deadlines.clicked', - courserun_key=str(course.id), + courserun_key=str(self.course.id), is_masquerading=True, is_staff=False, - org_key=course.org, + org_key=self.course.org, user_id=student_user_id, ) diff --git a/openedx/features/course_experience/utils.py b/openedx/features/course_experience/utils.py index 451d769eeade..d58b54f6139f 100644 --- a/openedx/features/course_experience/utils.py +++ b/openedx/features/course_experience/utils.py @@ -9,7 +9,7 @@ from lms.djangoapps.course_blocks.api import get_course_blocks from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.lib.cache_utils import request_cached -from openedx.features.course_experience import RELATIVE_DATES_FLAG +from openedx.features.course_experience import RELATIVE_DATES_DISABLE_RESET_FLAG, RELATIVE_DATES_FLAG from common.djangoapps.student.models import CourseEnrollment from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order @@ -155,6 +155,14 @@ def dates_banner_should_display(course_key, user): if not RELATIVE_DATES_FLAG.is_enabled(course_key): return False, False + if RELATIVE_DATES_DISABLE_RESET_FLAG.is_enabled(course_key): + # The `missed_deadlines` value is ignored by `reset_course_deadlines` views. Instead, they check the value of + # `missed_gated_content` to determine if learners can reset the deadlines by themselves. + # We could have added this logic directly to `reset_self_paced_schedule`, but this function is used in other + # places (e.g., when an enrollment mode is changed). We want this flag to affect only the use case when + # learners try to reset their deadlines. + return False, True + course_overview = CourseOverview.objects.get(id=str(course_key)) # Only display the banner for self-paced courses