feat: Program Inspectors API View
This commit is contained in:
committed by
Ansab Gillani
parent
573c841bf6
commit
8610856a30
@@ -1113,6 +1113,285 @@ class ProgramEnrollmentsInspectorViewTests(SupportViewTestCase):
|
||||
assert expected_info == render_call_dict['learner_program_enrollments']
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class ProgramEnrollmentsInspectorAPIViewTests(SupportViewTestCase):
|
||||
"""
|
||||
View tests for Program Enrollments Inspector API
|
||||
"""
|
||||
_url = reverse("support:program_enrollments_inspector_details")
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
SupportStaffRole().add_users(self.user)
|
||||
self.program_uuid = str(uuid4())
|
||||
self.external_user_key = 'abcaaa'
|
||||
# Setup three orgs and their SAML providers
|
||||
self.org_key_list = ['test_org', 'donut_org', 'tri_org']
|
||||
for org_key in self.org_key_list:
|
||||
lms_org = OrganizationFactory(
|
||||
short_name=org_key
|
||||
)
|
||||
SAMLProviderConfigFactory(
|
||||
organization=lms_org,
|
||||
slug=org_key,
|
||||
enabled=True,
|
||||
)
|
||||
self.no_saml_org_key = 'no_saml_org'
|
||||
self.no_saml_lms_org = OrganizationFactory(
|
||||
short_name=self.no_saml_org_key
|
||||
)
|
||||
|
||||
def _serialize_datetime(self, dt):
|
||||
return dt.strftime('%Y-%m-%dT%H:%M:%S')
|
||||
|
||||
def test_default_response(self):
|
||||
response = self.client.get(self._url)
|
||||
content = json.loads(response.content.decode('utf-8'))
|
||||
assert response.status_code == 200
|
||||
assert '' == content['org_keys']
|
||||
|
||||
def _construct_user(self, username, org_key=None, external_user_key=None):
|
||||
"""
|
||||
Provided the username, create an edx account user. If the org_key is provided,
|
||||
SSO link the user with the IdP associated with org_key. Return the created user and
|
||||
expected user info object from the view
|
||||
"""
|
||||
user = UserFactory(username=username)
|
||||
user_info = {
|
||||
'username': user.username,
|
||||
'email': user.email
|
||||
}
|
||||
if org_key and external_user_key:
|
||||
user_social_auth = UserSocialAuth.objects.create(
|
||||
user=user,
|
||||
uid=f'{org_key}:{external_user_key}',
|
||||
provider='tpa-saml'
|
||||
)
|
||||
user_info['sso_list'] = [{
|
||||
'uid': user_social_auth.uid
|
||||
}]
|
||||
return user, user_info
|
||||
|
||||
def _construct_enrollments(self, program_uuids, course_ids, external_user_key, edx_user=None):
|
||||
"""
|
||||
A helper function to setup the program enrollments for a given learner.
|
||||
If the edx user is provided, it will try to SSO the user with the enrollments
|
||||
Return the expected info object that should be created based on the model setup
|
||||
"""
|
||||
program_enrollments = []
|
||||
for program_uuid in program_uuids:
|
||||
course_enrollment = None
|
||||
program_enrollment = ProgramEnrollmentFactory.create(
|
||||
external_user_key=external_user_key,
|
||||
program_uuid=program_uuid,
|
||||
user=edx_user
|
||||
)
|
||||
|
||||
for course_id in course_ids:
|
||||
if edx_user:
|
||||
course_enrollment = CourseEnrollmentFactory.create(
|
||||
course_id=course_id,
|
||||
user=edx_user,
|
||||
mode=CourseMode.MASTERS,
|
||||
is_active=True
|
||||
)
|
||||
|
||||
program_course_enrollment = ProgramCourseEnrollmentFactory.create( # lint-amnesty, pylint: disable=unused-variable
|
||||
program_enrollment=program_enrollment,
|
||||
course_key=course_id,
|
||||
course_enrollment=course_enrollment,
|
||||
status='active',
|
||||
)
|
||||
|
||||
program_enrollments.append(program_enrollment)
|
||||
|
||||
serialized = ProgramEnrollmentSerializer(program_enrollments, many=True)
|
||||
return serialized.data
|
||||
|
||||
def _construct_id_verification(self, user):
|
||||
"""
|
||||
Helper function to create the SSO verified record for the user
|
||||
so that the user is ID Verified
|
||||
"""
|
||||
SSOVerificationFactory(
|
||||
identity_provider_slug=self.org_key_list[0],
|
||||
user=user,
|
||||
)
|
||||
return IDVerificationService.user_status(user)
|
||||
|
||||
def test_search_username_well_connected_user(self):
|
||||
created_user, expected_user_info = self._construct_user(
|
||||
'test_user_connected',
|
||||
self.org_key_list[0],
|
||||
self.external_user_key
|
||||
)
|
||||
id_verified = self._construct_id_verification(created_user)
|
||||
expected_enrollments = self._construct_enrollments(
|
||||
[self.program_uuid],
|
||||
[self.course.id],
|
||||
self.external_user_key,
|
||||
created_user
|
||||
)
|
||||
response = self.client.get(self._url + f'?edx_user={created_user.username}&org_key={self.org_key_list[0]}')
|
||||
response = json.loads(response.content.decode('utf-8'))
|
||||
expected_info = {
|
||||
'user': expected_user_info,
|
||||
'enrollments': expected_enrollments,
|
||||
'id_verification': id_verified
|
||||
}
|
||||
assert expected_info == response['learner_program_enrollments']
|
||||
|
||||
def test_search_username_user_not_connected(self):
|
||||
created_user, expected_user_info = self._construct_user('user_not_connected')
|
||||
response = self.client.get(self._url + f'?edx_user={created_user.username}&org_key={self.org_key_list[0]}')
|
||||
response = json.loads(response.content.decode('utf-8'))
|
||||
expected_info = {
|
||||
'user': expected_user_info,
|
||||
'id_verification': IDVerificationService.user_status(created_user)
|
||||
}
|
||||
|
||||
assert expected_info == response['learner_program_enrollments']
|
||||
|
||||
def test_search_username_user_no_enrollment(self):
|
||||
created_user, expected_user_info = self._construct_user(
|
||||
'user_connected',
|
||||
self.org_key_list[0],
|
||||
self.external_user_key
|
||||
)
|
||||
response = self.client.get(self._url + f'?edx_user={created_user.username}&org_key={self.org_key_list[0]}')
|
||||
response = json.loads(response.content.decode('utf-8'))
|
||||
expected_info = {
|
||||
'user': expected_user_info,
|
||||
'id_verification': IDVerificationService.user_status(created_user),
|
||||
}
|
||||
assert expected_info == response['learner_program_enrollments']
|
||||
|
||||
def test_search_username_user_no_course_enrollment(self):
|
||||
created_user, expected_user_info = self._construct_user(
|
||||
'user_connected',
|
||||
self.org_key_list[0],
|
||||
self.external_user_key
|
||||
)
|
||||
expected_enrollments = self._construct_enrollments(
|
||||
[self.program_uuid],
|
||||
[],
|
||||
self.external_user_key,
|
||||
created_user,
|
||||
)
|
||||
response = self.client.get(self._url + f'?edx_user={created_user.username}&org_key={self.org_key_list[0]}')
|
||||
response = json.loads(response.content.decode('utf-8'))
|
||||
expected_info = {
|
||||
'user': expected_user_info,
|
||||
'enrollments': expected_enrollments,
|
||||
'id_verification': IDVerificationService.user_status(created_user),
|
||||
}
|
||||
|
||||
assert expected_info == response['learner_program_enrollments']
|
||||
|
||||
def test_search_username_user_not_connected_with_enrollments(self):
|
||||
created_user, expected_user_info = self._construct_user(
|
||||
'user_not_connected'
|
||||
)
|
||||
self._construct_enrollments(
|
||||
[self.program_uuid],
|
||||
[],
|
||||
self.external_user_key,
|
||||
)
|
||||
response = self.client.get(self._url + f'?edx_user={created_user.username}&org_key={self.org_key_list[0]}')
|
||||
response = json.loads(response.content.decode('utf-8'))
|
||||
expected_info = {
|
||||
'user': expected_user_info,
|
||||
'id_verification': IDVerificationService.user_status(created_user),
|
||||
}
|
||||
assert expected_info == response['learner_program_enrollments']
|
||||
|
||||
def test_search_username_user_id_verified(self):
|
||||
created_user, expected_user_info = self._construct_user(
|
||||
'user_not_connected'
|
||||
)
|
||||
id_verified = self._construct_id_verification(created_user)
|
||||
expected_info = {
|
||||
'user': expected_user_info,
|
||||
'id_verification': id_verified
|
||||
}
|
||||
response = self.client.get(self._url + f'?edx_user={created_user.username}&org_key={self.org_key_list[0]}')
|
||||
response = json.loads(response.content.decode('utf-8'))
|
||||
assert expected_info == response['learner_program_enrollments']
|
||||
|
||||
@ddt.data(
|
||||
('', 'test_org'),
|
||||
('bad_key', '')
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_search_no_external_user_key(self, user_key, org_key):
|
||||
response = self.client.get(self._url + f'?external_user_key={user_key}&org_key={org_key}')
|
||||
response = json.loads(response.content.decode('utf-8'))
|
||||
expected_error = (
|
||||
"To perform a search, you must provide either the student's "
|
||||
"(a) edX username, "
|
||||
"(b) email address associated with their edX account, or "
|
||||
"(c) Identity-providing institution and external key!"
|
||||
)
|
||||
|
||||
assert {} == response['learner_program_enrollments']
|
||||
assert expected_error == response['error']
|
||||
|
||||
def test_search_external_user_not_connected(self):
|
||||
expected_enrollments = self._construct_enrollments(
|
||||
[self.program_uuid],
|
||||
[self.course.id],
|
||||
self.external_user_key,
|
||||
)
|
||||
response = self.client.get(
|
||||
self._url + f'?external_user_key={self.external_user_key}&org_key={self.org_key_list[0]}'
|
||||
)
|
||||
response = json.loads(response.content.decode('utf-8'))
|
||||
expected_info = {
|
||||
'user': {
|
||||
'external_user_key': self.external_user_key,
|
||||
},
|
||||
'enrollments': expected_enrollments
|
||||
}
|
||||
assert expected_info == response['learner_program_enrollments']
|
||||
|
||||
def test_search_external_user_not_in_system(self):
|
||||
external_user_key = 'not_in_system'
|
||||
response = self.client.get(
|
||||
self._url + f'?external_user_key={external_user_key}&org_key={self.org_key_list[0]}'
|
||||
)
|
||||
response = json.loads(response.content.decode('utf-8'))
|
||||
expected_error = 'No user found for external key {} for institution {}'.format(
|
||||
external_user_key, self.org_key_list[0]
|
||||
)
|
||||
assert expected_error == response['error']
|
||||
|
||||
def test_search_external_user_case_insensitive(self):
|
||||
external_user_key = 'AbCdEf123'
|
||||
requested_external_user_key = 'aBcDeF123'
|
||||
created_user, expected_user_info = self._construct_user(
|
||||
'test_user_connected',
|
||||
self.org_key_list[0],
|
||||
external_user_key
|
||||
)
|
||||
expected_enrollments = self._construct_enrollments(
|
||||
[self.program_uuid],
|
||||
[self.course.id],
|
||||
external_user_key,
|
||||
created_user
|
||||
)
|
||||
id_verified = self._construct_id_verification(created_user)
|
||||
response = self.client.get(
|
||||
self._url + f'?external_user_key={requested_external_user_key}&org_key={self.org_key_list[0]}'
|
||||
)
|
||||
response = json.loads(response.content.decode('utf-8'))
|
||||
expected_info = {
|
||||
'user': expected_user_info,
|
||||
'enrollments': expected_enrollments,
|
||||
'id_verification': id_verified,
|
||||
}
|
||||
assert expected_info == response['learner_program_enrollments']
|
||||
|
||||
|
||||
class SsoRecordsTests(SupportViewTestCase): # lint-amnesty, pylint: disable=missing-class-docstring
|
||||
|
||||
def setUp(self):
|
||||
|
||||
@@ -17,6 +17,7 @@ from .views.program_enrollments import (
|
||||
LinkProgramEnrollmentSupportAPIView,
|
||||
ProgramEnrollmentsInspectorView,
|
||||
SAMLProvidersWithOrg,
|
||||
ProgramEnrollmentsInspectorAPIView,
|
||||
)
|
||||
from .views.sso_records import SsoView
|
||||
from .views.onboarding_status import OnboardingView
|
||||
@@ -71,6 +72,11 @@ urlpatterns = [
|
||||
SAMLProvidersWithOrg.as_view(),
|
||||
name='get_saml_providers'
|
||||
),
|
||||
re_path(
|
||||
r'program_enrollments_inspector_details/?$',
|
||||
ProgramEnrollmentsInspectorAPIView.as_view(),
|
||||
name='program_enrollments_inspector_details'
|
||||
),
|
||||
re_path(r'sso_records/(?P<username_or_email>[\w.@+-]+)?$', SsoView.as_view(), name='sso_records'),
|
||||
re_path(
|
||||
r'onboarding_status/(?P<username_or_email>[\w.@+-]+)?$',
|
||||
|
||||
@@ -116,64 +116,11 @@ class LinkProgramEnrollmentSupportAPIView(APIView):
|
||||
return Response(data)
|
||||
|
||||
|
||||
class ProgramEnrollmentsInspectorView(View):
|
||||
class ProgramEnrollmentInspector:
|
||||
"""
|
||||
The view to search and display the program enrollments
|
||||
information of a learner.
|
||||
A common class to provide functionality of search and display the program enrollments
|
||||
information of a learner for Program Inspector Views and APIViews.
|
||||
"""
|
||||
exclude_from_schema = True
|
||||
CONSOLE_TEMPLATE_PATH = 'support/program_enrollments_inspector.html'
|
||||
|
||||
@method_decorator(require_support_permission)
|
||||
def get(self, request):
|
||||
"""
|
||||
Based on the query string parameters passed through the GET request
|
||||
Search the data store for information about ProgramEnrollment and
|
||||
SSO linkage with the user.
|
||||
"""
|
||||
search_error = ''
|
||||
edx_username_or_email = request.GET.get('edx_user', '').strip()
|
||||
org_key = request.GET.get('org_key', '').strip()
|
||||
external_user_key = request.GET.get('external_user_key', '').strip()
|
||||
learner_program_enrollments = {}
|
||||
saml_providers_with_org_key = self._get_org_keys_and_idps()
|
||||
selected_provider = None
|
||||
if org_key:
|
||||
selected_provider = saml_providers_with_org_key.get(org_key)
|
||||
if edx_username_or_email:
|
||||
learner_program_enrollments, search_error = self._get_account_info(
|
||||
edx_username_or_email,
|
||||
selected_provider,
|
||||
)
|
||||
elif org_key and external_user_key:
|
||||
learner_program_enrollments = self._get_external_user_info(
|
||||
external_user_key,
|
||||
org_key,
|
||||
selected_provider,
|
||||
)
|
||||
if not learner_program_enrollments:
|
||||
search_error = 'No user found for external key {} for institution {}'.format(
|
||||
external_user_key, org_key
|
||||
)
|
||||
elif not org_key and not external_user_key:
|
||||
# This is initial rendering state.
|
||||
pass
|
||||
else:
|
||||
search_error = (
|
||||
"To perform a search, you must provide either the student's "
|
||||
"(a) edX username, "
|
||||
"(b) email address associated with their edX account, or "
|
||||
"(c) Identity-providing institution and external key!"
|
||||
)
|
||||
|
||||
return render_to_response(
|
||||
self.CONSOLE_TEMPLATE_PATH,
|
||||
{
|
||||
'error': search_error,
|
||||
'learner_program_enrollments': learner_program_enrollments,
|
||||
'org_keys': sorted(saml_providers_with_org_key.keys()),
|
||||
}
|
||||
)
|
||||
|
||||
def _get_org_keys_and_idps(self):
|
||||
"""
|
||||
@@ -297,3 +244,156 @@ class SAMLProvidersWithOrg(APIView):
|
||||
).select_related('organization')
|
||||
|
||||
return [saml_provider.organization.short_name for saml_provider in saml_providers]
|
||||
|
||||
|
||||
class ProgramEnrollmentsInspectorView(ProgramEnrollmentInspector, View):
|
||||
"""
|
||||
The view to search and display the program enrollments
|
||||
information of a learner.
|
||||
"""
|
||||
exclude_from_schema = True
|
||||
CONSOLE_TEMPLATE_PATH = 'support/program_enrollments_inspector.html'
|
||||
|
||||
@method_decorator(require_support_permission)
|
||||
def get(self, request):
|
||||
"""
|
||||
Based on the query string parameters passed through the GET request
|
||||
Search the data store for information about ProgramEnrollment and
|
||||
SSO linkage with the user.
|
||||
"""
|
||||
search_error = ''
|
||||
edx_username_or_email = request.GET.get('edx_user', '').strip()
|
||||
org_key = request.GET.get('org_key', '').strip()
|
||||
external_user_key = request.GET.get('external_user_key', '').strip()
|
||||
learner_program_enrollments = {}
|
||||
saml_providers_with_org_key = self._get_org_keys_and_idps()
|
||||
selected_provider = None
|
||||
if org_key:
|
||||
selected_provider = saml_providers_with_org_key.get(org_key)
|
||||
if edx_username_or_email:
|
||||
learner_program_enrollments, search_error = self._get_account_info(
|
||||
edx_username_or_email,
|
||||
selected_provider,
|
||||
)
|
||||
elif org_key and external_user_key:
|
||||
learner_program_enrollments = self._get_external_user_info(
|
||||
external_user_key,
|
||||
org_key,
|
||||
selected_provider,
|
||||
)
|
||||
if not learner_program_enrollments:
|
||||
search_error = 'No user found for external key {} for institution {}'.format(
|
||||
external_user_key, org_key
|
||||
)
|
||||
elif not org_key and not external_user_key:
|
||||
# This is initial rendering state.
|
||||
pass
|
||||
else:
|
||||
search_error = (
|
||||
"To perform a search, you must provide either the student's "
|
||||
"(a) edX username, "
|
||||
"(b) email address associated with their edX account, or "
|
||||
"(c) Identity-providing institution and external key!"
|
||||
)
|
||||
|
||||
return render_to_response(
|
||||
self.CONSOLE_TEMPLATE_PATH,
|
||||
{
|
||||
'error': search_error,
|
||||
'learner_program_enrollments': learner_program_enrollments,
|
||||
'org_keys': sorted(saml_providers_with_org_key.keys()),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ProgramEnrollmentsInspectorAPIView(ProgramEnrollmentInspector, APIView):
|
||||
"""
|
||||
The APIview to search and display the program enrollments
|
||||
information of a learner.
|
||||
"""
|
||||
|
||||
authentication_classes = (
|
||||
JwtAuthentication, SessionAuthentication
|
||||
)
|
||||
permission_classes = (
|
||||
IsAuthenticated,
|
||||
)
|
||||
|
||||
@method_decorator(require_support_permission)
|
||||
def get(self, request):
|
||||
"""
|
||||
Based on the query string parameters passed through the GET request
|
||||
Search the data store for information about ProgramEnrollment and
|
||||
SSO linkage with the user.
|
||||
* Example Request:
|
||||
- GET / support/program_enrollments_inspector_details?
|
||||
edx_user=<edx_user>&org_key=<org_key>&external_user_key=<external_user_key>
|
||||
* Example Response:
|
||||
{
|
||||
learner_program_enrollments: {
|
||||
"user": {
|
||||
"username": "edx",
|
||||
"email": "edx@example.com"
|
||||
},
|
||||
"id_verification": {
|
||||
"status": "none",
|
||||
"error": <error>,
|
||||
"should_display": true,
|
||||
"status_date": <status_date>,
|
||||
"verification_expiry": <verification_expiry>
|
||||
},
|
||||
"enrollments": [
|
||||
{
|
||||
"created": "2021-11-25T04:56:25",
|
||||
"modified": "2021-12-19T22:27:34",
|
||||
"external_user_key": "testuser",
|
||||
"status": "enrolled",
|
||||
"program_uuid": <program_uuid>,
|
||||
"program_course_enrollments": [],
|
||||
"program_name": <program_name>
|
||||
}
|
||||
],
|
||||
"user": {
|
||||
"external_user_key": "testuser"
|
||||
}
|
||||
},
|
||||
org_key: < org_key >
|
||||
errors: 'Error messages for invalid query'
|
||||
}
|
||||
"""
|
||||
search_error = ''
|
||||
edx_username_or_email = request.query_params.get('edx_user', '').strip()
|
||||
org_key = request.query_params.get('org_key', '').strip()
|
||||
external_user_key = request.query_params.get('external_user_key', '').strip()
|
||||
learner_program_enrollments = {}
|
||||
saml_providers_with_org_key = self._get_org_keys_and_idps()
|
||||
selected_provider = None
|
||||
if org_key:
|
||||
selected_provider = saml_providers_with_org_key.get(org_key)
|
||||
if edx_username_or_email:
|
||||
learner_program_enrollments, search_error = self._get_account_info(
|
||||
edx_username_or_email,
|
||||
selected_provider,
|
||||
)
|
||||
elif org_key and external_user_key:
|
||||
learner_program_enrollments = self._get_external_user_info(
|
||||
external_user_key,
|
||||
org_key,
|
||||
selected_provider,
|
||||
)
|
||||
if not learner_program_enrollments:
|
||||
search_error = 'No user found for external key {} for institution {}'.format(
|
||||
external_user_key, org_key
|
||||
)
|
||||
else:
|
||||
search_error = (
|
||||
"To perform a search, you must provide either the student's "
|
||||
"(a) edX username, "
|
||||
"(b) email address associated with their edX account, or "
|
||||
"(c) Identity-providing institution and external key!"
|
||||
)
|
||||
return Response(data={
|
||||
'error': search_error,
|
||||
'learner_program_enrollments': learner_program_enrollments,
|
||||
'org_keys': org_key,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user