Make student profile CSV download a background task, add cohort column
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user