Add discussion api

This commit is contained in:
Matt Tuchfarber
2019-02-28 16:35:09 -05:00
parent 5a0659ed9e
commit eec95cf538
8 changed files with 162 additions and 3 deletions

View File

@@ -10,6 +10,7 @@ from urlparse import urlparse
import ddt
import httpretty
import mock
from django.conf import settings
from django.urls import reverse
from edx_oauth2_provider.tests.factories import ClientFactory, AccessTokenFactory
from opaque_keys.edx.keys import CourseKey
@@ -235,6 +236,81 @@ class RetireViewTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
"""
pass
@httpretty.activate
@mock.patch('django.conf.settings.USERNAME_REPLACEMENT_WORKER', 'test_replace_username_service_worker')
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
class ReplaceUsernameViewTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
"""Tests for ReplaceUsernameView"""
def setUp(self):
super(ReplaceUsernameViewTest, self).setUp()
self.client_user = UserFactory()
self.client_user.username = "test_replace_username_service_worker"
self.new_username = "test_username_replacement"
self.url = reverse("replace_discussion_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 = create_jwt_for_user(user)
headers = {'HTTP_AUTHORIZATION': 'JWT ' + token}
return headers
def test_missing_params(self):
headers = self.build_jwt_headers(self.client_user)
# Using this instead of ddt so we can access self.user
bad_post_contents = (
{},
{"current_username": self.user.username},
{"new_username": "test_username_replacement"},
)
for data in bad_post_contents:
response = self.client.post(self.url, data, **headers)
self.assert_response_correct(response, 400, "")
def test_basic(self):
""" Check successful replacement """
self.register_get_username_replacement_response(self.user)
headers = self.build_jwt_headers(self.client_user)
data = {"current_username": self.user.username, "new_username": self.new_username}
response = self.client.post(self.url, data, **headers)
self.assert_response_correct(response, 204, "")
def test_nonexistant_user(self):
self.register_get_username_replacement_response(self.user)
headers = self.build_jwt_headers(self.client_user)
data = {"current_username": "non-existant-user", "new_username": self.new_username}
response = self.client.post(self.url, data, **headers)
self.assert_response_correct(response, 404, "")
def test_client_404(self):
self.register_get_username_replacement_response(self.user, status=404)
headers = self.build_jwt_headers(self.client_user)
data = {"current_username": self.user.username, "new_username": self.new_username}
response = self.client.post(self.url, data, **headers)
self.assert_response_correct(response, 404, "")
def test_client_500(self):
self.register_get_username_replacement_response(self.user, status=500)
headers = self.build_jwt_headers(self.client_user)
data = {"current_username": self.user.username, "new_username": self.new_username}
response = self.client.post(self.url, data, **headers)
self.assert_response_correct(response, 500, "")
def test_not_authenticated(self):
"""
Override the parent implementation of this, we JWT auth for this API
"""
pass
@ddt.ddt
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})

View File

@@ -226,6 +226,15 @@ class CommentsServiceMockMixin(object):
status=status
)
def register_get_username_replacement_response(self, user, status=200, body=""):
assert httpretty.is_enabled(), 'httpretty must be enabled to mock calls.'
httpretty.register_uri(
httpretty.POST,
"http://localhost:4567/api/v1/users/{id}/replace_username".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.'

View File

@@ -13,6 +13,7 @@ from discussion_api.views import (
CourseView,
ThreadViewSet,
RetireUserView,
ReplaceUsernameView,
)
ROUTER = SimpleRouter()
@@ -40,6 +41,7 @@ urlpatterns = [
name="discussion_course"
),
url(r"^v1/accounts/retire_forum", RetireUserView.as_view(), name="retire_discussion_user"),
url(r"^v1/accounts/replace_username", ReplaceUsernameView.as_view(), name="replace_discussion_username"),
url(
r"^v1/course_topics/{}".format(settings.COURSE_ID_PATTERN),
CourseTopicsView.as_view(),

View File

@@ -1,6 +1,7 @@
"""
Discussion API views
"""
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser
@@ -49,7 +50,7 @@ from discussion_api.serializers import (
from openedx.core.lib.api.authentication import OAuth2AuthenticationAllowInactiveUser
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 openedx.core.djangoapps.user_api.accounts.permissions import CanReplaceUsername, CanRetireUser
from openedx.core.djangoapps.user_api.models import UserRetirementStatus
from util.json_request import JsonResponse
from xmodule.modulestore.django import modulestore
@@ -587,6 +588,57 @@ class RetireUserView(APIView):
return Response(status=status.HTTP_204_NO_CONTENT)
class ReplaceUsernameView(APIView):
"""
A request from the settings.USERNAME_REPLACEMENT_WORKER user can replace
the username for a user. This will change their username and update all of
their comments to have the new username
POST /api/discussion/v1/accounts/replace_username/
{
"current_username": "users_current_username",
"new_username": "name_to_change_it_to"
}
Returns empty response if successful
"""
authentication_classes = (JwtAuthentication,)
permission_classes = (permissions.IsAuthenticated, CanReplaceUsername)
def post(self, request):
"""
Implements the username replacement endpoint
"""
current_username = request.data.get("current_username")
new_username = request.data.get("new_username")
if not (current_username and new_username):
return Response(status=status.HTTP_400_BAD_REQUEST)
try:
current_user = User.objects.get(username=current_username)
cc_user = comment_client.User.from_django_user(current_user)
cc_user.replace_username(new_username)
except User.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)
except comment_client.CommentClientRequestError as exc:
if exc.status_code == 404:
return Response(status=status.HTTP_404_NOT_FOUND)
raise
except Exception as exc:
return Response(text_type(exc), status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return Response(status=status.HTTP_204_NO_CONTENT)
class CourseDiscussionSettingsAPIView(DeveloperErrorViewMixin, APIView):
"""

View File

@@ -3450,6 +3450,8 @@ RETIREMENT_STATES = [
'COMPLETE',
]
USERNAME_REPLACEMENT_WORKER = "REPLACE WITH VALID USERNAME"
############## Settings for Microfrontends #########################
# If running a Gradebook container locally,
# modify lms/envs/private.py to give it a non-null value

View File

@@ -180,6 +180,17 @@ class User(models.Model):
metric_tags=self._metric_tags
)
def replace_username(self, new_username):
url = _url_for_username_replacement(self.id)
params = {"new_username": new_username}
utils.perform_request(
'post',
url,
params,
raw=True,
)
def _url_for_vote_comment(comment_id):
return "{prefix}/comments/{comment_id}/votes".format(prefix=settings.PREFIX, comment_id=comment_id)
@@ -213,3 +224,9 @@ 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)
def _url_for_username_replacement(user_id):
"""
Returns cs_comments_servuce url endpoint to replace the username of a user
"""
return "{prefix}/users/{user_id}/replace_username".format(prefix=settings.PREFIX, user_id=user_id)

View File

@@ -6,8 +6,6 @@ from __future__ import unicode_literals
from django.conf import settings
from rest_framework import permissions
USERNAME_REPLACEMENT_GROUP = "username_replacement_admin"
class CanDeactivateUser(permissions.BasePermission):
"""
Grants access to AccountDeactivationView if the requesting user is a superuser

View File

@@ -1014,6 +1014,9 @@ class UsernameReplacementView(APIView):
API will recieve a list of current usernames and their requested new
username. If their new username is taken, it will randomly assign a new username.
This API will be called first, before calling the APIs in other services as this
one handles the checks on the usernames provided.
"""
authentication_classes = (JwtAuthentication, )
permission_classes = (permissions.IsAuthenticated, CanReplaceUsername)