diff --git a/lms/djangoapps/discussion_api/tests/test_views.py b/lms/djangoapps/discussion_api/tests/test_views.py index 4a7f56ebef9fd23079a10179f6153dbbc3af8d02..5695b6b38998c0b1490505e1777b3efc8501f5d0 100644 --- a/lms/djangoapps/discussion_api/tests/test_views.py +++ b/lms/djangoapps/discussion_api/tests/test_views.py @@ -15,6 +15,7 @@ from nose.plugins.attrib import attr from pytz import UTC from rest_framework.parsers import JSONParser from rest_framework.test import APIClient +from six import text_type from common.test.utils import disable_signal from discussion_api import api @@ -27,7 +28,9 @@ from discussion_api.tests.utils import ( ) from django_comment_client.tests.utils import ForumsEnableMixin from openedx.core.djangoapps.user_api.accounts.image_helpers import get_profile_image_storage -from student.tests.factories import CourseEnrollmentFactory, UserFactory +from openedx.core.lib.token_utils import JwtBuilder +from student.models import get_retired_username_by_username +from student.tests.factories import CourseEnrollmentFactory, UserFactory, SuperuserFactory from util.testing import PatchMediaTypeMixin, UrlResetMixin from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.django import modulestore @@ -77,7 +80,7 @@ class DiscussionAPIViewTestMixin(ForumsEnableMixin, CommentsServiceMockMixin, Ur """ cs_thread = make_minimal_cs_thread({ "id": "test_thread", - "course_id": unicode(self.course.id), + "course_id": text_type(self.course.id), "commentable_id": "test_topic", "username": self.user.username, "user_id": str(self.user.id), @@ -95,7 +98,7 @@ class DiscussionAPIViewTestMixin(ForumsEnableMixin, CommentsServiceMockMixin, Ur """ cs_comment = make_minimal_cs_comment({ "id": "test_comment", - "course_id": unicode(self.course.id), + "course_id": text_type(self.course.id), "thread_id": "test_thread", "username": self.user.username, "user_id": str(self.user.id), @@ -125,7 +128,7 @@ class CourseViewTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): """Tests for CourseView""" def setUp(self): super(CourseViewTest, self).setUp() - self.url = reverse("discussion_course", kwargs={"course_id": unicode(self.course.id)}) + self.url = reverse("discussion_course", kwargs={"course_id": text_type(self.course.id)}) def test_404(self): response = self.client.get( @@ -143,7 +146,7 @@ class CourseViewTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): response, 200, { - "id": unicode(self.course.id), + "id": text_type(self.course.id), "blackouts": [], "thread_list_url": "http://testserver/api/discussion/v1/threads/?course_id=x%2Fy%2Fz", "following_thread_list_url": ( @@ -154,6 +157,84 @@ class CourseViewTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): ) +@httpretty.activate +@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) +class RetireViewTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): + """Tests for CourseView""" + def setUp(self): + super(RetireViewTest, self).setUp() + self.superuser = SuperuserFactory() + self.retired_username = get_retired_username_by_username(self.user.username) + self.url = reverse( + "retire_discussion_user", + kwargs={"username": text_type(self.user.username)} + ) + + def assert_response_correct(self, response, expected_status, expected_content): + """ + Assert that the response has the given status code and content + """ + self.assertEqual(response.status_code, expected_status) + + if expected_content: + self.assertEqual(text_type(response.content), expected_content) + + def build_jwt_headers(self, user): + """ + Helper function for creating headers for the JWT authentication. + """ + token = JwtBuilder(user).build_token([]) + headers = {'HTTP_AUTHORIZATION': 'JWT ' + token} + return headers + + def test_basic(self): + """ + Check successful retirement case + """ + self.register_get_user_retire_response(self.user) + headers = self.build_jwt_headers(self.superuser) + response = self.client.post(self.url, {'retired_username': self.retired_username}, **headers) + self.assert_response_correct(response, 204, "") + + def test_bad_hash(self): + """ + Check that we fail on a hash mismatch with an appropriate error + """ + headers = self.build_jwt_headers(self.superuser) + response = self.client.post(self.url, {'retired_username': "this will never match"}, **headers) + self.assert_response_correct(response, 500, '"Mismatched hashed_username, bad salt?"') + + def test_downstream_forums_error(self): + """ + Check that we bubble up errors from the comments service + """ + self.register_get_user_retire_response(self.user, status=500, body="Server error") + headers = self.build_jwt_headers(self.superuser) + response = self.client.post(self.url, {'retired_username': self.retired_username}, **headers) + self.assert_response_correct(response, 500, '"Server error"') + + def test_nonexistent_user(self): + """ + Check that we handle unknown users appropriately + """ + nonexistent_username = "nonexistent user" + self.url = reverse( + "retire_discussion_user", + kwargs={"username": nonexistent_username} + ) + self.retired_username = get_retired_username_by_username(nonexistent_username) + + headers = self.build_jwt_headers(self.superuser) + response = self.client.post(self.url, {'retired_username': self.retired_username}, **headers) + self.assert_response_correct(response, 404, None) + + def test_not_authenticated(self): + """ + Override the parent implementation of this, we auth differently + """ + pass + + @ddt.ddt @mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) class CourseTopicsViewTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): @@ -162,7 +243,7 @@ class CourseTopicsViewTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): """ def setUp(self): super(CourseTopicsViewTest, self).setUp() - self.url = reverse("course_topics", kwargs={"course_id": unicode(self.course.id)}) + self.url = reverse("course_topics", kwargs={"course_id": text_type(self.course.id)}) def create_course(self, modules_count, module_store, topics): """ @@ -177,7 +258,7 @@ class CourseTopicsViewTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): discussion_topics=topics ) CourseEnrollmentFactory.create(user=self.user, course_id=course.id) - course_url = reverse("course_topics", kwargs={"course_id": unicode(course.id)}) + course_url = reverse("course_topics", kwargs={"course_id": text_type(course.id)}) # add some discussion xblocks for i in range(modules_count): ItemFactory.create( @@ -325,7 +406,7 @@ class ThreadViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase, Pro """ thread = make_minimal_cs_thread({ "id": "test_thread", - "course_id": unicode(self.course.id), + "course_id": text_type(self.course.id), "commentable_id": "test_topic", "user_id": str(self.user.id), "username": self.user.username, @@ -350,7 +431,7 @@ class ThreadViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase, Pro ) def test_404(self): - response = self.client.get(self.url, {"course_id": unicode("non/existent/course")}) + response = self.client.get(self.url, {"course_id": text_type("non/existent/course")}) self.assert_response_correct( response, 404, @@ -373,7 +454,7 @@ class ThreadViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase, Pro "editable_fields": ["abuse_flagged", "following", "read", "voted"], })] self.register_get_threads_response(source_threads, page=1, num_pages=2) - response = self.client.get(self.url, {"course_id": unicode(self.course.id), "following": ""}) + response = self.client.get(self.url, {"course_id": text_type(self.course.id), "following": ""}) expected_response = make_paginated_api_response( results=expected_threads, count=1, @@ -388,8 +469,8 @@ class ThreadViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase, Pro expected_response ) self.assert_last_query_params({ - "user_id": [unicode(self.user.id)], - "course_id": [unicode(self.course.id)], + "user_id": [text_type(self.user.id)], + "course_id": [text_type(self.course.id)], "sort_key": ["activity"], "page": ["1"], "per_page": ["10"], @@ -403,13 +484,13 @@ class ThreadViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase, Pro self.client.get( self.url, { - "course_id": unicode(self.course.id), + "course_id": text_type(self.course.id), "view": query, } ) self.assert_last_query_params({ - "user_id": [unicode(self.user.id)], - "course_id": [unicode(self.course.id)], + "user_id": [text_type(self.user.id)], + "course_id": [text_type(self.course.id)], "sort_key": ["activity"], "page": ["1"], "per_page": ["10"], @@ -421,7 +502,7 @@ class ThreadViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase, Pro self.register_get_threads_response([], page=1, num_pages=1) response = self.client.get( self.url, - {"course_id": unicode(self.course.id), "page": "18", "page_size": "4"} + {"course_id": text_type(self.course.id), "page": "18", "page_size": "4"} ) self.assert_response_correct( response, @@ -429,8 +510,8 @@ class ThreadViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase, Pro {"developer_message": "Page not found (No results on this page)."} ) self.assert_last_query_params({ - "user_id": [unicode(self.user.id)], - "course_id": [unicode(self.course.id)], + "user_id": [text_type(self.user.id)], + "course_id": [text_type(self.course.id)], "sort_key": ["activity"], "page": ["18"], "per_page": ["4"], @@ -441,7 +522,7 @@ class ThreadViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase, Pro self.register_get_threads_search_response([], None, num_pages=0) response = self.client.get( self.url, - {"course_id": unicode(self.course.id), "text_search": "test search string"} + {"course_id": text_type(self.course.id), "text_search": "test search string"} ) expected_response = make_paginated_api_response( @@ -454,8 +535,8 @@ class ThreadViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase, Pro expected_response ) self.assert_last_query_params({ - "user_id": [unicode(self.user.id)], - "course_id": [unicode(self.course.id)], + "user_id": [text_type(self.user.id)], + "course_id": [text_type(self.course.id)], "sort_key": ["activity"], "page": ["1"], "per_page": ["10"], @@ -469,7 +550,7 @@ class ThreadViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase, Pro response = self.client.get( self.url, { - "course_id": unicode(self.course.id), + "course_id": text_type(self.course.id), "following": following, } ) @@ -493,7 +574,7 @@ class ThreadViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase, Pro response = self.client.get( self.url, { - "course_id": unicode(self.course.id), + "course_id": text_type(self.course.id), "following": following, } ) @@ -509,7 +590,7 @@ class ThreadViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase, Pro response = self.client.get( self.url, { - "course_id": unicode(self.course.id), + "course_id": text_type(self.course.id), "following": "invalid-boolean", } ) @@ -541,13 +622,13 @@ class ThreadViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase, Pro self.client.get( self.url, { - "course_id": unicode(self.course.id), + "course_id": text_type(self.course.id), "order_by": http_query, } ) self.assert_last_query_params({ - "user_id": [unicode(self.user.id)], - "course_id": [unicode(self.course.id)], + "user_id": [text_type(self.user.id)], + "course_id": [text_type(self.course.id)], "page": ["1"], "per_page": ["10"], "sort_key": [cc_query], @@ -564,13 +645,13 @@ class ThreadViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase, Pro self.client.get( self.url, { - "course_id": unicode(self.course.id), + "course_id": text_type(self.course.id), "order_direction": "desc", } ) self.assert_last_query_params({ - "user_id": [unicode(self.user.id)], - "course_id": [unicode(self.course.id)], + "user_id": [text_type(self.user.id)], + "course_id": [text_type(self.course.id)], "sort_key": ["activity"], "page": ["1"], "per_page": ["10"], @@ -583,7 +664,7 @@ class ThreadViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase, Pro self.register_get_user_response(self.user) self.register_get_threads_search_response([], None, num_pages=0) response = self.client.get(self.url, { - "course_id": unicode(self.course.id), + "course_id": text_type(self.course.id), "text_search": "test search string", "topic_id": "topic1, topic2", }) @@ -616,7 +697,7 @@ class ThreadViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase, Pro response = self.client.get( self.url, - {"course_id": unicode(self.course.id), "requested_fields": "profile_image"}, + {"course_id": text_type(self.course.id), "requested_fields": "profile_image"}, ) self.assertEqual(response.status_code, 200) response_threads = json.loads(response.content)['results'] @@ -641,7 +722,7 @@ class ThreadViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase, Pro response = self.client.get( self.url, - {"course_id": unicode(self.course.id), "requested_fields": "profile_image"}, + {"course_id": text_type(self.course.id), "requested_fields": "profile_image"}, ) self.assertEqual(response.status_code, 200) response_thread = json.loads(response.content)['results'][0] @@ -667,7 +748,7 @@ class ThreadViewSetCreateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): }) self.register_post_thread_response(cs_thread) request_data = { - "course_id": unicode(self.course.id), + "course_id": text_type(self.course.id), "topic_id": "test_topic", "type": "discussion", "title": "Test Title", @@ -684,7 +765,7 @@ class ThreadViewSetCreateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): self.assertEqual( httpretty.last_request().parsed_body, { - "course_id": [unicode(self.course.id)], + "course_id": [text_type(self.course.id)], "commentable_id": ["test_topic"], "thread_type": ["discussion"], "title": ["Test Title"], @@ -755,7 +836,7 @@ class ThreadViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTest self.assertEqual( httpretty.last_request().parsed_body, { - "course_id": [unicode(self.course.id)], + "course_id": [text_type(self.course.id)], "commentable_id": ["test_topic"], "thread_type": ["discussion"], "title": ["Test Title"], @@ -885,7 +966,7 @@ class ThreadViewSetDeleteTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): self.register_get_user_response(self.user) cs_thread = make_minimal_cs_thread({ "id": self.thread_id, - "course_id": unicode(self.course.id), + "course_id": text_type(self.course.id), "username": self.user.username, "user_id": str(self.user.id), }) @@ -943,7 +1024,7 @@ class CommentViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase, Pr already in overrides. """ overrides = overrides.copy() if overrides else {} - overrides.setdefault("course_id", unicode(self.course.id)) + overrides.setdefault("course_id", text_type(self.course.id)) return make_minimal_cs_thread(overrides) def expected_response_comment(self, overrides=None): @@ -1006,7 +1087,7 @@ class CommentViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase, Pr })] self.register_get_thread_response({ "id": self.thread_id, - "course_id": unicode(self.course.id), + "course_id": text_type(self.course.id), "thread_type": "discussion", "children": source_comments, "resp_total": 100, @@ -1043,7 +1124,7 @@ class CommentViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase, Pr self.register_get_user_response(self.user) self.register_get_thread_response(make_minimal_cs_thread({ "id": self.thread_id, - "course_id": unicode(self.course.id), + "course_id": text_type(self.course.id), "thread_type": "discussion", "resp_total": 10, })) @@ -1152,7 +1233,7 @@ class CommentViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase, Pr }) thread = self.make_minimal_cs_thread({ "id": self.thread_id, - "course_id": unicode(self.course.id), + "course_id": text_type(self.course.id), "thread_type": "discussion", "children": [response_1, response_2], "resp_total": 2, @@ -1187,7 +1268,7 @@ class CommentViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase, Pr source_comments = [self.create_source_comment()] self.register_get_thread_response({ "id": self.thread_id, - "course_id": unicode(self.course.id), + "course_id": text_type(self.course.id), "thread_type": "discussion", "children": source_comments, "resp_total": 100, @@ -1296,7 +1377,7 @@ class CommentViewSetDeleteTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): self.register_get_user_response(self.user) cs_thread = make_minimal_cs_thread({ "id": "test_thread", - "course_id": unicode(self.course.id), + "course_id": text_type(self.course.id), }) self.register_get_thread_response(cs_thread) cs_comment = make_minimal_cs_comment({ @@ -1376,7 +1457,7 @@ class CommentViewSetCreateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): self.assertEqual( httpretty.last_request().parsed_body, { - "course_id": [unicode(self.course.id)], + "course_id": [text_type(self.course.id)], "body": ["Test body"], "user_id": [str(self.user.id)], } @@ -1475,7 +1556,7 @@ class CommentViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTes httpretty.last_request().parsed_body, { "body": ["Edited body"], - "course_id": [unicode(self.course.id)], + "course_id": [text_type(self.course.id)], "user_id": [str(self.user.id)], "anonymous": ["False"], "anonymous_to_peers": ["False"], @@ -1543,7 +1624,7 @@ class ThreadViewSetRetrieveTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase, self.register_get_user_response(self.user) cs_thread = make_minimal_cs_thread({ "id": self.thread_id, - "course_id": unicode(self.course.id), + "course_id": text_type(self.course.id), "commentable_id": "test_topic", "username": self.user.username, "user_id": str(self.user.id), @@ -1568,7 +1649,7 @@ class ThreadViewSetRetrieveTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase, self.register_get_user_response(self.user) cs_thread = make_minimal_cs_thread({ "id": self.thread_id, - "course_id": unicode(self.course.id), + "course_id": text_type(self.course.id), "username": self.user.username, "user_id": str(self.user.id), }) @@ -1598,7 +1679,7 @@ class CommentViewSetRetrieveTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase return make_minimal_cs_comment({ "id": comment_id, "parent_id": parent_id, - "course_id": unicode(self.course.id), + "course_id": text_type(self.course.id), "thread_id": self.thread_id, "thread_type": "discussion", "username": self.user.username, @@ -1615,7 +1696,7 @@ class CommentViewSetRetrieveTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase cs_comment = self.make_comment_data(self.comment_id, None, [cs_comment_child]) cs_thread = make_minimal_cs_thread({ "id": self.thread_id, - "course_id": unicode(self.course.id), + "course_id": text_type(self.course.id), "children": [cs_comment], }) self.register_get_thread_response(cs_thread) @@ -1663,7 +1744,7 @@ class CommentViewSetRetrieveTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase cs_comment = self.make_comment_data(self.comment_id, None, [cs_comment_child]) cs_thread = make_minimal_cs_thread({ "id": self.thread_id, - "course_id": unicode(self.course.id), + "course_id": text_type(self.course.id), "children": [cs_comment], }) self.register_get_thread_response(cs_thread) @@ -1687,7 +1768,7 @@ class CommentViewSetRetrieveTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase cs_comment = self.make_comment_data(self.comment_id, None, [cs_comment_child]) cs_thread = make_minimal_cs_thread({ 'id': self.thread_id, - 'course_id': unicode(self.course.id), + 'course_id': text_type(self.course.id), 'children': [cs_comment], }) self.register_get_thread_response(cs_thread) diff --git a/lms/djangoapps/discussion_api/tests/utils.py b/lms/djangoapps/discussion_api/tests/utils.py index 2112295911bb48bdc18b1a5a19ca170b1c1f6f17..6a0b43764f0ef7e87c91cbf1ef1cc26e0b69d14b 100644 --- a/lms/djangoapps/discussion_api/tests/utils.py +++ b/lms/djangoapps/discussion_api/tests/utils.py @@ -216,6 +216,16 @@ class CommentsServiceMockMixin(object): status=200 ) + def register_get_user_retire_response(self, user, status=200, body=""): + """Register a mock response for GET on the CS user retirement endpoint""" + assert httpretty.is_enabled(), 'httpretty must be enabled to mock calls.' + httpretty.register_uri( + httpretty.POST, + "http://localhost:4567/api/v1/users/{id}/retire".format(id=user.id), + body=body, + status=status + ) + def register_subscribed_threads_response(self, user, threads, page, num_pages): """Register a mock response for GET on the CS user instance endpoint""" assert httpretty.is_enabled(), 'httpretty must be enabled to mock calls.' diff --git a/lms/djangoapps/discussion_api/urls.py b/lms/djangoapps/discussion_api/urls.py index 4d884582ba8d4ad93816d63ebdf2fad349ca0590..898dfdc921a3cd38a48bba1c28c8cbc8d6949746 100644 --- a/lms/djangoapps/discussion_api/urls.py +++ b/lms/djangoapps/discussion_api/urls.py @@ -5,7 +5,7 @@ from django.conf import settings from django.conf.urls import include, url from rest_framework.routers import SimpleRouter -from discussion_api.views import CommentViewSet, CourseTopicsView, CourseView, ThreadViewSet +from discussion_api.views import CommentViewSet, CourseTopicsView, CourseView, ThreadViewSet, RetireUserView ROUTER = SimpleRouter() ROUTER.register("threads", ThreadViewSet, base_name="thread") @@ -17,6 +17,7 @@ urlpatterns = [ CourseView.as_view(), name="discussion_course" ), + url(r"^v1/users/{}".format(settings.USERNAME_PATTERN), RetireUserView.as_view(), name="retire_discussion_user"), url( r"^v1/course_topics/{}".format(settings.COURSE_ID_PATTERN), CourseTopicsView.as_view(), diff --git a/lms/djangoapps/discussion_api/views.py b/lms/djangoapps/discussion_api/views.py index fcf89087d720cb08e8f0067876375c3498b00be9..a264064c5e5b0b84a960327e258ee726488a5269 100644 --- a/lms/djangoapps/discussion_api/views.py +++ b/lms/djangoapps/discussion_api/views.py @@ -2,13 +2,19 @@ Discussion API views """ from django.core.exceptions import ValidationError +from django.contrib.auth import get_user_model +from edx_rest_framework_extensions.authentication import JwtAuthentication from opaque_keys.edx.keys import CourseKey +from rest_framework import permissions +from rest_framework import status from rest_framework.exceptions import UnsupportedMediaType from rest_framework.parsers import JSONParser from rest_framework.response import Response from rest_framework.views import APIView from rest_framework.viewsets import ViewSet +from six import text_type +from lms.lib import comment_client from discussion_api.api import ( create_comment, create_thread, @@ -26,6 +32,8 @@ from discussion_api.api import ( from discussion_api.forms import CommentGetForm, CommentListGetForm, ThreadListGetForm from openedx.core.lib.api.parsers import MergePatchParser from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, view_auth_classes +from openedx.core.djangoapps.user_api.accounts.permissions import CanRetireUser +from student.models import get_potentially_retired_user_by_username_and_hash from xmodule.modulestore.django import modulestore @@ -512,3 +520,48 @@ class CommentViewSet(DeveloperErrorViewMixin, ViewSet): if request.content_type != MergePatchParser.media_type: raise UnsupportedMediaType(request.content_type) return Response(update_comment(request, comment_id, request.data)) + + +class RetireUserView(APIView): + """ + **Use Cases** + + A superuser or the user with the settings.RETIREMENT_SERVICE_WORKER_USERNAME + can "retire" the user's data from the comments service, which will remove + personal information and blank all posts / comments the user has made. + + **Example Requests**: + POST /api/discussion/v1/retire_user/ + { + "retired_username": "old_user_name" + } + + **Example Response**: + Empty string + """ + + authentication_classes = (JwtAuthentication,) + permission_classes = (permissions.IsAuthenticated, CanRetireUser) + + def post(self, request, username): + """ + Implements the retirement endpoint. + """ + user_model = get_user_model() + retired_username = request.data['retired_username'] + + try: + user = get_potentially_retired_user_by_username_and_hash(username, retired_username) + cc_user = comment_client.User.from_django_user(user) + + # We can't count on the LMS username being un-retired at this point, + # so we pass the old username as a parameter to describe which + # user to retire. This will either succeed or throw an error which + # should be good to raise from here. + cc_user.retire(username) + except user_model.DoesNotExist: + return Response(status=status.HTTP_404_NOT_FOUND) + except Exception as exc: # pylint: disable=broad-except + return Response(text_type(exc), status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/lms/lib/comment_client/user.py b/lms/lib/comment_client/user.py index 238061c330ad679b484be91fb3d09eb6afdadb88..c58092128f685c062fc60afa718ba20caaca493f 100644 --- a/lms/lib/comment_client/user.py +++ b/lms/lib/comment_client/user.py @@ -2,9 +2,7 @@ from six import text_type import settings - import models - import utils @@ -169,6 +167,19 @@ class User(models.Model): raise self._update_from_response(response) + def retire(self, retired_username): + url = _url_for_retire(self.id) + params = {'retired_username': retired_username} + + utils.perform_request( + 'post', + url, + params, + raw=True, + metric_action='user.retire', + metric_tags=self._metric_tags + ) + def _url_for_vote_comment(comment_id): return "{prefix}/comments/{comment_id}/votes".format(prefix=settings.PREFIX, comment_id=comment_id) @@ -195,3 +206,10 @@ def _url_for_read(user_id): Returns cs_comments_service url endpoint to mark thread as read for given user_id """ return "{prefix}/users/{user_id}/read".format(prefix=settings.PREFIX, user_id=user_id) + + +def _url_for_retire(user_id): + """ + Returns cs_comments_service url endpoint to retire a user (remove all post content, etc.) + """ + return "{prefix}/users/{user_id}/retire".format(prefix=settings.PREFIX, user_id=user_id)