@@ -1477,7 +1477,7 @@ def post(self, request, course_id, csv=False): # pylint: disable=redefined-oute
14771477 course_key = CourseKey .from_string (course_id )
14781478 course = get_course_by_id (course_key )
14791479 report_type = _ ('enrolled learner profile' )
1480- available_features = instructor_analytics_basic .AVAILABLE_FEATURES
1480+ available_features = instructor_analytics_basic .get_available_features ( course_key )
14811481
14821482 # Allow for sites to be able to define additional columns.
14831483 # Note that adding additional columns has the potential to break
@@ -1493,9 +1493,40 @@ def post(self, request, course_id, csv=False): # pylint: disable=redefined-oute
14931493 query_features = [
14941494 'id' , 'username' , 'name' , 'email' , 'language' , 'location' ,
14951495 'year_of_birth' , 'gender' , 'level_of_education' , 'mailing_address' ,
1496- 'goals' , 'enrollment_mode' , 'last_login' , 'date_joined' , 'external_user_key'
1496+ 'goals' , 'enrollment_mode' , 'last_login' , 'date_joined' , 'external_user_key' ,
1497+ 'enrollment_date' ,
14971498 ]
14981499
1500+ additional_attributes = configuration_helpers .get_value_for_org (
1501+ course_key .org ,
1502+ "additional_student_profile_attributes"
1503+ )
1504+ if additional_attributes :
1505+ # Fail fast: must be list/tuple of strings.
1506+ if not isinstance (additional_attributes , (list , tuple )):
1507+ return JsonResponseBadRequest (
1508+ _ ('Invalid additional student attribute configuration: expected list of strings, got {type}.' )
1509+ .format (type = type (additional_attributes ).__name__ )
1510+ )
1511+ if not all (isinstance (v , str ) for v in additional_attributes ):
1512+ return JsonResponseBadRequest (
1513+ _ ('Invalid additional student attribute configuration: all entries must be strings.' )
1514+ )
1515+ # Reject empty string entries explicitly.
1516+ if any (v == '' for v in additional_attributes ):
1517+ return JsonResponseBadRequest (
1518+ _ ('Invalid additional student attribute configuration: empty attribute names are not allowed.' )
1519+ )
1520+ # Validate each attribute is in available_features; allow duplicates as provided.
1521+ invalid = [v for v in additional_attributes if v not in available_features ]
1522+ if invalid :
1523+ return JsonResponseBadRequest (
1524+ _ ('Invalid additional student attributes: {attrs}' ).format (
1525+ attrs = ', ' .join (invalid )
1526+ )
1527+ )
1528+ query_features .extend (additional_attributes )
1529+
14991530 # Provide human-friendly and translatable names for these features. These names
15001531 # will be displayed in the table generated in data_download.js. It is not (yet)
15011532 # used as the header row in the CSV, but could be in the future.
@@ -1515,8 +1546,16 @@ def post(self, request, course_id, csv=False): # pylint: disable=redefined-oute
15151546 'last_login' : _ ('Last Login' ),
15161547 'date_joined' : _ ('Date Joined' ),
15171548 'external_user_key' : _ ('External User Key' ),
1549+ 'enrollment_date' : _ ('Enrollment Date' ),
15181550 }
15191551
1552+ if additional_attributes :
1553+ for attr in additional_attributes :
1554+ if attr not in query_features_names :
1555+ formatted_name = attr .replace ('_' , ' ' ).title ()
1556+ # pylint: disable-next=translation-of-non-string
1557+ query_features_names [attr ] = _ (formatted_name )
1558+
15201559 for field in settings .PROFILE_INFORMATION_REPORT_PRIVATE_FIELDS :
15211560 keep_field_private (query_features , field )
15221561 query_features_names .pop (field , None )
0 commit comments