Make student profile CSV download a background task, add cohort column

This commit is contained in:
Daniel Friedman
2014-09-26 17:01:04 -04:00
parent bb58e4b3ce
commit ba3f7f9409
17 changed files with 351 additions and 114 deletions

View File

@@ -92,9 +92,9 @@ def click_a_button(step, button): # pylint: disable=unused-argument
# Expect to see a message that grade report is being generated
expected_msg = "Your grade report is being generated! You can view the status of the generation task in the 'Pending Instructor Tasks' section."
world.wait_for_visible('#grade-request-response')
world.wait_for_visible('#report-request-response')
assert_in(
expected_msg, world.css_text('#grade-request-response'),
expected_msg, world.css_text('#report-request-response'),
msg="Could not find grade report generation success message."
)
@@ -113,7 +113,8 @@ def click_a_button(step, button): # pylint: disable=unused-argument
elif button == "Download profile information as a CSV":
# Go to the data download section of the instructor dash
go_to_section("data_download")
# Don't do anything else, next step will handle clicking & downloading
world.css_click('input[name="list-profiles-csv"]')
else:
raise ValueError("Unrecognized button option " + button)

View File

@@ -44,3 +44,12 @@ Feature: LMS.Instructor Dash Data Download
| Role |
| instructor |
| staff |
Scenario: Generate & download a student profile report
Given I am "<Role>" for a course
When I click "Download profile information as a CSV"
Then I see a student profile csv file in the reports table
Examples:
| Role |
| instructor |
| staff |

View File

@@ -68,14 +68,23 @@ length=0"""
assert_in(expected_config, world.css_text('#data-grade-config-text'))
@step(u"I see a grade report csv file in the reports table")
def find_grade_report_csv_link(step): # pylint: disable=unused-argument
# Need to reload the page to see the grades download table
def verify_report_is_generated(report_name_substring):
# Need to reload the page to see the reports table updated
reload_the_page(step)
world.wait_for_visible('#report-downloads-table')
# Find table and assert a .csv file is present
expected_file_regexp = 'edx_999_Test_Course_grade_report_\d{4}-\d{2}-\d{2}-\d{4}\.csv'
expected_file_regexp = 'edx_999_Test_Course_{0}_'.format(report_name_substring) + '\d{4}-\d{2}-\d{2}-\d{4}\.csv'
assert_regexp_matches(
world.css_html('#report-downloads-table'), expected_file_regexp,
msg="Expected grade report filename was not found."
msg="Expected report filename was not found."
)
@step(u"I see a grade report csv file in the reports table")
def find_grade_report_csv_link(step): # pylint: disable=unused-argument
verify_report_is_generated('grade_report')
@step(u"I see a student profile csv file in the reports table")
def find_student_profile_report_csv_link(step): # pylint: disable=unused-argument
verify_report_is_generated('student_profile_info')

View File

@@ -55,6 +55,22 @@ from ..views.tools import get_extended_due
EXPECTED_CSV_HEADER = '"code","course_id","company_name","created_by","redeemed_by","invoice_id","purchaser","customer_reference_number","internal_reference"'
EXPECTED_COUPON_CSV_HEADER = '"course_id","percentage_discount","code_redeemed_count","description"'
# ddt data for test cases involving reports
REPORTS_DATA = (
{
'report_type': 'grade',
'instructor_api_endpoint': 'calculate_grades_csv',
'task_api_endpoint': 'instructor_task.api.submit_calculate_grades_csv',
'extra_instructor_api_kwargs': {}
},
{
'report_type': 'enrolled student profile',
'instructor_api_endpoint': 'get_students_features',
'task_api_endpoint': 'instructor_task.api.submit_calculate_students_features_csv',
'extra_instructor_api_kwargs': {'csv': '/csv'}
}
)
@common_exceptions_400
def view_success(request): # pylint: disable=W0613
@@ -156,6 +172,7 @@ class TestInstructorAPIDenyLevels(ModuleStoreTestCase, LoginEnrollmentTestCase):
('list_background_email_tasks', {}),
('list_report_downloads', {}),
('calculate_grades_csv', {}),
('get_students_features', {}),
]
# Endpoints that only Instructors can access
self.instructor_level_endpoints = [
@@ -1343,6 +1360,7 @@ class TestInstructorAPILevelsAccess(ModuleStoreTestCase, LoginEnrollmentTestCase
self.assertNotIn(rolename, user_roles)
@ddt.ddt
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCase):
"""
@@ -1350,6 +1368,7 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
"""
def setUp(self):
super(TestInstructorAPILevelsDataDump, self).setUp()
self.course = CourseFactory.create()
self.course_mode = CourseMode(course_id=self.course.id,
mode_slug="honor",
@@ -1613,6 +1632,21 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
self.assertEqual(student_json['username'], student.username)
self.assertEqual(student_json['email'], student.email)
@ddt.data(True, False)
def test_get_students_features_cohorted(self, is_cohorted):
"""
Test that get_students_features includes cohort info when the course is
cohorted, and does not when the course is not cohorted.
"""
url = reverse('get_students_features', kwargs={'course_id': unicode(self.course.id)})
self.course.cohort_config = {'cohorted': is_cohorted}
self.store.update_item(self.course, self.instructor.id)
response = self.client.get(url, {})
res_json = json.loads(response.content)
self.assertEqual('cohort' in res_json['feature_names'], is_cohorted)
@patch.object(instructor.views.api, 'anonymous_id_for_user', Mock(return_value='42'))
@patch.object(instructor.views.api, 'unique_id_for_user', Mock(return_value='41'))
def test_get_anon_ids(self):
@@ -1625,9 +1659,9 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
body = response.content.replace('\r', '')
self.assertTrue(body.startswith(
'"User ID","Anonymized User ID","Course Specific Anonymized User ID"'
'\n"2","41","42"\n'
'\n"3","41","42"\n'
))
self.assertTrue(body.endswith('"7","41","42"\n'))
self.assertTrue(body.endswith('"8","41","42"\n'))
def test_list_report_downloads(self):
url = reverse('list_report_downloads', kwargs={'course_id': self.course.id.to_deprecated_string()})
@@ -1655,33 +1689,31 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
res_json = json.loads(response.content)
self.assertEqual(res_json, expected_response)
def test_calculate_grades_csv_success(self):
url = reverse('calculate_grades_csv', kwargs={'course_id': self.course.id.to_deprecated_string()})
@ddt.data(*REPORTS_DATA)
@ddt.unpack
def test_calculate_report_csv_success(self, report_type, instructor_api_endpoint, task_api_endpoint, extra_instructor_api_kwargs):
kwargs = {'course_id': unicode(self.course.id)}
kwargs.update(extra_instructor_api_kwargs)
url = reverse(instructor_api_endpoint, kwargs=kwargs)
with patch('instructor_task.api.submit_calculate_grades_csv') as mock_cal_grades:
mock_cal_grades.return_value = True
with patch(task_api_endpoint):
response = self.client.get(url, {})
success_status = "Your grade report is being generated! You can view the status of the generation task in the 'Pending Instructor Tasks' section."
success_status = "Your {report_type} report is being generated! You can view the status of the generation task in the 'Pending Instructor Tasks' section.".format(report_type=report_type)
self.assertIn(success_status, response.content)
def test_calculate_grades_csv_already_running(self):
url = reverse('calculate_grades_csv', kwargs={'course_id': self.course.id.to_deprecated_string()})
@ddt.data(*REPORTS_DATA)
@ddt.unpack
def test_calculate_report_csv_already_running(self, report_type, instructor_api_endpoint, task_api_endpoint, extra_instructor_api_kwargs):
kwargs = {'course_id': unicode(self.course.id)}
kwargs.update(extra_instructor_api_kwargs)
url = reverse(instructor_api_endpoint, kwargs=kwargs)
with patch('instructor_task.api.submit_calculate_grades_csv') as mock_cal_grades:
mock_cal_grades.side_effect = AlreadyRunningError()
with patch(task_api_endpoint) as mock:
mock.side_effect = AlreadyRunningError()
response = self.client.get(url, {})
already_running_status = "A grade report generation task is already in progress. Check the 'Pending Instructor Tasks' table for the status of the task. When completed, the report will be available for download in the table below."
already_running_status = "{report_type} report generation task is already in progress. Check the 'Pending Instructor Tasks' table for the status of the task. When completed, the report will be available for download in the table below.".format(report_type=report_type)
self.assertIn(already_running_status, response.content)
def test_get_students_features_csv(self):
"""
Test that some minimum of information is formatted
correctly in the response to get_students_features.
"""
url = reverse('get_students_features', kwargs={'course_id': self.course.id.to_deprecated_string()})
response = self.client.get(url + '/csv', {})
self.assertEqual(response['Content-Type'], 'text/csv')
def test_get_distribution_no_feature(self):
"""
Test that get_distribution lists available features

View File

@@ -80,6 +80,7 @@ from .tools import (
bulk_email_is_enabled_for_course,
add_block_ids,
)
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from opaque_keys import InvalidKeyError
@@ -690,7 +691,8 @@ def get_students_features(request, course_id, csv=False): # pylint: disable=W06
TO DO accept requests for different attribute sets.
"""
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id)
course_key = CourseKey.from_string(course_id)
course = get_course_by_id(course_key)
available_features = instructor_analytics.basic.AVAILABLE_FEATURES
@@ -704,8 +706,6 @@ def get_students_features(request, course_id, csv=False): # pylint: disable=W06
'goals'
]
student_data = instructor_analytics.basic.enrolled_students_features(course_id, query_features)
# Provide human-friendly and translatable names for these features. These names
# will be displayed in the table generated in data_download.coffee. It is not (yet)
# used as the header row in the CSV, but could be in the future.
@@ -723,9 +723,15 @@ def get_students_features(request, course_id, csv=False): # pylint: disable=W06
'goals': _('Goals'),
}
if course.is_cohorted:
# Translators: 'Cohort' refers to a group of students within a course.
query_features.append('cohort')
query_features_names['cohort'] = _('Cohort')
if not csv:
student_data = instructor_analytics.basic.enrolled_students_features(course_key, query_features)
response_payload = {
'course_id': course_id.to_deprecated_string(),
'course_id': unicode(course_key),
'students': student_data,
'students_count': len(student_data),
'queried_features': query_features,
@@ -734,8 +740,13 @@ def get_students_features(request, course_id, csv=False): # pylint: disable=W06
}
return JsonResponse(response_payload)
else:
header, datarows = instructor_analytics.csvs.format_dictlist(student_data, query_features)
return instructor_analytics.csvs.create_csv_response("enrolled_profiles.csv", header, datarows)
try:
instructor_task.api.submit_calculate_students_features_csv(request, course_key, query_features)
success_status = _("Your enrolled student profile report is being generated! You can view the status of the generation task in the 'Pending Instructor Tasks' section.")
return JsonResponse({"status": success_status})
except AlreadyRunningError:
already_running_status = _("An enrolled student profile report generation task is already in progress. Check the 'Pending Instructor Tasks' table for the status of the task. When completed, the report will be available for download in the table below.")
return JsonResponse({"status": already_running_status})
@ensure_csrf_cookie