Skip to content

Commit 480a445

Browse files
efortishsandroscosta
authored andcommitted
feat: enrollment_date added to csv report and add custom fields method (#37264)
* chore: enrollment_date added to csv report and add custom fields method managing * test: tests added * fix: pylint fix * fix: new line at test_basic.py added * feat: new function added to handle available features with custom fields * chore: replace include_ parameters with direct feature checks * feat: type validation for custom attributes added * chore: site config name and variable updated, attribute fixing erased * test: tests updated
1 parent 05687c8 commit 480a445

File tree

3 files changed

+458
-214
lines changed

3 files changed

+458
-214
lines changed

lms/djangoapps/instructor/views/api.py

Lines changed: 96 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1473,7 +1473,7 @@ def post(self, request, course_id, csv=False): # pylint: disable=redefined-oute
14731473
course_key = CourseKey.from_string(course_id)
14741474
course = get_course_by_id(course_key)
14751475
report_type = _('enrolled learner profile')
1476-
available_features = instructor_analytics_basic.AVAILABLE_FEATURES
1476+
available_features = instructor_analytics_basic.get_available_features(course_key)
14771477

14781478
# Allow for sites to be able to define additional columns.
14791479
# Note that adding additional columns has the potential to break
@@ -1485,67 +1485,103 @@ def post(self, request, course_id, csv=False): # pylint: disable=redefined-oute
14851485
# We need to clone the list because we modify it below
14861486
query_features = list(configuration_helpers.get_value('student_profile_download_fields', []))
14871487

1488-
if not query_features:
1489-
query_features = [
1490-
'id', 'username', 'name', 'email', 'language', 'location',
1491-
'year_of_birth', 'gender', 'level_of_education', 'mailing_address',
1492-
'goals', 'enrollment_mode', 'verification_status',
1493-
'last_login', 'date_joined', 'external_user_key',
1494-
'enrollment_date'
1495-
]
1496-
1497-
# Provide human-friendly and translatable names for these features. These names
1498-
# will be displayed in the table generated in data_download.js. It is not (yet)
1499-
# used as the header row in the CSV, but could be in the future.
1500-
query_features_names = {
1501-
'id': _('User ID'),
1502-
'username': _('Username'),
1503-
'name': _('Name'),
1504-
'email': _('Email'),
1505-
'language': _('Language'),
1506-
'location': _('Location'),
1507-
'year_of_birth': _('Birth Year'),
1508-
'gender': _('Gender'),
1509-
'level_of_education': _('Level of Education'),
1510-
'mailing_address': _('Mailing Address'),
1511-
'goals': _('Goals'),
1512-
'enrollment_mode': _('Enrollment Mode'),
1513-
'last_login': _('Last Login'),
1514-
'date_joined': _('Date Joined'),
1515-
'external_user_key': _('External User Key'),
1516-
'enrollment_date': _('Enrollment Date'),
1517-
}
1518-
1519-
if not settings.FEATURES.get('SHOW_PRIVATE_FIELDS_IN_PROFILE_INFORMATION_REPORT', False):
1520-
keep_field_private(query_features, 'year_of_birth')
1521-
query_features_names.pop('year_of_birth', None)
1522-
1523-
if is_course_cohorted(course.id):
1524-
# Translators: 'Cohort' refers to a group of students within a course.
1525-
query_features.append('cohort')
1526-
query_features_names['cohort'] = _('Cohort')
1527-
1528-
if course.teams_enabled:
1529-
query_features.append('team')
1530-
query_features_names['team'] = _('Team')
1488+
if not query_features:
1489+
query_features = [
1490+
'id', 'username', 'name', 'email', 'language', 'location',
1491+
'year_of_birth', 'gender', 'level_of_education', 'mailing_address',
1492+
'goals', 'enrollment_mode', 'last_login', 'date_joined', 'external_user_key',
1493+
'enrollment_date',
1494+
]
15311495

1532-
# For compatibility reasons, city and country should always appear last.
1533-
query_features.append('city')
1534-
query_features_names['city'] = _('City')
1535-
query_features.append('country')
1536-
query_features_names['country'] = _('Country')
1496+
additional_attributes = configuration_helpers.get_value_for_org(
1497+
course_key.org,
1498+
"additional_student_profile_attributes"
1499+
)
1500+
if additional_attributes:
1501+
# Fail fast: must be list/tuple of strings.
1502+
if not isinstance(additional_attributes, (list, tuple)):
1503+
return JsonResponseBadRequest(
1504+
_('Invalid additional student attribute configuration: expected list of strings, got {type}.')
1505+
.format(type=type(additional_attributes).__name__)
1506+
)
1507+
if not all(isinstance(v, str) for v in additional_attributes):
1508+
return JsonResponseBadRequest(
1509+
_('Invalid additional student attribute configuration: all entries must be strings.')
1510+
)
1511+
# Reject empty string entries explicitly.
1512+
if any(v == '' for v in additional_attributes):
1513+
return JsonResponseBadRequest(
1514+
_('Invalid additional student attribute configuration: empty attribute names are not allowed.')
1515+
)
1516+
# Validate each attribute is in available_features; allow duplicates as provided.
1517+
invalid = [v for v in additional_attributes if v not in available_features]
1518+
if invalid:
1519+
return JsonResponseBadRequest(
1520+
_('Invalid additional student attributes: {attrs}').format(
1521+
attrs=', '.join(invalid)
1522+
)
1523+
)
1524+
query_features.extend(additional_attributes)
1525+
1526+
# Provide human-friendly and translatable names for these features. These names
1527+
# will be displayed in the table generated in data_download.js. It is not (yet)
1528+
# used as the header row in the CSV, but could be in the future.
1529+
query_features_names = {
1530+
'id': _('User ID'),
1531+
'username': _('Username'),
1532+
'name': _('Name'),
1533+
'email': _('Email'),
1534+
'language': _('Language'),
1535+
'location': _('Location'),
1536+
'year_of_birth': _('Birth Year'),
1537+
'gender': _('Gender'),
1538+
'level_of_education': _('Level of Education'),
1539+
'mailing_address': _('Mailing Address'),
1540+
'goals': _('Goals'),
1541+
'enrollment_mode': _('Enrollment Mode'),
1542+
'last_login': _('Last Login'),
1543+
'date_joined': _('Date Joined'),
1544+
'external_user_key': _('External User Key'),
1545+
'enrollment_date': _('Enrollment Date'),
1546+
}
15371547

1538-
if not csv:
1539-
student_data = instructor_analytics_basic.enrolled_students_features(course_key, query_features)
1540-
response_payload = {
1541-
'course_id': str(course_key),
1542-
'students': student_data,
1543-
'students_count': len(student_data),
1544-
'queried_features': query_features,
1545-
'feature_names': query_features_names,
1546-
'available_features': available_features,
1547-
}
1548-
return JsonResponse(response_payload)
1548+
if additional_attributes:
1549+
for attr in additional_attributes:
1550+
if attr not in query_features_names:
1551+
formatted_name = attr.replace('_', ' ').title()
1552+
# pylint: disable-next=translation-of-non-string
1553+
query_features_names[attr] = _(formatted_name)
1554+
1555+
for field in settings.PROFILE_INFORMATION_REPORT_PRIVATE_FIELDS:
1556+
keep_field_private(query_features, field)
1557+
query_features_names.pop(field, None)
1558+
1559+
if is_course_cohorted(course.id):
1560+
# Translators: 'Cohort' refers to a group of students within a course.
1561+
query_features.append('cohort')
1562+
query_features_names['cohort'] = _('Cohort')
1563+
1564+
if course.teams_enabled:
1565+
query_features.append('team')
1566+
query_features_names['team'] = _('Team')
1567+
1568+
# For compatibility reasons, city and country should always appear last.
1569+
query_features.append('city')
1570+
query_features_names['city'] = _('City')
1571+
query_features.append('country')
1572+
query_features_names['country'] = _('Country')
1573+
1574+
if not csv:
1575+
student_data = instructor_analytics_basic.enrolled_students_features(course_key, query_features)
1576+
response_payload = {
1577+
'course_id': str(course_key),
1578+
'students': student_data,
1579+
'students_count': len(student_data),
1580+
'queried_features': query_features,
1581+
'feature_names': query_features_names,
1582+
'available_features': available_features,
1583+
}
1584+
return JsonResponse(response_payload)
15491585

15501586
else:
15511587
try:

0 commit comments

Comments
 (0)