From 7489f14bafa48a93863b86e74995447111c72b0c Mon Sep 17 00:00:00 2001 From: HammadAhmadWaqas Date: Tue, 20 Apr 2021 16:35:27 +0500 Subject: [PATCH] added search_emails endpoin in accounts. --- .../user_api/accounts/serializers.py | 9 +++ .../user_api/accounts/tests/test_views.py | 32 +++++++++ .../djangoapps/user_api/accounts/views.py | 66 +++++++++++++++---- openedx/core/djangoapps/user_api/urls.py | 9 +++ 4 files changed, 104 insertions(+), 12 deletions(-) diff --git a/openedx/core/djangoapps/user_api/accounts/serializers.py b/openedx/core/djangoapps/user_api/accounts/serializers.py index 7dcf679b75..9929af0a60 100644 --- a/openedx/core/djangoapps/user_api/accounts/serializers.py +++ b/openedx/core/djangoapps/user_api/accounts/serializers.py @@ -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 diff --git a/openedx/core/djangoapps/user_api/accounts/tests/test_views.py b/openedx/core/djangoapps/user_api/accounts/tests/test_views.py index a00ac57440..3badf26491 100644 --- a/openedx/core/djangoapps/user_api/accounts/tests/test_views.py +++ b/openedx/core/djangoapps/user_api/accounts/tests/test_views.py @@ -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. diff --git a/openedx/core/djangoapps/user_api/accounts/views.py b/openedx/core/djangoapps/user_api/accounts/views.py index 04eb6b05f5..0841f1cbce 100644 --- a/openedx/core/djangoapps/user_api/accounts/views.py +++ b/openedx/core/djangoapps/user_api/accounts/views.py @@ -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}/ diff --git a/openedx/core/djangoapps/user_api/urls.py b/openedx/core/djangoapps/user_api/urls.py index ba8ee9a7e1..0006671f1b 100644 --- a/openedx/core/djangoapps/user_api/urls.py +++ b/openedx/core/djangoapps/user_api/urls.py @@ -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,