Merge pull request #27382 from edx/hammad/ENT-4483

ENT-4483 | added search_emails endpoint in accounts.
This commit is contained in:
Hammad Ahmad Waqas
2021-04-21 20:16:57 +05:00
committed by GitHub
4 changed files with 104 additions and 12 deletions

View File

@@ -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

View File

@@ -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.

View File

@@ -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}/

View File

@@ -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,