MST-157 PART-2 Complete the functionality of searching for user info on program enrollment. The search result will be displayed on support tool's frontend.

This commit is contained in:
Simon Chen
2020-03-01 15:18:29 -05:00
parent c8d78eb3db
commit 0b90de1d94
7 changed files with 341 additions and 150 deletions

View File

@@ -26,8 +26,9 @@ from .reading import (
get_program_enrollment,
get_provider_slug,
get_saml_provider_for_organization,
get_saml_provider_for_program,
get_users_by_external_keys
get_org_key_for_program,
get_users_by_external_keys,
get_users_by_external_keys_and_org_key
)
from .writing import (
change_program_course_enrollment_status,

View File

@@ -342,6 +342,44 @@ def _remove_none_values(dictionary):
}
def get_users_by_external_keys_and_org_key(external_user_keys, org_key):
"""
Given an organization_key and a set of external keys,
return a dict from external user keys to Users.
Args:
external_user_keys (sequence[str]):
external user keys used by the program creator's IdP.
org_key (str):
The organization short name of which the external_user_key belongs to
Returns: dict[str: User|None]
A dict mapping external user keys to Users.
If an external user key is not registered, then None is returned instead
of a User for that key.
Raises:
BadOrganizationShortNameException
ProviderDoesNotExistsException
ProviderConfigurationException
"""
saml_provider = get_saml_provider_by_org_key(org_key)
social_auth_uids = {
saml_provider.get_social_auth_uid(external_user_key)
for external_user_key in external_user_keys
}
social_auths = UserSocialAuth.objects.filter(uid__in=social_auth_uids)
found_users_by_external_keys = {
saml_provider.get_remote_id_from_social_auth(social_auth): social_auth.user
for social_auth in social_auths
}
# Default all external keys to None, because external keys
# without a User will not appear in `found_users_by_external_keys`.
users_by_external_keys = {key: None for key in external_user_keys}
users_by_external_keys.update(found_users_by_external_keys)
return users_by_external_keys
def get_users_by_external_keys(program_uuid, external_user_keys):
"""
Given a program and a set of external keys,
@@ -365,21 +403,8 @@ def get_users_by_external_keys(program_uuid, external_user_keys):
ProviderDoesNotExistsException
ProviderConfigurationException
"""
saml_provider = get_saml_provider_for_program(program_uuid)
social_auth_uids = {
saml_provider.get_social_auth_uid(external_user_key)
for external_user_key in external_user_keys
}
social_auths = UserSocialAuth.objects.filter(uid__in=social_auth_uids)
found_users_by_external_keys = {
saml_provider.get_remote_id_from_social_auth(social_auth): social_auth.user
for social_auth in social_auths
}
# Default all external keys to None, because external keys
# without a User will not appear in `found_users_by_external_keys`.
users_by_external_keys = {key: None for key in external_user_keys}
users_by_external_keys.update(found_users_by_external_keys)
return users_by_external_keys
org_key = get_org_key_for_program(program_uuid)
return get_users_by_external_keys_and_org_key(external_user_keys, org_key)
def get_external_key_by_user_and_course(user, course_key):
@@ -409,20 +434,38 @@ def get_external_key_by_user_and_course(user, course_key):
return relevant_pce.program_enrollment.external_user_key
def get_saml_provider_for_program(program_uuid):
def get_saml_provider_by_org_key(org_key):
"""
Return currently configured SAML provider for the Organization
Returns the SAML provider associated with the provided org_key
Arguments:
org_key (str)
Returns: SAMLProvider
Raises:
BadOrganizationShortNameException
"""
try:
organization = Organization.objects.get(short_name=org_key)
except Organization.DoesNotExist:
raise BadOrganizationShortNameException(org_key)
return get_saml_provider_for_organization(organization)
def get_org_key_for_program(program_uuid):
"""
Return the key of the first Organization
administering the given program.
Arguments:
program_uuid (UUID|str)
Returns: SAMLProvider
Returns: org_key (str)
Raises:
ProgramDoesNotExistException
ProgramHasNoAuthoringOrganizationException
BadOrganizationShortNameException
"""
program = get_programs(uuid=program_uuid)
if program is None:
@@ -431,11 +474,7 @@ def get_saml_provider_for_program(program_uuid):
org_key = authoring_orgs[0].get('key') if authoring_orgs else None
if not org_key:
raise ProgramHasNoAuthoringOrganizationException(program_uuid)
try:
organization = Organization.objects.get(short_name=org_key)
except Organization.DoesNotExist:
raise BadOrganizationShortNameException(org_key)
return get_saml_provider_for_organization(organization)
return org_key
def get_saml_provider_for_organization(organization):

View File

@@ -5,7 +5,14 @@ Serializers for use in the support app.
from rest_framework import serializers
from student.models import ManualEnrollmentAudit
from student.models import CourseEnrollment, ManualEnrollmentAudit
from lms.djangoapps.program_enrollments.models import (
ProgramEnrollment,
ProgramCourseEnrollment,
)
DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%S'
# pylint: disable=abstract-method
class ManualEnrollmentSerializer(serializers.ModelSerializer):
@@ -15,3 +22,57 @@ class ManualEnrollmentSerializer(serializers.ModelSerializer):
class Meta(object):
model = ManualEnrollmentAudit
fields = ('enrolled_by', 'time_stamp', 'reason')
class CourseEnrollmentSerializer(serializers.Serializer):
""" Serializers a student_courseenrollment model object """
course_id = serializers.CharField()
is_active = serializers.BooleanField()
mode = serializers.CharField()
class Meta(object):
model = CourseEnrollment
class ProgramCourseEnrollmentSerializer(serializers.Serializer):
""" Serializes a Program Course Enrollment model object """
created = serializers.DateTimeField(format=DATETIME_FORMAT)
modified = serializers.DateTimeField(format=DATETIME_FORMAT)
status = serializers.CharField()
course_key = serializers.CharField()
course_enrollment = CourseEnrollmentSerializer()
class Meta(object):
model = ProgramCourseEnrollment
class ProgramEnrollmentSerializer(serializers.Serializer):
""" Serializes a Program Enrollment Model object """
created = serializers.DateTimeField(format=DATETIME_FORMAT)
modified = serializers.DateTimeField(format=DATETIME_FORMAT)
external_user_key = serializers.CharField()
status = serializers.CharField()
program_uuid = serializers.UUIDField()
program_course_enrollments = ProgramCourseEnrollmentSerializer(many=True)
class Meta(object):
model = ProgramEnrollment
def serialize_user_info(user, user_social_auth=None):
"""
Helper method to serialize resulting in user_info_object
based on passed in django models
"""
user_info = {
'username': user.username,
'email': user.email,
}
if user_social_auth:
_, external_key = user_social_auth.uid.split(':', 1)
user_info['external_user_key'] = external_key
user_info['sso'] = {
'uid': user_social_auth.uid,
'provider': user_social_auth.provider
}
return user_info

View File

@@ -4,17 +4,14 @@ import { Button, InputText, StatusAlert, InputSelect } from '@edx/paragon';
export const ProgramEnrollmentsInspectorPage = props => (
<form method="get">
{props.successes.length > 0 && (
{props.errors.map(errorItem => (
<StatusAlert
open
alertType="success"
dialog={(
<div>
<span></span>
</div>
)}
dismissible={false}
alertType="danger"
dialog={errorItem}
/>
)}
))}
<div key="edX_accounts">
<InputText
name="edx_user"
@@ -43,15 +40,21 @@ export const ProgramEnrollmentsInspectorPage = props => (
);
ProgramEnrollmentsInspectorPage.propTypes = {
successes: PropTypes.arrayOf(PropTypes.string),
errors: PropTypes.arrayOf(PropTypes.string),
learnerInfo: PropTypes.string,
learnerInfo: PropTypes.shape({
user: PropTypes.shape({
external_user_key: PropTypes.string,
username: PropTypes.string,
}),
enrollments: PropTypes.arrayOf(
PropTypes.string,
),
}),
orgKeys: PropTypes.arrayOf(PropTypes.object),
};
ProgramEnrollmentsInspectorPage.defaultProps = {
successes: [],
errors: [],
learnerInfo: '',
learnerInfo: {},
orgKeys: [],
};

View File

@@ -25,7 +25,10 @@ from common.test.utils import disable_signal
from course_modes.models import CourseMode
from course_modes.tests.factories import CourseModeFactory
from lms.djangoapps.program_enrollments.tests.factories import ProgramCourseEnrollmentFactory, ProgramEnrollmentFactory
from lms.djangoapps.support.serializers import ProgramEnrollmentSerializer
from lms.djangoapps.verify_student.models import VerificationDeadline
from lms.djangoapps.verify_student.services import IDVerificationService
from lms.djangoapps.verify_student.tests.factories import SSOVerificationFactory
from student.models import ENROLLED_TO_ENROLLED, CourseEnrollment, CourseEnrollmentAttribute, ManualEnrollmentAudit
from student.roles import GlobalStaff, SupportStaffRole
from student.tests.factories import CourseEnrollmentFactory, UserFactory
@@ -545,6 +548,7 @@ class SupportViewLinkProgramEnrollmentsTests(SupportViewTestCase):
assert render_call_dict['errors'] == [msg]
@ddt.ddt
class ProgramEnrollmentsInspectorViewTests(SupportViewTestCase):
"""
View tests for Program Enrollments Inspector
@@ -603,10 +607,10 @@ class ProgramEnrollmentsInspectorViewTests(SupportViewTestCase):
user_social_auth = UserSocialAuth.objects.create(
user=user,
uid='{0}:{1}'.format(org_key, external_user_key),
provider=org_key
provider='tpa-saml'
)
user_info['external_user_key'] = external_user_key
user_info['SSO'] = {
user_info['sso'] = {
'uid': user_social_auth.uid,
'provider': user_social_auth.provider
}
@@ -618,27 +622,14 @@ class ProgramEnrollmentsInspectorViewTests(SupportViewTestCase):
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
"""
expected_enrollments = []
program_enrollments = []
for program_uuid in program_uuids:
expected_enrollment = {}
expected_course_enrollment = {}
course_enrollment = None
program_enrollment = ProgramEnrollmentFactory.create(
external_user_key=external_user_key,
program_uuid=program_uuid,
user=edx_user
)
expected_enrollment['program_enrollment'] = {
'created': self._serialize_datetime(
program_enrollment.created
),
'modified': self._serialize_datetime(
program_enrollment.modified
),
'program_uuid': program_enrollment.program_uuid,
'external_user_key': external_user_key,
'status': program_enrollment.status
}
for course_id in course_ids:
if edx_user:
@@ -648,11 +639,6 @@ class ProgramEnrollmentsInspectorViewTests(SupportViewTestCase):
mode=CourseMode.MASTERS,
is_active=True
)
expected_course_enrollment = {
'course_id': str(course_enrollment.course_id),
'is_active': course_enrollment.is_active,
'mode': course_enrollment.mode,
}
program_course_enrollment = ProgramCourseEnrollmentFactory.create(
program_enrollment=program_enrollment,
@@ -660,26 +646,22 @@ class ProgramEnrollmentsInspectorViewTests(SupportViewTestCase):
course_enrollment=course_enrollment,
status='active',
)
expected_program_course_enrollment = {
'created': self._serialize_datetime(
program_course_enrollment.created
),
'modified': self._serialize_datetime(
program_course_enrollment.modified
),
'status': program_course_enrollment.status,
'course_key': str(program_course_enrollment.course_key),
}
if expected_course_enrollment:
expected_program_course_enrollment['course_enrollment'] = expected_course_enrollment
expected_enrollment.setdefault('program_course_enrollments', []).append(
expected_program_course_enrollment
)
program_enrollments.append(program_enrollment)
expected_enrollments.append(expected_enrollment)
serialized = ProgramEnrollmentSerializer(program_enrollments, many=True)
return serialized.data
return expected_enrollments
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)
@patch_render
def test_search_username_well_connected_user(self, mocked_render):
@@ -688,6 +670,7 @@ class ProgramEnrollmentsInspectorViewTests(SupportViewTestCase):
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],
@@ -699,7 +682,8 @@ class ProgramEnrollmentsInspectorViewTests(SupportViewTestCase):
})
expected_info = {
'user': expected_user_info,
'enrollments': expected_enrollments
'enrollments': expected_enrollments,
'id_verification': id_verified
}
render_call_dict = mocked_render.call_args[0][1]
@@ -712,7 +696,8 @@ class ProgramEnrollmentsInspectorViewTests(SupportViewTestCase):
'edx_user': created_user.email
})
expected_info = {
'user': expected_user_info
'user': expected_user_info,
'id_verification': IDVerificationService.user_status(created_user)
}
render_call_dict = mocked_render.call_args[0][1]
@@ -729,7 +714,8 @@ class ProgramEnrollmentsInspectorViewTests(SupportViewTestCase):
'edx_user': created_user.email
})
expected_info = {
'user': expected_user_info
'user': expected_user_info,
'id_verification': IDVerificationService.user_status(created_user),
}
render_call_dict = mocked_render.call_args[0][1]
@@ -753,7 +739,8 @@ class ProgramEnrollmentsInspectorViewTests(SupportViewTestCase):
})
expected_info = {
'user': expected_user_info,
'enrollments': expected_enrollments
'enrollments': expected_enrollments,
'id_verification': IDVerificationService.user_status(created_user),
}
render_call_dict = mocked_render.call_args[0][1]
@@ -774,7 +761,110 @@ class ProgramEnrollmentsInspectorViewTests(SupportViewTestCase):
})
expected_info = {
'user': expected_user_info,
'id_verification': IDVerificationService.user_status(created_user),
}
render_call_dict = mocked_render.call_args[0][1]
assert expected_info == render_call_dict['learner_program_enrollments']
@patch_render
def test_search_username_user_id_verified(self, mocked_render):
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
}
self.client.get(self.url, data={
'edx_user': created_user.email
})
render_call_dict = mocked_render.call_args[0][1]
assert expected_info == render_call_dict['learner_program_enrollments']
@patch_render
def test_search_external_key_well_connected(self, mocked_render):
created_user, expected_user_info = self._construct_user(
'test_user_connected',
self.org_key_list[0],
self.external_user_key
)
expected_enrollments = self._construct_enrollments(
[self.program_uuid],
[self.course.id],
self.external_user_key,
created_user
)
id_verified = self._construct_id_verification(created_user)
self.client.get(self.url, data={
'external_user_key': self.external_user_key,
'IdPSelect': self.org_key_list[0]
})
expected_info = {
'user': expected_user_info,
'enrollments': expected_enrollments,
'id_verification': id_verified,
}
render_call_dict = mocked_render.call_args[0][1]
assert expected_info == render_call_dict['learner_program_enrollments']
@ddt.data(
('aabbcc', ''),
('', 'test_org')
)
@ddt.unpack
@patch_render
def test_search_external_key_no_idp(self, user_key_input, idp_input, mocked_render):
self.client.get(self.url, data={
'external_user_key': user_key_input,
'IdPSelect': idp_input,
})
expected_errors = [
"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!"
]
render_call_dict = mocked_render.call_args[0][1]
assert {} == render_call_dict['learner_program_enrollments']
assert expected_errors == render_call_dict['errors']
@patch_render
def test_search_external_user_not_connected(self, mocked_render):
expected_enrollments = self._construct_enrollments(
[self.program_uuid],
[self.course.id],
self.external_user_key,
)
self.client.get(self.url, data={
'external_user_key': self.external_user_key,
'IdPSelect': self.org_key_list[0]
})
expected_info = {
'enrollments': expected_enrollments
}
render_call_dict = mocked_render.call_args[0][1]
assert expected_info == render_call_dict['learner_program_enrollments']
@patch_render
def test_search_external_user_not_in_system(self, mocked_render):
external_user_key = 'not_in_system'
self.client.get(self.url, data={
'external_user_key': external_user_key,
'IdPSelect': self.org_key_list[0],
})
expected_errors = [
'No user found for external key {} for institution {}'.format(
external_user_key, self.org_key_list[0]
)
]
render_call_dict = mocked_render.call_args[0][1]
assert expected_errors == render_call_dict['errors']

View File

@@ -15,9 +15,20 @@ from social_django.models import UserSocialAuth
from edxmako.shortcuts import render_to_response
from lms.djangoapps.program_enrollments.api import (
fetch_program_enrollments_by_student,
get_users_by_external_keys_and_org_key,
link_program_enrollments
)
from lms.djangoapps.program_enrollments.exceptions import (
BadOrganizationShortNameException,
ProviderConfigurationException,
ProviderDoesNotExistException
)
from lms.djangoapps.support.decorators import require_support_permission
from lms.djangoapps.support.serializers import (
ProgramEnrollmentSerializer,
serialize_user_info
)
from lms.djangoapps.verify_student.services import IDVerificationService
from third_party_auth.models import SAMLProviderConfig
TEMPLATE_PATH = 'support/link_program_enrollments.html'
@@ -133,17 +144,27 @@ class ProgramEnrollmentsInspectorView(View):
if error:
errors.append(error)
elif org_key and external_user_key:
learner_program_enrollments = {}
elif not external_user_key and org_key:
learner_program_enrollments = self._get_external_user_info(
external_user_key,
org_key
)
if not learner_program_enrollments:
errors.append(
'No user found for external key {} for institution {}'.format(
external_user_key, org_key
)
)
else:
errors.append(
'You must provide either the edX username or email, or the '
'Learner Account Provider and External Key pair to do search!'
"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,
{
'successes': [],
'errors': errors,
'learner_program_enrollments': learner_program_enrollments,
'org_keys': self._get_org_keys_with_idp(),
@@ -168,32 +189,59 @@ class ProgramEnrollmentsInspectorView(View):
and program_enrollments_info. If we cannot identify the user, return
empty object and error.
"""
user_info = {}
external_key = None
try:
user = User.objects.get(Q(username=username_or_email) | Q(email=username_or_email))
user_info['username'] = user.username
user_info['email'] = user.email
user_social_auth = None
try:
user_social_auth = UserSocialAuth.objects.get(user=user)
_, external_key = user_social_auth.uid.split(':', 1)
user_info['external_user_key'] = external_key
user_info['SSO'] = {
'uid': user_social_auth.uid,
'provider': user_social_auth.provider
}
except UserSocialAuth.DoesNotExist:
pass
user_info = serialize_user_info(user, user_social_auth)
enrollments = self._get_enrollments(user=user)
result = {'user': user_info}
if enrollments:
result['enrollments'] = enrollments
result['id_verification'] = IDVerificationService.user_status(user)
return result, ''
except User.DoesNotExist:
return {}, 'Could not find edx account with {}'.format(username_or_email)
def _get_external_user_info(self, external_user_key, org_key):
"""
Provided the external_user_key and org_key, return edx account info
and program_enrollments_info if any. If we cannot identify the data,
return empty object.
"""
found_user = None
result = {}
try:
users_by_key = get_users_by_external_keys_and_org_key(
[external_user_key],
org_key
)
found_user = users_by_key.get(external_user_key)
except (
BadOrganizationShortNameException,
ProviderConfigurationException,
ProviderDoesNotExistException
):
# We cannot identify edX user from external_user_key and org_key pair
pass
if found_user:
try:
user_social_auth = UserSocialAuth.objects.get(user=found_user)
except UserSocialAuth.DoesNotExist:
user_social_auth = None
user_info = serialize_user_info(found_user, user_social_auth)
result['user'] = user_info
result['id_verification'] = IDVerificationService.user_status(found_user)
enrollments = self._get_enrollments(external_user_key=external_user_key)
if enrollments:
result['enrollments'] = enrollments
return result
def _get_enrollments(self, user=None, external_user_key=None):
"""
With the user or external_user_key passed in,
@@ -204,55 +252,5 @@ class ProgramEnrollmentsInspectorView(View):
user=user,
external_user_key=external_user_key
).prefetch_related('program_course_enrollments')
enrollments_by_program_uuid = {}
for program_enrollment in program_enrollments:
serialized_program_enrollment = self._serialize_program_enrollment(program_enrollment)
enrollment_item = {
'program_enrollment': serialized_program_enrollment
}
program_course_enrollments = program_enrollment.program_course_enrollments.all()
for program_course_enrollment in program_course_enrollments.select_related(
'course_enrollment'
):
serialized_program_course_enrollment = self._serialize_program_course_enrollment(
program_course_enrollment
)
enrollment_item.setdefault('program_course_enrollments', []).append(
serialized_program_course_enrollment
)
enrollments_by_program_uuid[program_enrollment.program_uuid] = enrollment_item
return list(enrollments_by_program_uuid.values())
def _serialize_program_enrollment(self, program_enrollment):
if not program_enrollment:
return {}
return {
'created': program_enrollment.created.strftime(DATETIME_FORMAT),
'modified': program_enrollment.modified.strftime(DATETIME_FORMAT),
'program_uuid': str(program_enrollment.program_uuid),
'external_user_key': program_enrollment.external_user_key,
'status': program_enrollment.status
}
def _serialize_program_course_enrollment(self, program_course_enrollment):
"""
Return a dictionary of ProgramCourseEnrollment serialized
"""
if not program_course_enrollment:
return {}
course_enrollment = program_course_enrollment.course_enrollment
return {
'created': program_course_enrollment.created.strftime(DATETIME_FORMAT),
'modified': program_course_enrollment.modified.strftime(DATETIME_FORMAT),
'course_enrollment': {
'course_id': str(course_enrollment.course_id),
'is_active': course_enrollment.is_active,
'mode': course_enrollment.mode,
},
'status': program_course_enrollment.status,
'course_key': str(program_course_enrollment.course_key),
}
serialized = ProgramEnrollmentSerializer(program_enrollments, many=True)
return serialized.data

View File

@@ -26,7 +26,6 @@ from openedx.core.djangolib.js_utils import js_escaped_string
component="ProgramEnrollmentsInspectorPage",
id="entitlement-support-page",
props={
'successes': successes,
'errors': errors,
'learnerInfo': learner_program_enrollments,
'orgKeys': org_keys