diff --git a/lms/djangoapps/instructor/tests/test_api.py b/lms/djangoapps/instructor/tests/test_api.py index e8bcc81318da..51fc514c4879 100644 --- a/lms/djangoapps/instructor/tests/test_api.py +++ b/lms/djangoapps/instructor/tests/test_api.py @@ -4175,6 +4175,16 @@ def test_change_due_date_with_reason(self): # This operation regenerates the cache, so we can use cached results from edx-when. assert get_date_for_block(self.course, self.week1, self.user1, use_cached=True) == due_date + def test_reset_due_date_with_reason(self): + url = reverse('reset_due_date', kwargs={'course_id': str(self.course.id)}) + response = self.client.post(url, { + 'student': self.user1.username, + 'url': str(self.week1.location), + 'reason': 'Testing reason.' # this is optional field. + }) + assert response.status_code == 200 + assert 'Successfully reset due date for student' in response.content.decode('utf-8') + def test_change_to_invalid_due_date(self): url = reverse('change_due_date', kwargs={'course_id': str(self.course.id)}) response = self.client.post(url, { diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index 58556ee9ab02..6978eaf3fe96 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -137,7 +137,6 @@ handle_dashboard_error, keep_field_private, parse_datetime, - require_student_from_identifier, set_due_date_extension, strip_if_string, ) @@ -3035,37 +3034,59 @@ def post(self, request, course_id): due_date.strftime('%Y-%m-%d %H:%M'))) -@handle_dashboard_error -@require_POST -@ensure_csrf_cookie -@cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_course_permission(permissions.GIVE_STUDENT_EXTENSION) -@require_post_params('student', 'url') -def reset_due_date(request, course_id): +@method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True), name='dispatch') +class ResetDueDate(APIView): """ Rescinds a due date extension for a student on a particular unit. """ - course = get_course_by_id(CourseKey.from_string(course_id)) - student = require_student_from_identifier(request.POST.get('student')) - unit = find_unit(course, request.POST.get('url')) - reason = strip_tags(request.POST.get('reason', '')) + permission_classes = (IsAuthenticated, permissions.InstructorPermission) + permission_name = permissions.GIVE_STUDENT_EXTENSION + serializer_class = BlockDueDateSerializer - version = getattr(course, 'course_version', None) + @method_decorator(ensure_csrf_cookie) + def post(self, request, course_id): + """ + reset a due date extension to a student for a particular unit. + params: + url (str): The URL related to the block that needs the due date update. + student (str): The email or username of the student whose access is being modified. + reason (str): Optional param. + """ + serializer_data = self.serializer_class(data=request.data, context={'disable_due_datetime': True}) + if not serializer_data.is_valid(): + return HttpResponseBadRequest(reason=serializer_data.errors) - original_due_date = get_date_for_block(course_id, unit.location, published_version=version) + student = serializer_data.validated_data.get('student') + if not student: + response_payload = { + 'error': f'Could not find student matching identifier: {request.data.get("student")}' + } + return JsonResponse(response_payload) - set_due_date_extension(course, unit, student, None, request.user, reason=reason) - if not original_due_date: - # It's possible the normal due date was deleted after an extension was granted: - return JsonResponse( - _("Successfully removed invalid due date extension (unit has no due date).") - ) + course = get_course_by_id(CourseKey.from_string(course_id)) + unit = find_unit(course, serializer_data.validated_data.get('url')) + reason = strip_tags(serializer_data.validated_data.get('reason', '')) + + version = getattr(course, 'course_version', None) + + original_due_date = get_date_for_block(course_id, unit.location, published_version=version) - original_due_date_str = original_due_date.strftime('%Y-%m-%d %H:%M') - return JsonResponse(_( - 'Successfully reset due date for student {0} for {1} ' - 'to {2}').format(student.profile.name, _display_unit(unit), - original_due_date_str)) + try: + set_due_date_extension(course, unit, student, None, request.user, reason=reason) + if not original_due_date: + # It's possible the normal due date was deleted after an extension was granted: + return JsonResponse( + _("Successfully removed invalid due date extension (unit has no due date).") + ) + + original_due_date_str = original_due_date.strftime('%Y-%m-%d %H:%M') + return JsonResponse(_( + 'Successfully reset due date for student {0} for {1} ' + 'to {2}').format(student.profile.name, _display_unit(unit), + original_due_date_str)) + + except Exception as error: # pylint: disable=broad-except + return JsonResponse({'error': str(error)}, status=400) @handle_dashboard_error diff --git a/lms/djangoapps/instructor/views/api_urls.py b/lms/djangoapps/instructor/views/api_urls.py index 9c0939a1c1b8..a248b46ae531 100644 --- a/lms/djangoapps/instructor/views/api_urls.py +++ b/lms/djangoapps/instructor/views/api_urls.py @@ -51,7 +51,7 @@ path('update_forum_role_membership', api.update_forum_role_membership, name='update_forum_role_membership'), path('change_due_date', api.ChangeDueDate.as_view(), name='change_due_date'), path('send_email', api.SendEmail.as_view(), name='send_email'), - path('reset_due_date', api.reset_due_date, name='reset_due_date'), + path('reset_due_date', api.ResetDueDate.as_view(), name='reset_due_date'), path('show_unit_extensions', api.show_unit_extensions, name='show_unit_extensions'), path('show_student_extensions', api.ShowStudentExtensions.as_view(), name='show_student_extensions'), diff --git a/lms/djangoapps/instructor/views/serializer.py b/lms/djangoapps/instructor/views/serializer.py index da91eba43124..5d123ad66c81 100644 --- a/lms/djangoapps/instructor/views/serializer.py +++ b/lms/djangoapps/instructor/views/serializer.py @@ -215,3 +215,10 @@ def validate_student(self, value): return None return user + + def __init__(self, *args, **kwargs): + # Get context to check if `due_datetime` should be optional + disable_due_datetime = kwargs.get('context', {}).get('disable_due_datetime', False) + super().__init__(*args, **kwargs) + if disable_due_datetime: + self.fields['due_datetime'].required = False