diff --git a/lms/djangoapps/instructor/tests/test_api.py b/lms/djangoapps/instructor/tests/test_api.py
index cd0b230423..c5b1b21b52 100644
--- a/lms/djangoapps/instructor/tests/test_api.py
+++ b/lms/djangoapps/instructor/tests/test_api.py
@@ -446,6 +446,19 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
self.assertEqual(student_json['username'], student.username)
self.assertEqual(student_json['email'], student.email)
+ def test_get_anon_ids(self):
+ """
+ Test the CSV output for the anonymized user ids.
+ """
+ url = reverse('get_anon_ids', kwargs={'course_id': self.course.id})
+ with patch('instructor.views.api.unique_id_for_user') as mock_unique:
+ mock_unique.return_value = '42'
+ response = self.client.get(url, {})
+ self.assertEqual(response['Content-Type'], 'text/csv')
+ body = response.content.replace('\r', '')
+ self.assertTrue(body.startswith('"User ID","Anonymized user ID"\n"2","42"\n'))
+ self.assertTrue(body.endswith('"7","42"\n'))
+
def test_get_students_features_csv(self):
"""
Test that some minimum of information is formatted
diff --git a/lms/djangoapps/instructor/tests/test_legacy_anon_csv.py b/lms/djangoapps/instructor/tests/test_legacy_anon_csv.py
new file mode 100644
index 0000000000..947b72f298
--- /dev/null
+++ b/lms/djangoapps/instructor/tests/test_legacy_anon_csv.py
@@ -0,0 +1,71 @@
+"""
+Unit tests for instructor dashboard
+
+Based on (and depends on) unit tests for courseware.
+
+Notes for running by hand:
+
+./manage.py lms --settings test test lms/djangoapps/instructor
+"""
+
+from django.test.utils import override_settings
+
+# Need access to internal func to put users in the right group
+from django.contrib.auth.models import Group, User
+
+from django.core.urlresolvers import reverse
+
+from courseware.access import _course_staff_group_name
+from courseware.tests.helpers import LoginEnrollmentTestCase
+from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
+from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
+from xmodule.modulestore.django import modulestore, clear_existing_modulestores
+import xmodule.modulestore.django
+
+from mock import patch
+
+
+@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
+class TestInstructorDashboardAnonCSV(ModuleStoreTestCase, LoginEnrollmentTestCase):
+ '''
+ Check for download of csv
+ '''
+
+ # Note -- I copied this setUp from a similar test
+ def setUp(self):
+ clear_existing_modulestores()
+ self.toy = modulestore().get_course("edX/toy/2012_Fall")
+
+ # Create two accounts
+ self.student = 'view@test.com'
+ self.instructor = 'view2@test.com'
+ self.password = 'foo'
+ self.create_account('u1', self.student, self.password)
+ self.create_account('u2', self.instructor, self.password)
+ self.activate_user(self.student)
+ self.activate_user(self.instructor)
+
+ def make_instructor(course):
+ """ Create an instructor for the course. """
+ group_name = _course_staff_group_name(course.location)
+ group = Group.objects.create(name=group_name)
+ group.user_set.add(User.objects.get(email=self.instructor))
+
+ make_instructor(self.toy)
+
+ self.logout()
+ self.login(self.instructor, self.password)
+ self.enroll(self.toy)
+
+ def test_download_anon_csv(self):
+ course = self.toy
+ url = reverse('instructor_dashboard', kwargs={'course_id': course.id})
+
+ with patch('instructor.views.legacy.unique_id_for_user') as mock_unique:
+ mock_unique.return_value = 42
+ response = self.client.post(url, {'action': 'Download CSV of all student anonymized IDs'})
+
+ self.assertEqual(response['Content-Type'], 'text/csv')
+ body = response.content.replace('\r', '')
+ self.assertEqual(body, '"User ID","Anonymized user ID"\n"2","42"\n')
+
diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py
index 6c68b7fed6..8a552feb66 100644
--- a/lms/djangoapps/instructor/views/api.py
+++ b/lms/djangoapps/instructor/views/api.py
@@ -28,6 +28,7 @@ from django_comment_common.models import (Role,
FORUM_ROLE_COMMUNITY_TA)
from courseware.models import StudentModule
+from student.models import unique_id_for_user
import instructor_task.api
from instructor_task.api_helper import AlreadyRunningError
import instructor.enrollment as enrollment
@@ -37,6 +38,7 @@ import instructor.access as access
import analytics.basic
import analytics.distributions
import analytics.csvs
+import csv
log = logging.getLogger(__name__)
@@ -368,6 +370,38 @@ def get_students_features(request, course_id, csv=False): # pylint: disable=W06
return analytics.csvs.create_csv_response("enrolled_profiles.csv", header, datarows)
+@ensure_csrf_cookie
+@cache_control(no_cache=True, no_store=True, must_revalidate=True)
+@require_level('staff')
+def get_anon_ids(request, course_id): # pylint: disable=W0613
+ """
+ Respond with 2-column CSV output of user-id, anonymized-user-id
+ """
+ # TODO: the User.objects query and CSV generation here could be
+ # centralized into analytics. Currently analytics has similar functionality
+ # but not quite what's needed.
+ def csv_response(filename, header, rows):
+ """Returns a CSV http response for the given header and rows (excel/utf-8)."""
+ response = HttpResponse(mimetype='text/csv')
+ response['Content-Disposition'] = 'attachment; filename={0}'.format(filename)
+ writer = csv.writer(response, dialect='excel', quotechar='"', quoting=csv.QUOTE_ALL)
+ # In practice, there should not be non-ascii data in this query,
+ # but trying to do the right thing anyway.
+ encoded = [unicode(s).encode('utf-8') for s in header]
+ writer.writerow(encoded)
+ for row in rows:
+ encoded = [unicode(s).encode('utf-8') for s in row]
+ writer.writerow(encoded)
+ return response
+
+ students = User.objects.filter(
+ courseenrollment__course_id=course_id,
+ ).order_by('id')
+ header =['User ID', 'Anonymized user ID']
+ rows = [[s.id, unique_id_for_user(s)] for s in students]
+ return csv_response(course_id.replace('/', '-') + '-anon-ids.csv', header, rows)
+
+
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
@require_level('staff')
diff --git a/lms/djangoapps/instructor/views/api_urls.py b/lms/djangoapps/instructor/views/api_urls.py
index 8c67c24a77..07af69558f 100644
--- a/lms/djangoapps/instructor/views/api_urls.py
+++ b/lms/djangoapps/instructor/views/api_urls.py
@@ -16,6 +16,8 @@ urlpatterns = patterns('', # nopep8
'instructor.views.api.get_grading_config', name="get_grading_config"),
url(r'^get_students_features(?P
+ +