Revert "MST-157 Create a new program enrollments inspector support tool to help edX support members to inspect the existing states of program enrollees"

This reverts commit 9bdcb9c5f5.
This commit is contained in:
David Ormsbee
2020-03-12 17:28:58 -04:00
committed by GitHub
parent 9bdcb9c5f5
commit 37f3c3f829
7 changed files with 5 additions and 503 deletions

View File

@@ -1,57 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button, InputText, StatusAlert, InputSelect } from '@edx/paragon';
export const ProgramEnrollmentsInspectorPage = props => (
<form method="get">
{props.successes.length > 0 && (
<StatusAlert
open
alertType="success"
dialog={(
<div>
<span></span>
</div>
)}
/>
)}
<div key="edX_accounts">
<InputText
name="edx_user"
label="edX account username or email"
value={(props.learnerInfo && props.learnerInfo.user && props.learnerInfo.user.username) || ''}
/>
</div>
<div key="school_accounts">
<InputSelect
name="IdPSelect"
label="Learner Account Providers"
value="Select One"
options={
props.orgKeys
}
/>
<InputText
name="external_user_key"
label="Institution user key from school. For example, GTPersonDirectoryId for GT students"
value={(props.learnerInfo && props.learnerInfo.user && props.learnerInfo.user.external_user_key) || ''}
/>
</div>
<Button label="Search" type="submit" className={['btn', 'btn-primary']} />
</form>
);
ProgramEnrollmentsInspectorPage.propTypes = {
successes: PropTypes.arrayOf(PropTypes.string),
errors: PropTypes.arrayOf(PropTypes.string),
learnerInfo: PropTypes.string,
orgKeys: PropTypes.arrayOf(PropTypes.object),
};
ProgramEnrollmentsInspectorPage.defaultProps = {
successes: [],
errors: [],
learnerInfo: '',
orgKeys: [],
};

View File

@@ -8,7 +8,7 @@ import itertools
import json
import re
from datetime import datetime, timedelta
from uuid import UUID, uuid4
from uuid import uuid4, UUID
import ddt
import six
@@ -17,19 +17,15 @@ from django.db.models import signals
from django.http import HttpResponse
from django.urls import reverse
from mock import patch
from organizations.tests.factories import OrganizationFactory
from pytz import UTC
from social_django.models import UserSocialAuth
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.verify_student.models import VerificationDeadline
from student.models import ENROLLED_TO_ENROLLED, CourseEnrollment, CourseEnrollmentAttribute, ManualEnrollmentAudit
from student.roles import GlobalStaff, SupportStaffRole
from student.tests.factories import CourseEnrollmentFactory, UserFactory
from third_party_auth.tests.factories import SAMLProviderConfigFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
@@ -543,238 +539,3 @@ class SupportViewLinkProgramEnrollmentsTests(SupportViewTestCase):
msg = u"All linking lines must be in the format 'external_user_key,lms_username'"
render_call_dict = mocked_render.call_args[0][1]
assert render_call_dict['errors'] == [msg]
class ProgramEnrollmentsInspectorViewTests(SupportViewTestCase):
"""
View tests for Program Enrollments Inspector
"""
patch_render = patch(
'support.views.program_enrollments.render_to_response',
return_value=HttpResponse(),
autospec=True,
)
def setUp(self):
super(ProgramEnrollmentsInspectorViewTests, self).setUp()
self.url = reverse("support:program_enrollments_inspector")
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_initial_rendering(self):
response = self.client.get(self.url)
content = six.text_type(response.content, encoding='utf-8')
expected_organization_serialized = '"orgKeys": {}'.format(json.dumps(self.org_key_list))
assert response.status_code == 200
assert expected_organization_serialized in content
assert '"learnerInfo": {}' in content
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='{0}:{1}'.format(org_key, external_user_key),
provider=org_key
)
user_info['external_user_key'] = external_user_key
user_info['SSO'] = {
'uid': user_social_auth.uid,
'provider': user_social_auth.provider
}
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
"""
expected_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:
course_enrollment = CourseEnrollmentFactory.create(
course_id=course_id,
user=edx_user,
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,
course_key=course_id,
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
)
expected_enrollments.append(expected_enrollment)
return expected_enrollments
@patch_render
def test_search_username_well_connected_user(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
)
self.client.get(self.url, data={
'edx_user': created_user.username
})
expected_info = {
'user': expected_user_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_username_user_not_connected(self, mocked_render):
created_user, expected_user_info = self._construct_user('user_not_connected')
self.client.get(self.url, data={
'edx_user': created_user.email
})
expected_info = {
'user': expected_user_info
}
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_no_enrollment(self, mocked_render):
created_user, expected_user_info = self._construct_user(
'user_connected',
self.org_key_list[0],
self.external_user_key
)
self.client.get(self.url, data={
'edx_user': created_user.email
})
expected_info = {
'user': expected_user_info
}
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_no_course_enrollment(self, mocked_render):
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,
)
self.client.get(self.url, data={
'edx_user': created_user.email
})
expected_info = {
'user': expected_user_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_username_user_not_connected_with_enrollments(self, mocked_render):
created_user, expected_user_info = self._construct_user(
'user_not_connected'
)
self._construct_enrollments(
[self.program_uuid],
[],
self.external_user_key,
)
self.client.get(self.url, data={
'edx_user': created_user.email
})
expected_info = {
'user': expected_user_info,
}
render_call_dict = mocked_render.call_args[0][1]
assert expected_info == render_call_dict['learner_program_enrollments']

View File

@@ -12,8 +12,8 @@ from support.views.enrollments import EnrollmentSupportListView, EnrollmentSuppo
from support.views.feature_based_enrollments import FeatureBasedEnrollmentsSupportView
from support.views.index import index
from support.views.manage_user import ManageUserDetailView, ManageUserSupportView
from support.views.program_enrollments import LinkProgramEnrollmentSupportView, ProgramEnrollmentsInspectorView
from support.views.refund import RefundSupportView
from support.views.program_enrollments import LinkProgramEnrollmentSupportView
COURSE_ENTITLEMENTS_VIEW = EntitlementSupportView.as_view()
@@ -41,10 +41,5 @@ urlpatterns = [
FeatureBasedEnrollmentsSupportView.as_view(),
name="feature_based_enrollments"
),
url(r'link_program_enrollments/?$', LinkProgramEnrollmentSupportView.as_view(), name='link_program_enrollments'),
url(
r'program_enrollments_inspector/?$',
ProgramEnrollmentsInspectorView.as_view(),
name='program_enrollments_inspector'
)
url(r'link_program_enrollments/?$', LinkProgramEnrollmentSupportView.as_view(), name='link_program_enrollments')
]

View File

@@ -3,9 +3,9 @@ Certificate tool in the student support app.
"""
from six.moves.urllib.parse import unquote, quote_plus # pylint: disable=import-error
from django.utils.decorators import method_decorator
from django.views.generic import View
from six.moves.urllib.parse import quote_plus, unquote
from edxmako.shortcuts import render_to_response
from support.decorators import require_support_permission

View File

@@ -6,22 +6,14 @@ Support tool for changing course enrollments.
import csv
from uuid import UUID
from django.contrib.auth.models import User
from django.db.models import Q
from django.utils.decorators import method_decorator
from django.views.generic import View
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,
link_program_enrollments
)
from lms.djangoapps.program_enrollments.api import link_program_enrollments
from lms.djangoapps.support.decorators import require_support_permission
from third_party_auth.models import SAMLProviderConfig
TEMPLATE_PATH = 'support/link_program_enrollments.html'
DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%S'
class LinkProgramEnrollmentSupportView(View):
@@ -106,153 +98,3 @@ class LinkProgramEnrollmentSupportView(View):
]
errors = [message for message in link_errors.values()]
return successes, errors
class ProgramEnrollmentsInspectorView(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.
"""
errors = []
edx_username_or_email = request.GET.get('edx_user', '').strip()
org_key = request.GET.get('IdPSelect', '').strip()
external_user_key = request.GET.get('external_user_key', '').strip()
learner_program_enrollments = {}
if edx_username_or_email:
learner_program_enrollments, error = self._get_account_info(edx_username_or_email)
if error:
errors.append(error)
elif org_key and external_user_key:
learner_program_enrollments = {}
elif not external_user_key and org_key:
errors.append(
'You must provide either the edX username or email, or the '
'Learner Account Provider and External Key pair to do search!'
)
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(),
}
)
def _get_org_keys_with_idp(self):
"""
From our Third_party_auth models, return a list
of organizations whose SAMLProviders are active and configured
"""
saml_providers = SAMLProviderConfig.objects.current_set().filter(
enabled=True,
organization__isnull=False
).select_related('organization')
return [saml_provider.organization.short_name for saml_provider in saml_providers]
def _get_account_info(self, username_or_email):
"""
Provided the edx account username or email, return edx account info
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
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
enrollments = self._get_enrollments(user=user)
result = {'user': user_info}
if enrollments:
result['enrollments'] = enrollments
return result, ''
except User.DoesNotExist:
return {}, 'Could not find edx account with {}'.format(username_or_email)
def _get_enrollments(self, user=None, external_user_key=None):
"""
With the user or external_user_key passed in,
return an array of dictionariers with corresponding ProgramEnrollments
and ProgramCourseEnrollments all serialized for view
"""
program_enrollments = fetch_program_enrollments_by_student(
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),
}

View File

@@ -1,37 +0,0 @@
<%page expression_filter="h"/>
<%!
from django.utils.translation import ugettext as _
from openedx.core.djangolib.js_utils import js_escaped_string
%>
## Override the default styles_version to use Bootstrap
<%! main_css = "css/bootstrap/lms-main.css" %>
<%namespace name='static' file='../static_content.html'/>
<%inherit file="../main.html" />
<%block name="js_extra">
</%block>
<%block name="pagetitle">
${_("Program Enrollments Inspector")}
</%block>
<%block name="content">
<section class="container outside-app">
<h3> Program Enrollments Inspector </h3>
${static.renderReact(
component="ProgramEnrollmentsInspectorPage",
id="entitlement-support-page",
props={
'successes': successes,
'errors': errors,
'learnerInfo': learner_program_enrollments,
'orgKeys': org_keys
}
)
}
</section>
</%block>

View File

@@ -86,8 +86,6 @@ module.exports = Merge.smart({
EntitlementSupportPage: './lms/djangoapps/support/static/support/jsx/entitlements/index.jsx',
LinkProgramEnrollmentsSupportPage: './lms/djangoapps/support/static/support/jsx/' +
'program_enrollments/index.jsx',
ProgramEnrollmentsInspectorPage: './lms/djangoapps/support/static/support/jsx/' +
'program_enrollments/inspector.jsx',
PasswordResetConfirmation: './lms/static/js/student_account/components/PasswordResetConfirmation.jsx',
StudentAccountDeletion: './lms/static/js/student_account/components/StudentAccountDeletion.jsx',
StudentAccountDeletionInitializer: './lms/static/js/student_account/StudentAccountDeletionInitializer.js',