diff --git a/common/djangoapps/util/views.py b/common/djangoapps/util/views.py index d7c1faf00bf4aecfacf4c5a6348b589d7ae0e0ea..9bb769dc1435f5b1c2c14d03617af745cbcecb4f 100644 --- a/common/djangoapps/util/views.py +++ b/common/djangoapps/util/views.py @@ -196,6 +196,9 @@ def reset_course_deadlines(request): """ Set the start_date of a schedule to today, which in turn will adjust due dates for sequentials belonging to a self paced course + + IMPORTANT NOTE: If updates are happening to the logic here, ALSO UPDATE the `reset_course_deadlines` + function in openedx/features/course_experience/api/v1/views.py as well. """ course_key = CourseKey.from_string(request.POST.get('course_id')) _course_masquerade, user = setup_masquerade( diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index 582438de5018889f79b28fd428ff7592df07150d..72b634ba6bf0cc6bddba3e52bdcf75435b60d8d2 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -1631,6 +1631,9 @@ def render_xblock(request, usage_key_string, check_if_enrolled=True): u"Rendering of the xblock view '{}' is not supported.".format(bleach.clean(requested_view, strip=True)) ) + staff_access = has_access(request.user, 'staff', course_key) + _course_masquerade, request.user = setup_masquerade(request, course_key, staff_access) + with modulestore().bulk_operations(course_key): # verify the user has access to the course, including enrollment check try: @@ -1668,7 +1671,7 @@ def render_xblock(request, usage_key_string, check_if_enrolled=True): 'disable_window_wrap': True, 'enable_completion_on_view_service': enable_completion_on_view_service, 'edx_notes_enabled': is_feature_enabled(course, request.user), - 'staff_access': bool(request.user.has_perm(VIEW_XQA_INTERFACE, course)), + 'staff_access': staff_access, 'xqa_server': settings.FEATURES.get('XQA_SERVER', 'http://your_xqa_server.com'), 'missed_deadlines': missed_deadlines, 'missed_gated_content': missed_gated_content, 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 795e61d74fe21f0e4714f1c3a8e68b5f323f88ad..7f18409aa3de183e0066173c118d7bbef9c36cd4 100644 --- a/openedx/features/course_experience/api/v1/tests/test_views.py +++ b/openedx/features/course_experience/api/v1/tests/test_views.py @@ -1,23 +1,29 @@ """ Tests for reset deadlines endpoint. """ +import datetime import ddt from django.urls import reverse +from django.utils import timezone +from mock import patch from common.djangoapps.course_modes.models import CourseMode -from lms.djangoapps.course_home_api.tests.utils import BaseCourseHomeTests from common.djangoapps.student.models import CourseEnrollment +from lms.djangoapps.courseware.tests.helpers import MasqueradeMixin +from lms.djangoapps.course_home_api.tests.utils import BaseCourseHomeTests +from openedx.core.djangoapps.schedules.models import Schedule +from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory +from xmodule.modulestore.tests.factories import CourseFactory @ddt.ddt -class ResetCourseDeadlinesViewTests(BaseCourseHomeTests): +class ResetCourseDeadlinesViewTests(BaseCourseHomeTests, MasqueradeMixin): """ Tests for reset deadlines endpoint. """ - @ddt.data(CourseMode.VERIFIED) - def test_reset_deadlines(self, enrollment_mode): - CourseEnrollment.enroll(self.user, self.course.id, enrollment_mode) + def test_reset_deadlines(self): + CourseEnrollment.enroll(self.user, self.course.id, CourseMode.VERIFIED) # Test correct post body response = self.client.post(reverse('course-experience-reset-course-deadlines'), {'course_key': self.course.id}) self.assertEqual(response.status_code, 200) @@ -30,6 +36,32 @@ class ResetCourseDeadlinesViewTests(BaseCourseHomeTests): ) self.assertEqual(response.status_code, 400) + 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) + student_username = self.user.username + student_enrollment = CourseEnrollment.enroll(self.user, course.id) + student_schedule = ScheduleFactory.create( + start_date=timezone.now() - datetime.timedelta(days=100), + enrollment=student_enrollment + ) + staff_schedule = ScheduleFactory( + start_date=timezone.now() - datetime.timedelta(days=30), + enrollment__course__id=course.id, + enrollment__user=self.staff_user, + ) + + self.switch_to_staff() + self.update_masquerade(course=course, username=student_username) + + with patch('openedx.features.course_experience.api.v1.views.dates_banner_should_display', + return_value=(True, False)): + self.client.post(reverse('course-experience-reset-course-deadlines'), {'course_key': course.id}) + updated_schedule = Schedule.objects.get(id=student_schedule.id) + self.assertEqual(updated_schedule.start_date.date(), datetime.datetime.today().date()) + updated_staff_schedule = Schedule.objects.get(id=staff_schedule.id) + self.assertEqual(updated_staff_schedule.start_date, staff_schedule.start_date) + def test_post_unauthenticated_user(self): self.client.logout() response = self.client.post(reverse('course-experience-reset-course-deadlines'), {'course_key': self.course.id}) diff --git a/openedx/features/course_experience/api/v1/views.py b/openedx/features/course_experience/api/v1/views.py index 676f2e2d5a47f0db39f6a5002bfb8ef126831f89..41ba6803323eb90f847665b84823b81490b3eb65 100644 --- a/openedx/features/course_experience/api/v1/views.py +++ b/openedx/features/course_experience/api/v1/views.py @@ -1,4 +1,7 @@ -import six +""" +Views for Course Experience API. +""" +import logging from django.conf import settings from django.urls import reverse @@ -13,15 +16,20 @@ from rest_framework.generics import RetrieveAPIView from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser +from opaque_keys.edx.keys import CourseKey from lms.djangoapps.course_home_api.toggles import course_home_mfe_dates_tab_is_active from lms.djangoapps.course_home_api.utils import get_microfrontend_url +from lms.djangoapps.courseware.access import has_access from lms.djangoapps.courseware.courses import get_course_with_access +from lms.djangoapps.courseware.masquerade import setup_masquerade -from opaque_keys.edx.keys import CourseKey from openedx.core.djangoapps.schedules.utils import reset_self_paced_schedule from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser from openedx.features.course_experience.api.v1.serializers import CourseDeadlinesMobileSerializer +from openedx.features.course_experience.utils import dates_banner_should_display + +log = logging.getLogger(__name__) class UnableToResetDeadlines(APIException): @@ -36,6 +44,13 @@ class UnableToResetDeadlines(APIException): )) @permission_classes((IsAuthenticated,)) def reset_course_deadlines(request): + """ + Set the start_date of a schedule to today, which in turn will adjust due dates for + sequentials belonging to a self paced course + + IMPORTANT NOTE: If updates are happening to the logic here, ALSO UPDATE the `reset_course_deadlines` + function in common/djangoapps/util/views.py as well. + """ course_key = request.data.get('course_key', None) # If body doesnt contain 'course_key', return 400 to client. @@ -47,13 +62,21 @@ def reset_course_deadlines(request): raise ParseError(_("Only 'course_key' is expected.")) try: - reset_self_paced_schedule(request.user, course_key) - - key = CourseKey.from_string(course_key) - if course_home_mfe_dates_tab_is_active(key): - body_link = get_microfrontend_url(course_key=course_key, view_name='dates') + course_key = CourseKey.from_string(course_key) + _course_masquerade, user = setup_masquerade( + request, + course_key, + has_access(request.user, 'staff', course_key) + ) + + missed_deadlines, missed_gated_content = dates_banner_should_display(course_key, user) + if missed_deadlines and not missed_gated_content: + reset_self_paced_schedule(user, course_key) + + if course_home_mfe_dates_tab_is_active(course_key): + body_link = get_microfrontend_url(course_key=str(course_key), view_name='dates') else: - body_link = '{}{}'.format(settings.LMS_ROOT_URL, reverse('dates', args=[six.text_type(course_key)])) + body_link = '{}{}'.format(settings.LMS_ROOT_URL, reverse('dates', args=[str(course_key)])) return Response({ 'body': format_html('<a href="{}">{}</a>', body_link, _('View all dates')), @@ -62,7 +85,8 @@ def reset_course_deadlines(request): 'link_text': _('View all dates'), 'message': _('Deadlines successfully reset.'), }) - except Exception: + except Exception as e: + log.exception(e) raise UnableToResetDeadlines