Merge pull request #27382 from edx/hammad/ENT-4483
ENT-4483 | added search_emails endpoint in accounts.
This commit is contained in:
@@ -489,6 +489,15 @@ class UserRetirementStatusSerializer(serializers.ModelSerializer):
|
||||
exclude = ['responses', ]
|
||||
|
||||
|
||||
class UserSearchEmailSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Perform serialization for the User model used in accounts/search_emails endpoint.
|
||||
"""
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ('email', 'id', 'username')
|
||||
|
||||
|
||||
class UserRetirementPartnerReportSerializer(serializers.Serializer):
|
||||
"""
|
||||
Perform serialization for the UserRetirementPartnerReportingStatus model
|
||||
|
||||
@@ -70,6 +70,16 @@ class UserAPITestCase(APITestCase):
|
||||
assert expected_status == response.status_code
|
||||
return response
|
||||
|
||||
def post_search_api(self, client, json_data, content_type='application/merge-patch+json', expected_status=200):
|
||||
"""
|
||||
Helper method for sending a post to the server, defaulting to application/merge-patch+json content_type.
|
||||
Verifies the expected status and returns the response.
|
||||
"""
|
||||
# pylint: disable=no-member
|
||||
response = client.post(self.search_api_url, data=json.dumps(json_data), content_type=content_type)
|
||||
assert expected_status == response.status_code
|
||||
return response
|
||||
|
||||
def send_get(self, client, query_parameters=None, expected_status=200):
|
||||
"""
|
||||
Helper method for sending a GET to the server. Verifies the expected status and returns the response.
|
||||
@@ -209,6 +219,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase):
|
||||
super().setUp()
|
||||
|
||||
self.url = reverse("accounts_api", kwargs={'username': self.user.username})
|
||||
self.search_api_url = reverse("accounts_search_emails_api")
|
||||
|
||||
def _set_user_age_to_10_years(self, user):
|
||||
"""
|
||||
@@ -346,6 +357,27 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase):
|
||||
response = self.send_get(client, query_parameters=f'email={self.user.email}')
|
||||
self._verify_full_account_response(response)
|
||||
|
||||
def test_search_emails(self):
|
||||
client = self.login_client('client', 'user')
|
||||
json_data = {'emails': [self.user.email]}
|
||||
response = self.post_search_api(client, json_data=json_data)
|
||||
assert response.data == [{'email': self.user.email, 'id': self.user.id, 'username': self.user.username}]
|
||||
|
||||
def test_search_emails_with_non_existing_email(self):
|
||||
client = self.login_client('client', 'user')
|
||||
json_data = {"emails": ['non_existant_email@example.com']}
|
||||
response = self.post_search_api(client, json_data=json_data)
|
||||
assert response.data == []
|
||||
|
||||
def test_search_emails_with_invalid_param(self):
|
||||
client = self.login_client('client', 'user')
|
||||
json_data = {'invalid_key': [self.user.email]}
|
||||
response = self.post_search_api(client, json_data=json_data, expected_status=400)
|
||||
assert response.data == {
|
||||
'developer_message': "'emails' field is required",
|
||||
'user_message': "'emails' field is required"
|
||||
}
|
||||
|
||||
# Note: using getattr so that the patching works even if there is no configuration.
|
||||
# This is needed when testing CMS as the patching is still executed even though the
|
||||
# suite is skipped.
|
||||
|
||||
@@ -38,17 +38,6 @@ from wiki.models import ArticleRevision
|
||||
from wiki.models.pluginbase import RevisionPluginRevision
|
||||
|
||||
from common.djangoapps.entitlements.models import CourseEntitlement
|
||||
from openedx.core.djangoapps.ace_common.template_context import get_base_template_context
|
||||
from openedx.core.djangoapps.api_admin.models import ApiAccessRequest
|
||||
from openedx.core.djangoapps.course_groups.models import UnregisteredLearnerCohortAssignments
|
||||
from openedx.core.djangoapps.credit.models import CreditRequest, CreditRequirementStatus
|
||||
from openedx.core.djangoapps.external_user_ids.models import ExternalId, ExternalIdType
|
||||
from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY
|
||||
from openedx.core.djangoapps.profile_images.images import remove_profile_images
|
||||
from openedx.core.djangoapps.user_api.accounts.image_helpers import get_profile_image_names, set_has_profile_image
|
||||
from openedx.core.djangoapps.user_authn.exceptions import AuthFailedError
|
||||
from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser
|
||||
from openedx.core.lib.api.parsers import MergePatchParser
|
||||
from common.djangoapps.student.models import ( # lint-amnesty, pylint: disable=unused-import
|
||||
AccountRecovery,
|
||||
CourseEnrollmentAllowed,
|
||||
@@ -64,6 +53,17 @@ from common.djangoapps.student.models import ( # lint-amnesty, pylint: disable=
|
||||
get_retired_username_by_username,
|
||||
is_username_retired
|
||||
)
|
||||
from openedx.core.djangoapps.ace_common.template_context import get_base_template_context
|
||||
from openedx.core.djangoapps.api_admin.models import ApiAccessRequest
|
||||
from openedx.core.djangoapps.course_groups.models import UnregisteredLearnerCohortAssignments
|
||||
from openedx.core.djangoapps.credit.models import CreditRequest, CreditRequirementStatus
|
||||
from openedx.core.djangoapps.external_user_ids.models import ExternalId, ExternalIdType
|
||||
from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY
|
||||
from openedx.core.djangoapps.profile_images.images import remove_profile_images
|
||||
from openedx.core.djangoapps.user_api.accounts.image_helpers import get_profile_image_names, set_has_profile_image
|
||||
from openedx.core.djangoapps.user_authn.exceptions import AuthFailedError
|
||||
from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser
|
||||
from openedx.core.lib.api.parsers import MergePatchParser
|
||||
|
||||
from ..errors import AccountUpdateError, AccountValidationError, UserNotAuthorized, UserNotFound
|
||||
from ..message_types import DeletionNotificationMessage
|
||||
@@ -76,7 +76,11 @@ from ..models import (
|
||||
)
|
||||
from .api import get_account_settings, update_account_settings
|
||||
from .permissions import CanDeactivateUser, CanReplaceUsername, CanRetireUser
|
||||
from .serializers import UserRetirementPartnerReportSerializer, UserRetirementStatusSerializer
|
||||
from .serializers import (
|
||||
UserRetirementPartnerReportSerializer,
|
||||
UserRetirementStatusSerializer,
|
||||
UserSearchEmailSerializer
|
||||
)
|
||||
from .signals import USER_RETIRE_LMS_CRITICAL, USER_RETIRE_LMS_MISC, USER_RETIRE_MAILINGS
|
||||
from .utils import create_retirement_request_and_deactivate_account
|
||||
|
||||
@@ -134,6 +138,8 @@ class AccountViewSet(ViewSet):
|
||||
|
||||
PATCH /api/user/v1/accounts/{username}/{"key":"value"} "application/merge-patch+json"
|
||||
|
||||
POST /api/user/v1/accounts/search_emails "application/merge-patch+json"
|
||||
|
||||
**Notes for PATCH requests to /accounts endpoints**
|
||||
* Requested updates to social_links are automatically merged with
|
||||
previously set links. That is, any newly introduced platforms are
|
||||
@@ -312,6 +318,42 @@ class AccountViewSet(ViewSet):
|
||||
|
||||
return Response(account_settings)
|
||||
|
||||
def search_emails(self, request):
|
||||
"""
|
||||
POST /api/user/v1/accounts/search_emails
|
||||
Content Type: "application/merge-patch+json"
|
||||
{
|
||||
"emails": ["edx@example.com", "staff@example.com"]
|
||||
}
|
||||
Response:
|
||||
[
|
||||
{
|
||||
"username": "edx",
|
||||
"email": "edx@example.com",
|
||||
"id": 3,
|
||||
},
|
||||
{
|
||||
"username": "staff",
|
||||
"email": "staff@example.com",
|
||||
"id": 8,
|
||||
}
|
||||
]
|
||||
"""
|
||||
try:
|
||||
user_emails = request.data['emails']
|
||||
except KeyError as error:
|
||||
error_message = f'{error} field is required'
|
||||
return Response(
|
||||
{
|
||||
'developer_message': error_message,
|
||||
'user_message': error_message
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
users = User.objects.filter(email__in=user_emails)
|
||||
data = UserSearchEmailSerializer(users, many=True).data
|
||||
return Response(data)
|
||||
|
||||
def retrieve(self, request, username):
|
||||
"""
|
||||
GET /api/user/v1/accounts/{username}/
|
||||
|
||||
@@ -31,6 +31,10 @@ ACCOUNT_LIST = AccountViewSet.as_view({
|
||||
'get': 'list',
|
||||
})
|
||||
|
||||
ACCOUNT_SEARCH_EMAILS = AccountViewSet.as_view({
|
||||
'post': 'search_emails',
|
||||
})
|
||||
|
||||
ACCOUNT_DETAIL = AccountViewSet.as_view({
|
||||
'get': 'retrieve',
|
||||
'patch': 'partial_update',
|
||||
@@ -88,6 +92,11 @@ urlpatterns = [
|
||||
ACCOUNT_LIST,
|
||||
name='accounts_detail_api'
|
||||
),
|
||||
url(
|
||||
r'^v1/accounts/search_emails$',
|
||||
ACCOUNT_SEARCH_EMAILS,
|
||||
name='accounts_search_emails_api'
|
||||
),
|
||||
url(
|
||||
fr'^v1/accounts/{settings.USERNAME_PATTERN}$',
|
||||
ACCOUNT_DETAIL,
|
||||
|
||||
Reference in New Issue
Block a user