feat: Program Inspectors API View

This commit is contained in:
ansabgillani
2022-01-10 11:13:21 +05:00
committed by Ansab Gillani
parent 573c841bf6
commit 8610856a30
3 changed files with 441 additions and 56 deletions

View File

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

View File

@@ -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.@+-]+)?$',

View File

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