diff --git a/openedx/core/djangoapps/user_api/accounts/tests/test_retirement_views.py b/openedx/core/djangoapps/user_api/accounts/tests/test_retirement_views.py index e07740ad92..a1db84f2d1 100644 --- a/openedx/core/djangoapps/user_api/accounts/tests/test_retirement_views.py +++ b/openedx/core/djangoapps/user_api/accounts/tests/test_retirement_views.py @@ -1042,6 +1042,82 @@ class TestAccountRetirementRetrieve(RetirementTestCase): self.assert_status_and_user_data(values, username_to_find=original_username) +@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Account APIs are only supported in LMS') +class TestAccountRetirementCleanup(RetirementTestCase): + """ + Tests the account retirement cleanup endpoint. + """ + def setUp(self): + super(TestAccountRetirementCleanup, self).setUp() + self.pending_state = RetirementState.objects.get(state_name='PENDING') + self.complete_state = RetirementState.objects.get(state_name='COMPLETE') + self.retirements = [] + self.usernames = [] + + for _ in range(1, 10): + user = UserFactory() + self.retirements.append(create_retirement_status(user, state=self.complete_state)) + self.usernames.append(user.username) + + self.test_superuser = SuperuserFactory() + self.headers = build_jwt_headers(self.test_superuser) + self.headers['content_type'] = "application/json" + self.url = reverse('accounts_retirement_cleanup') + + def cleanup_and_assert_status(self, data=None, expected_status=status.HTTP_204_NO_CONTENT): + """ + Helper function for making a request to the retirement cleanup endpoint, and asserting the status. + """ + if data is None: + data = {'usernames': self.usernames} + + response = self.client.delete(self.url, json.dumps(data), **self.headers) + print(response) + self.assertEqual(response.status_code, expected_status) + return response + + def test_simple_success(self): + self.cleanup_and_assert_status() + self.assertFalse(UserRetirementStatus.objects.all()) + + def test_leaves_other_users(self): + remaining_usernames = [] + + # Create a bunch of local users in different states + for state in (self.pending_state, self.complete_state): + for _ in range(1, 3): + user = UserFactory() + remaining_usernames.append(create_retirement_status(user, state=state).user.username) + + # Call should succeed and leave behind the local users in both states + self.cleanup_and_assert_status() + self.assertEqual( + UserRetirementStatus.objects.filter(user__username__in=remaining_usernames).count(), + len(remaining_usernames) + ) + + def test_no_usernames(self): + self.cleanup_and_assert_status(data={'usernames': []}) + + def test_bad_usernames(self): + self.cleanup_and_assert_status(data={'usernames': 'foo'}, expected_status=status.HTTP_400_BAD_REQUEST) + + def test_nonexistent_username(self): + self.cleanup_and_assert_status( + data={'usernames': self.usernames + ['does not exist']}, + expected_status=status.HTTP_400_BAD_REQUEST + ) + + def test_username_bad_state(self): + # Set one of the users we're looking up to a non-COMPLETE state to + # force the error + retirement = UserRetirementStatus.objects.get(user__username=self.usernames[0]) + retirement.current_state = self.pending_state + retirement.save() + + self.cleanup_and_assert_status(expected_status=status.HTTP_400_BAD_REQUEST) + + @ddt.ddt @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Account APIs are only supported in LMS') class TestAccountRetirementUpdate(RetirementTestCase): diff --git a/openedx/core/djangoapps/user_api/accounts/views.py b/openedx/core/djangoapps/user_api/accounts/views.py index 89916e72cf..55645a22da 100644 --- a/openedx/core/djangoapps/user_api/accounts/views.py +++ b/openedx/core/djangoapps/user_api/accounts/views.py @@ -804,6 +804,39 @@ class AccountRetirementStatusView(ViewSet): except Exception as exc: # pylint: disable=broad-except return Response(text_type(exc), status=status.HTTP_500_INTERNAL_SERVER_ERROR) + def cleanup(self, request): + """ + DELETE /api/user/v1/accounts/update_retirement_status/ + + { + 'usernames': ['user1', 'user2', ...] + } + + Deletes a batch of retirement requests by username. + """ + try: + usernames = request.data['usernames'] + + if not isinstance(usernames, list): + raise TypeError('Usernames should be an array.') + + complete_state = RetirementState.objects.get(state_name='COMPLETE') + retirements = UserRetirementStatus.objects.filter( + original_username__in=usernames, + current_state=complete_state + ) + + # Sanity check that they're all valid usernames in the right state + if len(usernames) != len(retirements): + raise UserRetirementStatus.DoesNotExist('Not all usernames exist in the COMPLETE state.') + + retirements.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + except (RetirementStateError, UserRetirementStatus.DoesNotExist, TypeError) as exc: + return Response(text_type(exc), status=status.HTTP_400_BAD_REQUEST) + except Exception as exc: # pylint: disable=broad-except + return Response(text_type(exc), status=status.HTTP_500_INTERNAL_SERVER_ERROR) + class LMSAccountRetirementView(ViewSet): """ diff --git a/openedx/core/djangoapps/user_api/urls.py b/openedx/core/djangoapps/user_api/urls.py index 77b2f3a861..3c1658bbcf 100644 --- a/openedx/core/djangoapps/user_api/urls.py +++ b/openedx/core/djangoapps/user_api/urls.py @@ -58,6 +58,10 @@ RETIREMENT_UPDATE = AccountRetirementStatusView.as_view({ 'patch': 'partial_update', }) +RETIREMENT_CLEANUP = AccountRetirementStatusView.as_view({ + 'delete': 'cleanup', +}) + RETIREMENT_POST = AccountRetirementView.as_view({ 'post': 'post', }) @@ -127,6 +131,11 @@ urlpatterns = [ RETIREMENT_QUEUE, name='accounts_retirement_queue' ), + url( + r'^v1/accounts/retirement_cleanup/$', + RETIREMENT_CLEANUP, + name='accounts_retirement_cleanup' + ), url( r'^v1/accounts/retirements_by_status_and_date/$', RETIREMENT_LIST_BY_STATUS_AND_DATE,