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:
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user