Add discussion api
This commit is contained in:
@@ -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})
|
||||
|
||||
@@ -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.'
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user