MST-682 Add external_user_key to the student profile CSV (#27091)

* MST-682 Add external_user_key to the student profile CSV
This is a request from some Masters school partners. They would like to download the student enrolled list with the Masters external_user_key data referenced. This way, the schools can properly match the students enrolled in the course with the students enrolled through Masters enrollment system
This commit is contained in:
Simon Chen
2021-03-22 14:25:34 -04:00
committed by GitHub
parent 777bb633c5
commit 4c5d56ef06
7 changed files with 157 additions and 4 deletions

View File

@@ -81,6 +81,7 @@ from lms.djangoapps.instructor_task.api_helper import (
QueueConnectionError,
generate_already_running_error_message
)
from lms.djangoapps.program_enrollments.tests.factories import ProgramEnrollmentFactory
from openedx.core.djangoapps.course_date_signals.handlers import extract_dates
from openedx.core.djangoapps.course_groups.cohorts import set_course_cohorted
from openedx.core.djangoapps.django_comment_common.models import FORUM_ROLE_COMMUNITY_TA
@@ -2616,6 +2617,32 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment
assert ('team' in res_json['feature_names']) == has_teams
@ddt.data(True, False)
def test_get_students_features_external_user_key(self, has_program_enrollments):
external_key_dict = {}
if has_program_enrollments:
for i in range(len(self.students)):
student = self.students[i]
external_key = "{}_{}".format(student.username, i)
ProgramEnrollmentFactory.create(user=student, external_user_key=external_key)
external_key_dict[student.username] = external_key
url = reverse('get_students_features', kwargs={'course_id': str(self.course.id)})
response = self.client.post(url, {})
res_json = json.loads(response.content.decode('utf-8'))
assert 'external_user_key' in res_json['feature_names']
for student in self.students:
student_json = [
x for x in res_json['students']
if x['username'] == student.username
][0]
assert student_json['username'] == student.username
if has_program_enrollments:
assert student_json['external_user_key'] == external_key_dict[student.username]
else:
assert student_json['external_user_key'] == ''
def test_get_students_who_may_enroll(self):
"""
Test whether get_students_who_may_enroll returns an appropriate

View File

@@ -1141,7 +1141,7 @@ def get_students_features(request, course_id, csv=False): # pylint: disable=red
'id', 'username', 'name', 'email', 'language', 'location',
'year_of_birth', 'gender', 'level_of_education', 'mailing_address',
'goals', 'enrollment_mode', 'verification_status',
'last_login', 'date_joined',
'last_login', 'date_joined', 'external_user_key'
]
# Provide human-friendly and translatable names for these features. These names
@@ -1163,6 +1163,7 @@ def get_students_features(request, course_id, csv=False): # pylint: disable=red
'verification_status': _('Verification Status'),
'last_login': _('Last Login'),
'date_joined': _('Date Joined'),
'external_user_key': _('External User Key'),
}
if is_course_cohorted(course.id):

View File

@@ -23,6 +23,7 @@ from common.djangoapps.student.models import CourseEnrollment, CourseEnrollmentA
from lms.djangoapps.certificates.models import CertificateStatuses, GeneratedCertificate
from lms.djangoapps.courseware.models import StudentModule
from lms.djangoapps.grades.api import context as grades_context
from lms.djangoapps.program_enrollments.api import fetch_program_enrollments_by_students
from lms.djangoapps.verify_student.services import IDVerificationService
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangolib.markup import HTML, Text
@@ -35,6 +36,7 @@ STUDENT_FEATURES = ('id', 'username', 'first_name', 'last_name', 'is_staff', 'em
PROFILE_FEATURES = ('name', 'language', 'location', 'year_of_birth', 'gender',
'level_of_education', 'mailing_address', 'goals', 'meta',
'city', 'country')
PROGRAM_ENROLLMENT_FEATURES = ('external_user_key', )
ORDER_ITEM_FEATURES = ('list_price', 'unit_cost', 'status')
ORDER_FEATURES = ('purchase_time',)
@@ -46,7 +48,7 @@ SALE_ORDER_FEATURES = ('id', 'company_name', 'company_contact_name', 'company_co
'bill_to_street2', 'bill_to_city', 'bill_to_state', 'bill_to_postalcode',
'bill_to_country', 'order_type', 'created')
AVAILABLE_FEATURES = STUDENT_FEATURES + PROFILE_FEATURES
AVAILABLE_FEATURES = STUDENT_FEATURES + PROFILE_FEATURES + PROGRAM_ENROLLMENT_FEATURES
COURSE_REGISTRATION_FEATURES = ('code', 'course_id', 'created_by', 'created_at', 'is_valid')
COUPON_FEATURES = ('code', 'course_id', 'percentage_discount', 'description', 'expiration_date', 'is_active')
CERTIFICATE_FEATURES = ('course_id', 'mode', 'status', 'grade', 'created_date', 'is_active', 'error_reason')
@@ -96,6 +98,8 @@ def enrolled_students_features(course_key, features):
include_team_column = 'team' in features
include_enrollment_mode = 'enrollment_mode' in features
include_verification_status = 'verification_status' in features
include_program_enrollments = 'external_user_key' in features
external_user_key_dict = {}
students = User.objects.filter(
courseenrollment__course_id=course_key,
@@ -108,6 +112,11 @@ def enrolled_students_features(course_key, features):
if include_team_column:
students = students.prefetch_related('teams')
if include_program_enrollments and len(students) > 0:
program_enrollments = fetch_program_enrollments_by_students(users=students, realized_only=True)
for program_enrollment in program_enrollments:
external_user_key_dict[program_enrollment.user_id] = program_enrollment.external_user_key
def extract_attr(student, feature):
"""Evaluate a student attribute that is ready for JSON serialization"""
attr = getattr(student, feature)
@@ -167,6 +176,10 @@ def enrolled_students_features(course_key, features):
if include_enrollment_mode:
student_dict['enrollment_mode'] = enrollment_mode
if include_program_enrollments:
# extra external_user_key
student_dict['external_user_key'] = external_user_key_dict.get(student.id, '')
return student_dict
return [extract_student(student, features) for student in students]

View File

@@ -16,6 +16,7 @@ from lms.djangoapps.courseware.tests.factories import InstructorFactory
from lms.djangoapps.instructor_analytics.basic import ( # lint-amnesty, pylint: disable=unused-import
AVAILABLE_FEATURES,
PROFILE_FEATURES,
PROGRAM_ENROLLMENT_FEATURES,
STUDENT_FEATURES,
StudentModule,
enrolled_students_features,
@@ -24,6 +25,7 @@ from lms.djangoapps.instructor_analytics.basic import ( # lint-amnesty, pylint:
list_may_enroll,
list_problem_responses
)
from lms.djangoapps.program_enrollments.tests.factories import ProgramEnrollmentFactory
from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory
from common.djangoapps.student.models import CourseEnrollment, CourseEnrollmentAllowed
from common.djangoapps.student.tests.factories import UserFactory
@@ -224,9 +226,31 @@ class TestAnalyticsBasic(ModuleStoreTestCase):
else:
assert report['cohort'] == '[unassigned]'
def test_enrolled_student_features_external_user_keys(self):
query_features = ('username', 'name', 'email', 'city', 'country', 'external_user_key')
username_with_external_user_key_dict = {}
for i in range(len(self.users)):
# Setup some users with ProgramEnrollments
if i % 2 == 0:
user = self.users[i]
external_user_key = '{}_{}'.format(user.username, i)
ProgramEnrollmentFactory.create(user=user, external_user_key=external_user_key)
username_with_external_user_key_dict[user.username] = external_user_key
with self.assertNumQueries(2):
userreports = enrolled_students_features(self.course_key, query_features)
assert len(userreports) == 30
for report in userreports:
username = report['username']
external_key = username_with_external_user_key_dict.get(username)
if external_key:
assert external_key == report['external_user_key']
else:
assert '' == report['external_user_key']
def test_available_features(self):
assert len(AVAILABLE_FEATURES) == len((STUDENT_FEATURES + PROFILE_FEATURES))
assert set(AVAILABLE_FEATURES) == set((STUDENT_FEATURES + PROFILE_FEATURES))
assert len(AVAILABLE_FEATURES) == len((STUDENT_FEATURES + PROFILE_FEATURES + PROGRAM_ENROLLMENT_FEATURES))
assert set(AVAILABLE_FEATURES) == set((STUDENT_FEATURES + PROFILE_FEATURES + PROGRAM_ENROLLMENT_FEATURES))
def test_list_may_enroll(self):
may_enroll = list_may_enroll(self.course_key, ['email'])

View File

@@ -21,6 +21,7 @@ from .reading import (
fetch_program_course_enrollments_by_students,
fetch_program_enrollments,
fetch_program_enrollments_by_student,
fetch_program_enrollments_by_students,
get_external_key_by_user_and_course,
get_org_key_for_program,
get_program_course_enrollment,

View File

@@ -271,6 +271,47 @@ def fetch_program_enrollments_by_student(
return ProgramEnrollment.objects.filter(**_remove_none_values(filters))
def fetch_program_enrollments_by_students(
users=None,
external_user_keys=None,
program_enrollment_statuses=None,
realized_only=False,
waiting_only=False,
):
"""
Fetch program enrollments for a specific list of students.
Required arguments (at least one must be provided):
* users (iterable[User])
* external_user_keys (iterable[str])
Optional arguments:
* program_enrollment_statuses (iterable[str])
* realized_only (bool)
* waiting_only (bool)
Optional arguments are used as filtersets if they are not None.
Returns: queryset[ProgramEnrollment]
"""
if not (users or external_user_keys):
raise ValueError(_STUDENT_LIST_ARG_ERROR_MESSAGE)
if realized_only and waiting_only:
raise ValueError(
_REALIZED_FILTER_ERROR_TEMPLATE.format("realized_only", "waiting_only")
)
filters = {
"user__in": users,
"external_user_key__in": external_user_keys,
"status__in": program_enrollment_statuses,
}
if realized_only:
filters["user__isnull"] = False
if waiting_only:
filters["user__isnull"] = True
return ProgramEnrollment.objects.filter(**_remove_none_values(filters))
def fetch_program_course_enrollments_by_students(
users=None,
external_user_keys=None,

View File

@@ -42,6 +42,7 @@ from ..reading import (
fetch_program_course_enrollments_by_students,
fetch_program_enrollments,
fetch_program_enrollments_by_student,
fetch_program_enrollments_by_students,
get_external_key_by_user_and_course,
get_program_course_enrollment,
get_program_enrollment,
@@ -371,6 +372,51 @@ class ProgramEnrollmentReadingTests(TestCase):
actual_enrollment_ids = {enrollment.id for enrollment in actual_enrollments}
assert actual_enrollment_ids == expected_enrollment_ids
@ddt.data(
# User with no enrollments
(
{'usernames': [username_0]},
set(),
),
# Filters
(
{
'usernames': [username_3],
},
{3, 7},
),
# More filters
(
{
'usernames': [username_3],
'external_user_keys': [ext_3],
'program_enrollment_statuses': {PEStatuses.SUSPENDED, PEStatuses.CANCELED},
},
{7},
),
# Realized-only filter
(
{'usernames': [username_4], 'realized_only': True},
{4},
),
# Waiting-only filter
(
{'external_user_keys': [ext_4], 'waiting_only': True},
{8},
),
)
@ddt.unpack
def test_fetch_program_enrollments_by_students(self, kwargs, expected_enrollment_ids):
kwargs = self._usernames_to_users(kwargs)
actual_enrollments = fetch_program_enrollments_by_students(**kwargs)
actual_enrollment_ids = {enrollment.id for enrollment in actual_enrollments}
assert actual_enrollment_ids == expected_enrollment_ids
@ddt.data(
# User with no program enrollments