Skip to content

Commit

Permalink
feat: Add Replace Username endpoint.
Browse files Browse the repository at this point in the history
  • Loading branch information
magajh committed Mar 25, 2021
1 parent db6dccf commit c827f09
Show file tree
Hide file tree
Showing 8 changed files with 234 additions and 2 deletions.
49 changes: 49 additions & 0 deletions eox_core/api/support/v1/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,59 @@
from django.utils import timezone
from rest_framework import serializers

from eox_core.api.v1.serializers import MAX_SIGNUP_SOURCES_ALLOWED
from eox_core.edxapp_wrapper.users import (
check_edxapp_account_conflicts,
get_user_signup_source,
get_username_max_length,
)

UserSignupSource = get_user_signup_source() # pylint: disable=invalid-name

USERNAME_MAX_LENGTH = get_username_max_length()


class WrittableEdxappRemoveUserSerializer(serializers.Serializer):
"""
Handles the serialization when a user is being removed.
"""
case_id = serializers.CharField(write_only=True, default=timezone.now().strftime('%Y%m%d%H%M%S'))
is_support_user = serializers.BooleanField(default=True)


class WrittableEdxappUsernameSerializer(serializers.Serializer):
"""
Handles the serialization of the data required to update the username of an edxapp user.
"""
new_username = serializers.CharField(max_length=USERNAME_MAX_LENGTH, write_only=True)

def validate(self, attrs):
"""
When a username update is being made, then it checks that:
- The new username is not already taken by other user.
- The user is not staff or superuser.
- The user has just one signup source.
"""
username = attrs.get("new_username")
conflicts = check_edxapp_account_conflicts(None, username)
if conflicts:
raise serializers.ValidationError({"detail": "An account already exists with the provided username."})

if self.instance.is_staff or self.instance.is_superuser:
raise serializers.ValidationError({"detail": "You can't update users with roles like staff or superuser."})

if UserSignupSource.objects.filter(user__email=self.instance.email).count() > MAX_SIGNUP_SOURCES_ALLOWED:
raise serializers.ValidationError({"detail": "You can't update users with more than one sign up source."})

return attrs

def update(self, instance, validated_data):
"""
Method to update username of edxapp User.
"""
key = 'username'
if validated_data:
setattr(instance, key, validated_data['new_username'])
instance.save()

return instance
85 changes: 85 additions & 0 deletions eox_core/api/support/v1/tests/test_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""
Test module for users viewset.
"""
from django.contrib.auth.models import User
from django.test import TestCase
from django.urls import reverse
from mock import patch
from rest_framework import status
from rest_framework.test import APIClient


class EdxappReplaceUsernameAPITest(TestCase):
"""Test class for update username APIView."""

patch_permissions = patch('eox_core.api.support.v1.permissions.EoxCoreSupportAPIPermission.has_permission', return_value=True)

def setUp(self):
"""Setup method for test class."""
self.user = User(username="test-username", email="[email protected]", password="testusername")
self.client = APIClient()
self.url = reverse("eox-support-api:eox-support-api:edxapp-replace-username")
self.client.force_authenticate(user=self.user)

@patch_permissions
@patch('eox_core.api.support.v1.views.replace_username_cs_user')
@patch('eox_core.api.support.v1.serializers.UserSignupSource')
@patch('eox_core.api.support.v1.views.get_edxapp_user')
@patch('eox_core.api.support.v1.views.EdxappUserReadOnlySerializer')
def test_replace_username_success(self, user_serializer, get_edxapp_user, signup_source, replace_username_cs_user, _):
"""Test the replacement of the username of an edxapp user."""
update_data = {
"username": self.user.username,
"new_username": "replaced-username",
}

user_serializer.return_value.data = {}
get_edxapp_user.return_value = self.user
signup_source.objects.filter.return_value.count.return_value = 1

response = self.client.patch(self.url, data=update_data, format="json")

self.assertEqual("replaced-username", self.user.username)
self.assertEqual(status.HTTP_200_OK, response.status_code)

@patch_permissions
@patch('eox_core.api.support.v1.serializers.UserSignupSource')
@patch('eox_core.api.support.v1.views.get_edxapp_user')
def test_replace_username_bad_sign_up_source(self, get_edxapp_user, signup_source, _):
"""
Tests that when a user has more than one signup source then the
username cannot be replaced.
"""
update_data = {
"username": self.user.username,
"new_username": "another-username",
}

get_edxapp_user.return_value = self.user
signup_source.objects.filter.return_value.count.return_value = 2

response = self.client.patch(self.url, data=update_data, format="json")

self.assertEqual(status.HTTP_400_BAD_REQUEST, response.status_code)
self.assertEqual(response.content, '{"detail":["You can\'t update users with more than one sign up source."]}'
.encode())

@patch_permissions
@patch('eox_core.api.support.v1.serializers.UserSignupSource')
@patch('eox_core.api.support.v1.views.get_edxapp_user')
def test_replace_username_staff_user(self, get_edxapp_user, signup_source, _):
"""Tests that if a user is staff or superuser then the username cannot be replaced."""
user = User(username="test", email="[email protected]", password="testtest", is_staff=True)
update_data = {
"username": user.username,
"new_username": "new-test-username",
}

get_edxapp_user.return_value = user
signup_source.objects.filter.return_value.count.return_value = 1

response = self.client.patch(self.url, data=update_data, format="json")

self.assertEqual(status.HTTP_400_BAD_REQUEST, response.status_code)
self.assertEqual(response.content, '{"detail":["You can\'t update users with roles like staff or superuser."]}'
.encode())
1 change: 1 addition & 0 deletions eox_core/api/support/v1/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@

urlpatterns = [ # pylint: disable=invalid-name
url(r'^user/$', views.EdxappUser.as_view(), name='edxapp-user'),
url(r'^user/replace-username/$', views.EdxappReplaceUsername.as_view(), name='edxapp-replace-username'),
]
61 changes: 59 additions & 2 deletions eox_core/api/support/v1/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,20 @@

import logging

from django.conf import settings
from django.contrib.sites.shortcuts import get_current_site
from django.db import transaction
from rest_framework.authentication import SessionAuthentication
from rest_framework.renderers import BrowsableAPIRenderer, JSONRenderer
from rest_framework.response import Response
from rest_framework.views import APIView

from eox_core.api.support.v1.permissions import EoxCoreSupportAPIPermission
from eox_core.api.support.v1.serializers import WrittableEdxappRemoveUserSerializer
from eox_core.api.support.v1.serializers import WrittableEdxappRemoveUserSerializer, WrittableEdxappUsernameSerializer
from eox_core.api.v1.serializers import EdxappUserReadOnlySerializer
from eox_core.api.v1.views import UserQueryMixin
from eox_core.edxapp_wrapper.bearer_authentication import BearerAuthentication
from eox_core.edxapp_wrapper.comments_service_users import replace_username_cs_user
from eox_core.edxapp_wrapper.users import delete_edxapp_user, get_edxapp_user

LOG = logging.getLogger(__name__)
Expand All @@ -37,7 +41,7 @@ def delete(self, request, *args, **kwargs): # pylint: disable=too-many-locals
For example:
**Requests**:
DELETE <domain>/eox-core/api/v1/remove-user/
DELETE <domain>/eox-core/support-api/v1/user/
**Request body**:
{
Expand All @@ -61,3 +65,56 @@ def delete(self, request, *args, **kwargs): # pylint: disable=too-many-locals
message, status = delete_edxapp_user(**data)

return Response(message, status=status)


class EdxappReplaceUsername(UserQueryMixin, APIView):
"""
Handles the replacement of the username.
"""

authentication_classes = (BearerAuthentication, SessionAuthentication)
permission_classes = (EoxCoreSupportAPIPermission,)
renderer_classes = (JSONRenderer, BrowsableAPIRenderer)

def patch(self, request, *args, **kwargs):
"""
Allows to safely update an Edxapp user's Username along with the
forum associated User.
For now users that have different signup sources cannot be updated.
For example:
**Requests**:
PATCH <domain>/eox-core/support-api/v1/replace-username/
**Request body**
{
"new_username": "new username"
}
**Response values**
User serialized.
"""
query = self.get_user_query(request)
user = get_edxapp_user(**query)
data = request.data

with transaction.atomic():
serializer = WrittableEdxappUsernameSerializer(user, data=data)
serializer.is_valid(raise_exception=True)
serializer.save()

data = serializer.validated_data
data["user"] = user

# Update user in cs_comments_service forums
replace_username_cs_user(**data)

admin_fields = getattr(settings, "ACCOUNT_VISIBILITY_CONFIGURATION", {}).get(
"admin_fields", {}
)
serialized_user = EdxappUserReadOnlySerializer(
user, custom_fields=admin_fields, context={"request": request}
)
return Response(serialized_user.data)
17 changes: 17 additions & 0 deletions eox_core/edxapp_wrapper/backends/comments_service_users_j_v1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
""" Module for the cs_comments_service User object."""
from openedx.core.djangoapps.django_comment_common.comment_client.user import User # pylint: disable=import-error


def replace_username_cs_user(*args, **kwargs):
"""
Replace user's username in cs_comments_service (forums).
kwargs:
user: edxapp user whose username is being replaced.
new_username: new username.
"""
user = kwargs.get("user")
new_username = kwargs.get("new_username")

cs_user = User.from_django_user(user)
cs_user.replace_username(new_username)
18 changes: 18 additions & 0 deletions eox_core/edxapp_wrapper/comments_service_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
User model wrapper for cs_comments_service public definition.
"""

from importlib import import_module

from django.conf import settings


def replace_username_cs_user(*args, **kwargs):
""" Gets the User model wrapper for comments service"""

backend_function = settings.EOX_CORE_COMMENTS_SERVICE_USERS_BACKEND
backend = import_module(backend_function)

return backend.replace_username_cs_user(*args, **kwargs)
1 change: 1 addition & 0 deletions eox_core/settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def plugin_settings(settings):
Defines eox-core settings when app is used as a plugin to edx-platform.
See: https://github.com/edx/edx-platform/blob/master/openedx/core/djangoapps/plugins/README.rst
"""
settings.EOX_CORE_COMMENTS_SERVICE_USERS_BACKEND = "eox_core.edxapp_wrapper.backends.comments_service_users_j_v1"
settings.EOX_CORE_USERS_BACKEND = "eox_core.edxapp_wrapper.backends.users_j_v1"
settings.EOX_CORE_ENROLLMENT_BACKEND = "eox_core.edxapp_wrapper.backends.enrollment_h_v1"
settings.EOX_CORE_PRE_ENROLLMENT_BACKEND = "eox_core.edxapp_wrapper.backends.pre_enrollment_h_v1"
Expand Down
4 changes: 4 additions & 0 deletions eox_core/settings/production.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ def plugin_settings(settings): # pylint: disable=function-redefined
Set of plugin settings used by the Open Edx platform.
More info: https://github.com/edx/edx-platform/blob/master/openedx/core/djangoapps/plugins/README.rst
"""
settings.EOX_CORE_COMMENTS_SERVICE_USERS_BACKEND = getattr(settings, 'ENV_TOKENS', {}).get(
'EOX_CORE_COMMENTS_SERVICE_USERS_BACKEND',
settings.EOX_CORE_COMMENTS_SERVICE_USERS_BACKEND
)
settings.EOX_CORE_BEARER_AUTHENTICATION = getattr(settings, 'ENV_TOKENS', {}).get(
'EOX_CORE_BEARER_AUTHENTICATION',
settings.EOX_CORE_BEARER_AUTHENTICATION
Expand Down

0 comments on commit c827f09

Please sign in to comment.