From ed794913db2ae35a41931e9cf52b335400db771a Mon Sep 17 00:00:00 2001 From: awais qureshi Date: Wed, 28 Aug 2024 19:10:24 +0500 Subject: [PATCH 1/8] feat: upgrading simple api to drf compatible. --- lms/djangoapps/instructor/tests/test_api.py | 1 + lms/djangoapps/instructor/views/api.py | 55 ++++++++++++------- lms/djangoapps/instructor/views/api_urls.py | 2 +- lms/djangoapps/instructor/views/serializer.py | 20 +++++++ 4 files changed, 58 insertions(+), 20 deletions(-) diff --git a/lms/djangoapps/instructor/tests/test_api.py b/lms/djangoapps/instructor/tests/test_api.py index b0e533ee6f7f..ee136a65800d 100644 --- a/lms/djangoapps/instructor/tests/test_api.py +++ b/lms/djangoapps/instructor/tests/test_api.py @@ -4140,6 +4140,7 @@ def test_change_due_date(self): 'url': str(self.week1.location), 'due_datetime': '12/30/2013 00:00' }) + assert response.status_code == 200, response.content assert get_extended_due(self.course, self.week1, self.user1) == due_date # This operation regenerates the cache, so we can use cached results from edx-when. diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index 1aa40b5e3376..df22e7ff9756 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -106,7 +106,7 @@ from lms.djangoapps.instructor_task.data import InstructorTaskTypes from lms.djangoapps.instructor_task.models import ReportStore from lms.djangoapps.instructor.views.serializer import ( - AccessSerializer, RoleNameSerializer, ShowStudentExtensionSerializer, UserSerializer + AccessSerializer, BlockDueDateSerializer, RoleNameSerializer, ShowStudentExtensionSerializer, UserSerializer ) from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.course_groups.cohorts import add_user_to_cohort, is_course_cohorted @@ -2909,28 +2909,45 @@ def _display_unit(unit): return str(unit.location) -@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', 'due_datetime') -def change_due_date(request, course_id): + + +# @require_post_params('student', 'url', 'due_datetime') + +@method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True), name='dispatch') +class ChangeDueDate(APIView): """ Grants a due date extension to a student for 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')) - due_date = parse_datetime(request.POST.get('due_datetime')) - reason = strip_tags(request.POST.get('reason', '')) + permission_classes = (IsAuthenticated, permissions.InstructorPermission) + permission_name = permissions.GIVE_STUDENT_EXTENSION + serializer_class = BlockDueDateSerializer + + @method_decorator(ensure_csrf_cookie) + def post(self, request, course_id): - set_due_date_extension(course, unit, student, due_date, request.user, reason=reason) + serializer_data = self.serializer_class(data=request.data) + if not serializer_data.is_valid(): + return HttpResponseBadRequest(reason=serializer_data.errors) - return JsonResponse(_( - 'Successfully changed due date for student {0} for {1} ' - 'to {2}').format(student.profile.name, _display_unit(unit), - due_date.strftime('%Y-%m-%d %H:%M'))) + 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) + + course = get_course_by_id(CourseKey.from_string(course_id)) + + unit = find_unit(course, serializer_data.validated_data.get('url')) + due_date = parse_datetime( serializer_data.validated_data.get('due_datetime')) + reason = strip_tags( serializer_data.validated_data.get('reason', '')) + + set_due_date_extension(course, unit, student, due_date, request.user, reason=reason) + + return JsonResponse(_( + 'Successfully changed due date for student {0} for {1} ' + 'to {2}').format(student.profile.name, _display_unit(unit), + due_date.strftime('%Y-%m-%d %H:%M'))) @handle_dashboard_error @@ -2988,8 +3005,8 @@ class ShowStudentExtensions(APIView): particular course. """ permission_classes = (IsAuthenticated, permissions.InstructorPermission) - serializer_class = ShowStudentExtensionSerializer permission_name = permissions.GIVE_STUDENT_EXTENSION + serializer_class = BlockDueDateSerializer @method_decorator(ensure_csrf_cookie) def post(self, request, course_id): diff --git a/lms/djangoapps/instructor/views/api_urls.py b/lms/djangoapps/instructor/views/api_urls.py index 18da0b63b218..892216fb40b7 100644 --- a/lms/djangoapps/instructor/views/api_urls.py +++ b/lms/djangoapps/instructor/views/api_urls.py @@ -50,7 +50,7 @@ path('list_forum_members', api.list_forum_members, name='list_forum_members'), path('update_forum_role_membership', api.update_forum_role_membership, name='update_forum_role_membership'), path('send_email', api.send_email, name='send_email'), - path('change_due_date', api.change_due_date, name='change_due_date'), + path('change_due_date', api.ChangeDueDate.as_view(), name='change_due_date'), path('reset_due_date', api.reset_due_date, 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 0697bed6832d..5d1ec8293847 100644 --- a/lms/djangoapps/instructor/views/serializer.py +++ b/lms/djangoapps/instructor/views/serializer.py @@ -77,3 +77,23 @@ def validate_student(self, value): return None return user + + +class BlockDueDateSerializer(serializers.Serializer): + url = serializers.CharField() + due_datetime = serializers.CharField() + student = serializers.CharField( + max_length=255, + help_text="Email or username of user to change access" + ) + + def validate_student(self, value): + """ + Validate that the student corresponds to an existing user. + """ + try: + user = get_student_from_identifier(value) + except User.DoesNotExist: + return None + + return user From d4d42b1bb3e1629bb067d87dc63700f593f061a3 Mon Sep 17 00:00:00 2001 From: awais qureshi Date: Wed, 28 Aug 2024 19:13:33 +0500 Subject: [PATCH 2/8] feat: upgrading simple api to drf compatible. --- lms/djangoapps/instructor/views/api.py | 15 +++++++++------ lms/djangoapps/instructor/views/serializer.py | 7 +++++++ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index df22e7ff9756..61d6ae620dad 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -2909,10 +2909,6 @@ def _display_unit(unit): return str(unit.location) - - -# @require_post_params('student', 'url', 'due_datetime') - @method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True), name='dispatch') class ChangeDueDate(APIView): """ @@ -2924,7 +2920,14 @@ class ChangeDueDate(APIView): @method_decorator(ensure_csrf_cookie) def post(self, request, course_id): + """ + Grants 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. + due_datetime (str): The new due date and time for the block. + student (str): The email or username of the student whose access is being modified. + """ serializer_data = self.serializer_class(data=request.data) if not serializer_data.is_valid(): return HttpResponseBadRequest(reason=serializer_data.errors) @@ -2939,8 +2942,8 @@ def post(self, request, course_id): course = get_course_by_id(CourseKey.from_string(course_id)) unit = find_unit(course, serializer_data.validated_data.get('url')) - due_date = parse_datetime( serializer_data.validated_data.get('due_datetime')) - reason = strip_tags( serializer_data.validated_data.get('reason', '')) + due_date = parse_datetime(serializer_data.validated_data.get('due_datetime')) + reason = strip_tags(serializer_data.validated_data.get('reason', '')) set_due_date_extension(course, unit, student, due_date, request.user, reason=reason) diff --git a/lms/djangoapps/instructor/views/serializer.py b/lms/djangoapps/instructor/views/serializer.py index 5d1ec8293847..d877bf9c3bd2 100644 --- a/lms/djangoapps/instructor/views/serializer.py +++ b/lms/djangoapps/instructor/views/serializer.py @@ -80,6 +80,13 @@ def validate_student(self, value): class BlockDueDateSerializer(serializers.Serializer): + """ + Serializer for handling block due date updates for a specific student. + Fields: + url (str): The URL related to the block that needs the due date update. + due_datetime (str): The new due date and time for the block. + student (str): The email or username of the student whose access is being modified. + """ url = serializers.CharField() due_datetime = serializers.CharField() student = serializers.CharField( From c2c376e7775ff05040bdd54e796a4d5cfb781b79 Mon Sep 17 00:00:00 2001 From: awais qureshi Date: Wed, 28 Aug 2024 20:30:39 +0500 Subject: [PATCH 3/8] feat: upgrading simple api to drf compatible. --- lms/djangoapps/instructor/tests/test_api.py | 1 - lms/djangoapps/instructor/views/api.py | 7 +++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lms/djangoapps/instructor/tests/test_api.py b/lms/djangoapps/instructor/tests/test_api.py index ee136a65800d..b0e533ee6f7f 100644 --- a/lms/djangoapps/instructor/tests/test_api.py +++ b/lms/djangoapps/instructor/tests/test_api.py @@ -4140,7 +4140,6 @@ def test_change_due_date(self): 'url': str(self.week1.location), 'due_datetime': '12/30/2013 00:00' }) - assert response.status_code == 200, response.content assert get_extended_due(self.course, self.week1, self.user1) == due_date # This operation regenerates the cache, so we can use cached results from edx-when. diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index 61d6ae620dad..a0359e0615c5 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -2944,8 +2944,11 @@ def post(self, request, course_id): unit = find_unit(course, serializer_data.validated_data.get('url')) due_date = parse_datetime(serializer_data.validated_data.get('due_datetime')) reason = strip_tags(serializer_data.validated_data.get('reason', '')) + try: + set_due_date_extension(course, unit, student, due_date, request.user, reason=reason) + except Exception as error: # pylint: disable=broad-except + return JsonResponse({'error': str(error)}, status=400) - set_due_date_extension(course, unit, student, due_date, request.user, reason=reason) return JsonResponse(_( 'Successfully changed due date for student {0} for {1} ' @@ -3008,8 +3011,8 @@ class ShowStudentExtensions(APIView): particular course. """ permission_classes = (IsAuthenticated, permissions.InstructorPermission) + serializer_class = ShowStudentExtensionSerializer permission_name = permissions.GIVE_STUDENT_EXTENSION - serializer_class = BlockDueDateSerializer @method_decorator(ensure_csrf_cookie) def post(self, request, course_id): From 8fa3f044ddbf593876c8bab4514fbc102577b28f Mon Sep 17 00:00:00 2001 From: awais qureshi Date: Wed, 28 Aug 2024 21:47:38 +0500 Subject: [PATCH 4/8] feat: upgrading simple api to drf compatible. --- lms/djangoapps/instructor/views/api.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index a0359e0615c5..b82f22d2c8bd 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -2949,7 +2949,6 @@ def post(self, request, course_id): except Exception as error: # pylint: disable=broad-except return JsonResponse({'error': str(error)}, status=400) - return JsonResponse(_( 'Successfully changed due date for student {0} for {1} ' 'to {2}').format(student.profile.name, _display_unit(unit), From 4ef94b5832b6b7cc658d363b2e50da0215285b32 Mon Sep 17 00:00:00 2001 From: awais qureshi Date: Wed, 28 Aug 2024 22:49:08 +0500 Subject: [PATCH 5/8] feat: upgrading simple api to drf compatible. --- lms/djangoapps/instructor/tests/test_api.py | 15 +++++++++++++++ lms/djangoapps/instructor/views/serializer.py | 2 ++ 2 files changed, 17 insertions(+) diff --git a/lms/djangoapps/instructor/tests/test_api.py b/lms/djangoapps/instructor/tests/test_api.py index b0e533ee6f7f..495c567d0be2 100644 --- a/lms/djangoapps/instructor/tests/test_api.py +++ b/lms/djangoapps/instructor/tests/test_api.py @@ -4145,6 +4145,21 @@ def test_change_due_date(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_change_due_date_with_reason(self): + url = reverse('change_due_date', kwargs={'course_id': str(self.course.id)}) + due_date = datetime.datetime(2013, 12, 30, tzinfo=UTC) + response = self.client.post(url, { + 'student': self.user1.username, + 'url': str(self.week1.location), + 'due_datetime': '12/30/2013 00:00', + 'reason': 'Testing reason.' # this is optional field. + }) + assert response.status_code == 200, response.content + + assert get_extended_due(self.course, self.week1, self.user1) == due_date + # 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_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/serializer.py b/lms/djangoapps/instructor/views/serializer.py index d877bf9c3bd2..466800eb6f46 100644 --- a/lms/djangoapps/instructor/views/serializer.py +++ b/lms/djangoapps/instructor/views/serializer.py @@ -86,6 +86,7 @@ class BlockDueDateSerializer(serializers.Serializer): url (str): The URL related to the block that needs the due date update. due_datetime (str): The new due date and time for the block. student (str): The email or username of the student whose access is being modified. + reason (str): Reason why updating this. """ url = serializers.CharField() due_datetime = serializers.CharField() @@ -93,6 +94,7 @@ class BlockDueDateSerializer(serializers.Serializer): max_length=255, help_text="Email or username of user to change access" ) + reason = serializers.CharField(required=False) def validate_student(self, value): """ From 00e8c08c03b84f3cb9c7f4a9a491b2d25ef02e69 Mon Sep 17 00:00:00 2001 From: Awais Qureshi Date: Tue, 10 Sep 2024 19:03:56 +0500 Subject: [PATCH 6/8] chore: Update api.py --- lms/djangoapps/instructor/views/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index 604dea831160..d42e7173b0bf 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -107,8 +107,8 @@ from lms.djangoapps.instructor_task.data import InstructorTaskTypes from lms.djangoapps.instructor_task.models import ReportStore from lms.djangoapps.instructor.views.serializer import ( - AccessSerializer, BlockDueDateSerializer, RoleNameSerializer, ShowStudentExtensionSerializer, UserSerializer - UserSerializer, SendEmailSerializer, StudentAttemptsSerializer + AccessSerializer, BlockDueDateSerializer, RoleNameSerializer, ShowStudentExtensionSerializer, UserSerializer, + SendEmailSerializer, StudentAttemptsSerializer ) from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.course_groups.cohorts import add_user_to_cohort, is_course_cohorted From 82fc604a6edc093411563b91138ac02e3c762795 Mon Sep 17 00:00:00 2001 From: Awais Qureshi Date: Tue, 10 Sep 2024 19:04:38 +0500 Subject: [PATCH 7/8] chore: Update api_urls.py --- lms/djangoapps/instructor/views/api_urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/djangoapps/instructor/views/api_urls.py b/lms/djangoapps/instructor/views/api_urls.py index f0098ebe6b47..0cb80238f7c2 100644 --- a/lms/djangoapps/instructor/views/api_urls.py +++ b/lms/djangoapps/instructor/views/api_urls.py @@ -48,7 +48,7 @@ path('list_background_email_tasks', api.list_background_email_tasks, name='list_background_email_tasks'), path('list_email_content', api.ListEmailContent.as_view(), name='list_email_content'), path('list_forum_members', api.list_forum_members, name='list_forum_members'), - path('update_forum_role_membership', api.update_forum_role_membership, name='update_forum_role_membership'),, + 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'), From 1da64ae449a73b484fd48294d8bd3ecf86700e84 Mon Sep 17 00:00:00 2001 From: Awais Qureshi Date: Tue, 10 Sep 2024 23:58:28 +0500 Subject: [PATCH 8/8] chore: Update serializer.py --- lms/djangoapps/instructor/views/serializer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/djangoapps/instructor/views/serializer.py b/lms/djangoapps/instructor/views/serializer.py index 7df88c70eb3c..793acc9c6137 100644 --- a/lms/djangoapps/instructor/views/serializer.py +++ b/lms/djangoapps/instructor/views/serializer.py @@ -149,7 +149,7 @@ class SendEmailSerializer(serializers.Serializer): subject = serializers.CharField(max_length=128, write_only=True, required=True) message = serializers.CharField(required=True) schedule = serializers.CharField(required=False) - + class BlockDueDateSerializer(serializers.Serializer): """