diff --git a/lms/djangoapps/instructor/tests/test_tools.py b/lms/djangoapps/instructor/tests/test_tools.py index 26f1ccd1f3..0141ac9e83 100644 --- a/lms/djangoapps/instructor/tests/test_tools.py +++ b/lms/djangoapps/instructor/tests/test_tools.py @@ -458,3 +458,39 @@ class TestStudentFromIdentifier(TestCase): """Test with invalid identifier""" with pytest.raises(User.DoesNotExist): assert tools.get_student_from_identifier("invalid") + + +class TestProfilePrivacy(TestCase): + ''' + Tests utility function for stripping a feature from the list of features to be reported on + ''' + def test_no_feature_list_supplied(self): + ''' + Missing first argument raises an exception + ''' + with pytest.raises(tools.DashboardError): + assert tools.keep_field_private(None, "bogus_field_name") + + def test_no_privacy_feature_supplied(self): + ''' + Missing second argument raises an exception + ''' + with pytest.raises(tools.DashboardError): + assert tools.keep_field_private(["bogus_field1", "bogus_field2"], None) + + def test_feature_supplied_and_stripped(self): + ''' + Request to strip a feature in feature list succeeds + ''' + query_fields = ['Name', 'Address', 'Secret'] + assert 'Secret' in query_fields + tools.keep_field_private(query_fields, 'Secret') + assert 'Secret' not in query_fields + + def test_feature_absent_and_exception_consumed(self): + ''' + Request to strip a feature not in feature list is a silent no-op + ''' + query_fields = ['Name', 'Address'] + tools.keep_field_private(query_fields, 'Secret') + assert len(query_fields) == 2 diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index c83af8e1a4..ed3a733a0e 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -129,6 +129,7 @@ from .tools import ( find_unit, get_student_from_identifier, handle_dashboard_error, + keep_field_private, parse_datetime, require_student_from_identifier, set_due_date_extension, @@ -1430,6 +1431,7 @@ def get_students_features(request, course_id, csv=False): # pylint: disable=red 'year_of_birth', 'gender', 'level_of_education', 'mailing_address', 'goals', 'enrollment_mode', 'last_login', 'date_joined', 'external_user_key' ] + keep_field_private(query_features, 'year_of_birth') # protected information # Provide human-friendly and translatable names for these features. These names # will be displayed in the table generated in data_download.js. It is not (yet) @@ -1441,7 +1443,7 @@ def get_students_features(request, course_id, csv=False): # pylint: disable=red 'email': _('Email'), 'language': _('Language'), 'location': _('Location'), - 'year_of_birth': _('Birth Year'), + # 'year_of_birth': _('Birth Year'), treated as privileged information as of TNL-10683, not to go in reports 'gender': _('Gender'), 'level_of_education': _('Level of Education'), 'mailing_address': _('Mailing Address'), diff --git a/lms/djangoapps/instructor/views/tools.py b/lms/djangoapps/instructor/views/tools.py index b660a544ef..5a62f705f5 100644 --- a/lms/djangoapps/instructor/views/tools.py +++ b/lms/djangoapps/instructor/views/tools.py @@ -256,3 +256,17 @@ def dump_student_extensions(course, student): "title": _("Due date extensions for {0} {1} ({2})").format( student.first_name, student.last_name, student.username), "data": data} + + +def keep_field_private(query_features, field_name): + ''' + Utility to remove a field from a list of field names requested of a report + Keeps the specified field_name private (excluded from report) + ''' + if (query_features is None) or (field_name is None): + raise DashboardError("Missing private field specification") + + try: + query_features.remove(field_name) + except ValueError: + pass