@@ -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