From c18c9de3cd60d03256cec3fccb6ba1aa6fe3a680 Mon Sep 17 00:00:00 2001 From: hajorg Date: Wed, 28 Feb 2024 15:23:28 +0100 Subject: [PATCH 1/2] feat: add post endpoint for course reset --- lms/djangoapps/support/tests/test_views.py | 97 +++++++++++++++++++- lms/djangoapps/support/views/course_reset.py | 66 ++++++++++++- 2 files changed, 161 insertions(+), 2 deletions(-) diff --git a/lms/djangoapps/support/tests/test_views.py b/lms/djangoapps/support/tests/test_views.py index 6338935470f4..758e51fdf5f9 100644 --- a/lms/djangoapps/support/tests/test_views.py +++ b/lms/djangoapps/support/tests/test_views.py @@ -54,7 +54,7 @@ from common.djangoapps.third_party_auth.tests.factories import SAMLProviderConfigFactory from common.test.utils import disable_signal from lms.djangoapps.program_enrollments.tests.factories import ProgramCourseEnrollmentFactory, ProgramEnrollmentFactory -from lms.djangoapps.support.models import CourseResetAudit +from lms.djangoapps.support.models import CourseResetAudit, CourseResetCourseOptIn from lms.djangoapps.support.serializers import ProgramEnrollmentSerializer from lms.djangoapps.support.tests.factories import CourseResetCourseOptInFactory, CourseResetAuditFactory from lms.djangoapps.verify_student.models import VerificationDeadline @@ -2331,3 +2331,98 @@ def test_multiple_failed_audits(self): 'can_reset': True, 'status': most_recent_audit.status_message() }]) + + +class TestResetCourseViewPost(SupportViewTestCase): + """ + Tests for creating course request + """ + + def setUp(self): + super().setUp() + SupportStaffRole().add_users(self.user) + + self.course_id = 'course-v1:a+b+c' + + self.other_user = User.objects.create(username='otheruser', password='test') + + self.course = CourseFactory.create( + org='a', + course='b', + run='c', + enable_proctored_exams=True, + proctoring_provider=settings.PROCTORING_BACKENDS['DEFAULT'], + ) + self.enrollment = CourseEnrollmentFactory( + is_active=True, + mode='verified', + course_id=self.course.id, + user=self.user + ) + CourseResetCourseOptIn.objects.create( + course_id=self.course_id, + active=True + ) + + self.other_course = CourseFactory.create( + org='x', + course='y', + run='z', + ) + + def _url(self, username): + return reverse("support:course_reset", kwargs={'username_or_email': username}) + + def test_wrong_username(self): + """ + Test that a request with a username which does not exits returns 404 + """ + response = self.client.post(self._url(username='does_not_exist'), data={'course_id': 'course-v1:aa+bb+c'}) + self.assertEqual(response.status_code, 404) + + def test_learner_course_reset(self): + response = self.client.post(self._url(username=self.user.username), data={'course_id': self.course_id}) + self.assertEqual(response.status_code, 201) + self.assertEqual(response.data, { + 'course_id': self.course_id, + 'status': response.data['status'], + 'can_reset': False, + 'display_name': self.course.display_name + }) + + def test_course_not_opt_in(self): + response = self.client.post(self._url(username=self.user.username), data={'course_id': 'course-v1:aa+bb+c'}) + self.assertEqual(response.status_code, 404) + + def test_course_reset_failed(self): + course = CourseFactory.create( + org='xx', + course='yy', + run='zz', + ) + enrollment = CourseEnrollmentFactory( + is_active=True, + mode='verified', + course_id=course.id, + user=self.user + ) + + opt_in_course = CourseResetCourseOptIn.objects.create( + course_id=course.id, + active=True + ) + + CourseResetAudit.objects.create( + course=opt_in_course, + course_enrollment=enrollment, + reset_by=self.other_user, + status=CourseResetAudit.CourseResetStatus.FAILED + ) + response = self.client.post(self._url(username=self.user.username), data={'course_id': course.id}) + self.assertEqual(response.status_code, 200) + + def test_course_reset_dupe(self): + response = self.client.post(self._url(username=self.user.username), data={'course_id': self.course_id}) + self.assertEqual(response.status_code, 201) + resp = self.client.post(self._url(username=self.user.username), data={'course_id': self.course_id}) + self.assertEqual(resp.status_code, 204) diff --git a/lms/djangoapps/support/views/course_reset.py b/lms/djangoapps/support/views/course_reset.py index b274d4a97fa1..6ed705548ffe 100644 --- a/lms/djangoapps/support/views/course_reset.py +++ b/lms/djangoapps/support/views/course_reset.py @@ -95,6 +95,70 @@ def get(self, request, username_or_email): }) return Response(result) + @method_decorator(require_support_permission) def post(self, request, username_or_email): - """ Other Ticket """ + """ + Resets a course for the given learner + + returns a dicts with the format { + 'course_id': + 'display_name': + 'status': + 'can_reset': (boolean) + } + """ + course_id = request.data['course_id'] + try: + user = get_user_by_username_or_email(username_or_email) + except User.DoesNotExist: + return Response({'error': 'User does not exist'}, status=404) + try: + opt_in_course = CourseResetCourseOptIn.objects.get(course_id=course_id) + except CourseResetCourseOptIn.DoesNotExist: + return Response({'error': 'Course is not eligible'}, status=404) + enrollment = CourseEnrollment.objects.get( + course=course_id, + user=user, + is_active=True + ) + user_passed = user_has_passing_grade_in_course(enrollment=enrollment) + course_overview = enrollment.course_overview + course_reset_audit = CourseResetAudit.objects.filter(course_enrollment=enrollment).first() + + if course_reset_audit and course_reset_audit.status == CourseResetAudit.CourseResetStatus.FAILED and not user_passed: + course_reset_audit.status = CourseResetAudit.CourseResetStatus.ENQUEUED + course_reset_audit.save() + status = f"In progress - Started on {course_reset_audit.modified} by {course_reset_audit.reset_by.username}" + # Call celery task + resp = { + 'course_id': course_id, + 'status': status, + 'can_reset': False, + 'display_name': course_overview.display_name + } + return Response(resp, status=200) + + elif course_reset_audit and \ + (course_reset_audit.status == CourseResetAudit.CourseResetStatus.IN_PROGRESS or + course_reset_audit.status == CourseResetAudit.CourseResetStatus.ENQUEUED): + return Response(None, status=204) + + if enrollment and opt_in_course and not user_passed: + course_reset_audit = CourseResetAudit.objects.create( + course=opt_in_course, + course_enrollment=enrollment, + reset_by=request.user, + ) + status = f"In progress - Started on {course_reset_audit.modified} by {course_reset_audit.reset_by.username}" + resp = { + 'course_id': course_id, + 'status': status, + 'can_reset': False, + 'display_name': course_overview.display_name + } + # Call celery task + + return Response(resp, status=201) + else: + return Response(None, status=400) From 45648c911fb8e977356e32b9de562767ebd2ad3b Mon Sep 17 00:00:00 2001 From: hajorg Date: Wed, 28 Feb 2024 16:58:52 +0100 Subject: [PATCH 2/2] fix: resolve lint issues --- lms/djangoapps/support/tests/test_views.py | 15 +++++++------- lms/djangoapps/support/views/course_reset.py | 21 ++++++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/lms/djangoapps/support/tests/test_views.py b/lms/djangoapps/support/tests/test_views.py index 758e51fdf5f9..4bf71a16d8c6 100644 --- a/lms/djangoapps/support/tests/test_views.py +++ b/lms/djangoapps/support/tests/test_views.py @@ -2359,10 +2359,7 @@ def setUp(self): course_id=self.course.id, user=self.user ) - CourseResetCourseOptIn.objects.create( - course_id=self.course_id, - active=True - ) + self.opt_in = CourseResetCourseOptInFactory.create(course_id=self.course.id) self.other_course = CourseFactory.create( org='x', @@ -2422,7 +2419,9 @@ def test_course_reset_failed(self): self.assertEqual(response.status_code, 200) def test_course_reset_dupe(self): - response = self.client.post(self._url(username=self.user.username), data={'course_id': self.course_id}) - self.assertEqual(response.status_code, 201) - resp = self.client.post(self._url(username=self.user.username), data={'course_id': self.course_id}) - self.assertEqual(resp.status_code, 204) + CourseResetAuditFactory.create( + course=self.opt_in, + course_enrollment=self.enrollment, + ) + response2 = self.client.post(self._url(username=self.user.username), data={'course_id': self.course_id}) + self.assertEqual(response2.status_code, 204) diff --git a/lms/djangoapps/support/views/course_reset.py b/lms/djangoapps/support/views/course_reset.py index 6ed705548ffe..c8d4abb2cec1 100644 --- a/lms/djangoapps/support/views/course_reset.py +++ b/lms/djangoapps/support/views/course_reset.py @@ -95,7 +95,6 @@ def get(self, request, username_or_email): }) return Response(result) - @method_decorator(require_support_permission) def post(self, request, username_or_email): """ @@ -126,22 +125,25 @@ def post(self, request, username_or_email): course_overview = enrollment.course_overview course_reset_audit = CourseResetAudit.objects.filter(course_enrollment=enrollment).first() - if course_reset_audit and course_reset_audit.status == CourseResetAudit.CourseResetStatus.FAILED and not user_passed: + if course_reset_audit and ( + course_reset_audit.status == CourseResetAudit.CourseResetStatus.FAILED + and not user_passed + ): course_reset_audit.status = CourseResetAudit.CourseResetStatus.ENQUEUED course_reset_audit.save() - status = f"In progress - Started on {course_reset_audit.modified} by {course_reset_audit.reset_by.username}" # Call celery task resp = { 'course_id': course_id, - 'status': status, + 'status': course_reset_audit.status_message(), 'can_reset': False, 'display_name': course_overview.display_name } return Response(resp, status=200) - elif course_reset_audit and \ - (course_reset_audit.status == CourseResetAudit.CourseResetStatus.IN_PROGRESS or - course_reset_audit.status == CourseResetAudit.CourseResetStatus.ENQUEUED): + elif course_reset_audit and course_reset_audit.status in ( + CourseResetAudit.CourseResetStatus.IN_PROGRESS, + CourseResetAudit.CourseResetStatus.ENQUEUED + ): return Response(None, status=204) if enrollment and opt_in_course and not user_passed: @@ -150,15 +152,14 @@ def post(self, request, username_or_email): course_enrollment=enrollment, reset_by=request.user, ) - status = f"In progress - Started on {course_reset_audit.modified} by {course_reset_audit.reset_by.username}" resp = { 'course_id': course_id, - 'status': status, + 'status': course_reset_audit.status_message(), 'can_reset': False, 'display_name': course_overview.display_name } - # Call celery task + # Call celery task return Response(resp, status=201) else: return Response(None, status=400)