Merge pull request #1104 from edx/nick/anon-userids
Add "Download CSV of all student anonymized IDs" button to instructor dashboards
This commit is contained in:
@@ -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
|
||||
|
||||
71
lms/djangoapps/instructor/tests/test_legacy_anon_csv.py
Normal file
71
lms/djangoapps/instructor/tests/test_legacy_anon_csv.py
Normal file
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -16,6 +16,8 @@ urlpatterns = patterns('', # nopep8
|
||||
'instructor.views.api.get_grading_config', name="get_grading_config"),
|
||||
url(r'^get_students_features(?P<csv>/csv)?$',
|
||||
'instructor.views.api.get_students_features', name="get_students_features"),
|
||||
url(r'^get_anon_ids$',
|
||||
'instructor.views.api.get_anon_ids', name="get_anon_ids"),
|
||||
url(r'^get_distribution$',
|
||||
'instructor.views.api.get_distribution', name="get_distribution"),
|
||||
url(r'^get_student_progress_url$',
|
||||
|
||||
@@ -133,6 +133,7 @@ def _section_data_download(course_id):
|
||||
'section_display_name': _('Data Download'),
|
||||
'get_grading_config_url': reverse('get_grading_config', kwargs={'course_id': course_id}),
|
||||
'get_students_features_url': reverse('get_students_features', kwargs={'course_id': course_id}),
|
||||
'get_anon_ids_url': reverse('get_anon_ids', kwargs={'course_id': course_id}),
|
||||
}
|
||||
return section_data
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ from instructor_task.api import (get_running_instructor_tasks,
|
||||
from instructor_task.views import get_task_completion_info
|
||||
from mitxmako.shortcuts import render_to_response
|
||||
from psychometrics import psychoanalyze
|
||||
from student.models import CourseEnrollment, CourseEnrollmentAllowed
|
||||
from student.models import CourseEnrollment, CourseEnrollmentAllowed, unique_id_for_user
|
||||
from student.views import course_from_id
|
||||
import track.views
|
||||
from mitxmako.shortcuts import render_to_string
|
||||
@@ -584,6 +584,15 @@ def instructor_dashboard(request, course_id):
|
||||
datatable['title'] = 'Student state for problem %s' % problem_to_dump
|
||||
return return_csv('student_state_from_%s.csv' % problem_to_dump, datatable)
|
||||
|
||||
elif 'Download CSV of all student anonymized IDs' in action:
|
||||
students = User.objects.filter(
|
||||
courseenrollment__course_id=course_id,
|
||||
).order_by('id')
|
||||
|
||||
datatable = {'header': ['User ID', 'Anonymized user ID']}
|
||||
datatable['data'] = [[s.id, unique_id_for_user(s)] for s in students]
|
||||
return return_csv(course_id.replace('/', '-') + '-anon-ids.csv', datatable)
|
||||
|
||||
#----------------------------------------
|
||||
# Group management
|
||||
|
||||
|
||||
@@ -16,10 +16,16 @@ class DataDownload
|
||||
@$display_table = @$display.find '.data-display-table'
|
||||
@$request_response_error = @$display.find '.request-response-error'
|
||||
@$list_studs_btn = @$section.find("input[name='list-profiles']'")
|
||||
@$list_anon_btn = @$section.find("input[name='list-anon-ids']'")
|
||||
@$grade_config_btn = @$section.find("input[name='dump-gradeconf']'")
|
||||
|
||||
# attach click handlers
|
||||
|
||||
# The list-anon case is always CSV
|
||||
@$list_anon_btn.click (e) =>
|
||||
url = @$list_anon_btn.data 'endpoint'
|
||||
location.href = url
|
||||
|
||||
# this handler binds to both the download
|
||||
# and the csv button
|
||||
@$list_studs_btn.click (e) =>
|
||||
|
||||
@@ -416,6 +416,9 @@ function goto( mode)
|
||||
<input type="text" name="problem_to_dump" size="40">
|
||||
<input type="submit" name="action" value="Download CSV of all responses to problem">
|
||||
</p>
|
||||
<p>
|
||||
<input type="submit" name="action" value="Download CSV of all student anonymized IDs">
|
||||
</p>
|
||||
<hr width="40%" style="align:left">
|
||||
%endif
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
## <input type="button" name="list-answer-distributions" value="Answer distributions (x students got y points)">
|
||||
## <br>
|
||||
<input type="button" name="dump-gradeconf" value="${_("Grading Configuration")}" data-endpoint="${ section_data['get_grading_config_url'] }">
|
||||
<input type="button" name="list-anon-ids" value="${_("Get Student Anonymized IDs CSV")}" data-csv="true" class="csv" data-endpoint="${ section_data['get_anon_ids_url'] }">
|
||||
|
||||
|
||||
<div class="data-display">
|
||||
<div class="data-display-text"></div>
|
||||
|
||||
@@ -3,6 +3,19 @@
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
<%namespace name='static' file='/static_content.html'/>
|
||||
|
||||
## ----- Tips on adding something to the new instructor dashboard -----
|
||||
## 1. add your input element, e.g. in instructor_dashboard2/data_download.html
|
||||
## the input includes a reference like data-endpoint="${ section_data['get_anon_ids_url'] }"
|
||||
## 2. Go to the old dashboard djangoapps/instructor/views/instructor_dashboard.py and
|
||||
## add in a definition of 'xxx_url' in the right section_data for whatever page your
|
||||
## feature is on.
|
||||
## 3. Add a url() entry in api_urls.py
|
||||
## 4. Over in lms/static/coffee/src/instructor_dashboard/ there there are .coffee files
|
||||
## for each page which define the .js. Edit this to make your input do something
|
||||
## when clicked. The .coffee files use the name=xx to pick out inputs, not id=
|
||||
## 5. Implement your standard django/python in lms/djangoapps/instructor/views/api.py
|
||||
## 6. And tests go in lms/djangoapps/instructor/tests/
|
||||
|
||||
<%block name="headextra">
|
||||
<%static:css group='course'/>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/underscore-min.js')}"></script>
|
||||
|
||||
Reference in New Issue
Block a user