Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions common/test/acceptance/pages/lms/instructor_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -912,6 +912,13 @@ def report_download_links(self):
"""
return self.q(css="#report-downloads-table .file-download-link>a")

@property
def generate_ora2_response_report_button(self):
"""
Returns the ORA2 response download button for the current page.
"""
return self.q(css='input[name=export-ora2-data]')

def wait_for_available_report(self):
"""
Waits for a downloadable report to be available.
Expand Down
14 changes: 14 additions & 0 deletions common/test/acceptance/tests/lms/test_lms_instructor_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,20 @@ def test_problem_grade_report_download(self):
self.verify_report_requested_event(report_name)
self.verify_report_download(report_name)

def test_ora2_response_report_download(self):
"""
Scenario: Verify that an instructor can download an ORA2 grade report

Given that I am an instructor
And I visit the instructor dashboard's "Data Downloads" tab
And I click on the "Download ORA2 Responses" button
Then a report should be generated
"""
report_name = u"ORA_data"
self.data_download_section.generate_ora2_response_report_button.click()
self.data_download_section.wait_for_available_report()
self.verify_report_download(report_name)


@attr('shard_7')
class CertificatesTest(BaseInstructorDashboardTest):
Expand Down
2 changes: 1 addition & 1 deletion lms/djangoapps/instructor/features/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def click_a_button(step, button): # pylint: disable=unused-argument
# Expect to see a message that grade report is being generated
expected_msg = "The grade report is being created." \
" To view the status of the report, see" \
" Pending Instructor Tasks below."
" Pending Tasks below."
world.wait_for_visible('#report-request-response')
assert_in(
expected_msg, world.css_text('#report-request-response'),
Expand Down
36 changes: 30 additions & 6 deletions lms/djangoapps/instructor/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ def setUp(self):
('get_exec_summary_report', {}),
('get_proctored_exam_results', {}),
('get_problem_responses', {}),
('export_ora2_data', {}),
]
# Endpoints that only Instructors can access
self.instructor_level_endpoints = [
Expand Down Expand Up @@ -322,6 +323,8 @@ def test_staff_level(self):
# update_forum_role(self.course.id, staff_member, FORUM_ROLE_ADMINISTRATOR, 'allow')

for endpoint, args in self.staff_level_endpoints:
expected_status = 200

# TODO: make these work
if endpoint in ['update_forum_role_membership', 'list_forum_members']:
continue
Expand All @@ -333,7 +336,7 @@ def test_staff_level(self):
self._access_endpoint(
endpoint,
args,
200,
expected_status,
"Staff member should be allowed to access endpoint " + endpoint
)

Expand All @@ -356,6 +359,8 @@ def test_instructor_level(self):
self.client.login(username=inst.username, password='test')

for endpoint, args in self.staff_level_endpoints:
expected_status = 200

# TODO: make these work
if endpoint in ['update_forum_role_membership']:
continue
Expand All @@ -367,18 +372,20 @@ def test_instructor_level(self):
self._access_endpoint(
endpoint,
args,
200,
expected_status,
"Instructor should be allowed to access endpoint " + endpoint
)

for endpoint, args in self.instructor_level_endpoints:
expected_status = 200

# TODO: make this work
if endpoint in ['rescore_problem']:
continue
self._access_endpoint(
endpoint,
args,
200,
expected_status,
"Instructor should be allowed to access endpoint " + endpoint
)

Expand Down Expand Up @@ -2866,8 +2873,7 @@ def test_executive_summary_report_success(
response = self.client.get(url, {})
success_status = "The {report_type} report is being created." \
" To view the status of the report, see Pending" \
" Instructor Tasks" \
" below".format(report_type=report_type)
" Tasks below".format(report_type=report_type)
self.assertIn(success_status, response.content)

@ddt.data(*EXECUTIVE_SUMMARY_DATA)
Expand All @@ -2888,12 +2894,30 @@ def test_executive_summary_report_already_running(
mock.side_effect = AlreadyRunningError()
response = self.client.get(url, {})
already_running_status = "The {report_type} report is currently being created." \
" To view the status of the report, see Pending Instructor Tasks below." \
" To view the status of the report, see Pending Tasks below." \
" You will be able to download the report" \
" when it is" \
" complete.".format(report_type=report_type)
self.assertIn(already_running_status, response.content)

def test_get_ora2_responses_success(self):
url = reverse('export_ora2_data', kwargs={'course_id': unicode(self.course.id)})

with patch('instructor_task.api.submit_export_ora2_data') as mock_submit_ora2_task:
mock_submit_ora2_task.return_value = True
response = self.client.get(url, {})
success_status = "The ORA data report is being generated."
self.assertIn(success_status, response.content)

def test_get_ora2_responses_already_running(self):
url = reverse('export_ora2_data', kwargs={'course_id': unicode(self.course.id)})

with patch('instructor_task.api.submit_export_ora2_data') as mock_submit_ora2_task:
mock_submit_ora2_task.side_effect = AlreadyRunningError()
response = self.client.get(url, {})
already_running_status = "An ORA data report generation task is already in progress."
self.assertIn(already_running_status, response.content)

def test_get_student_progress_url(self):
""" Test that progress_url is in the successful response. """
url = reverse('get_student_progress_url', kwargs={'course_id': self.course.id.to_deprecated_string()})
Expand Down
61 changes: 42 additions & 19 deletions lms/djangoapps/instructor/views/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1290,12 +1290,12 @@ def get_students_features(request, course_id, csv=False): # pylint: disable=red
try:
instructor_task.api.submit_calculate_students_features_csv(request, course_key, query_features)
success_status = _("The enrolled learner profile report is being created."
" To view the status of the report, see Pending Instructor Tasks below.")
" To view the status of the report, see Pending Tasks below.")
return JsonResponse({"status": success_status})
except AlreadyRunningError:
already_running_status = _(
"This enrollment report is currently being created."
" To view the status of the report, see Pending Instructor Tasks below."
" To view the status of the report, see Pending Tasks below."
" You will be able to download the report when it is complete.")
return JsonResponse({"status": already_running_status})

Expand All @@ -1320,13 +1320,13 @@ def get_students_who_may_enroll(request, course_id):
success_status = _(
"The enrollment report is being created. This report contains"
" information about learners who can enroll in the course."
" To view the status of the report, see Pending Instructor Tasks below."
" To view the status of the report, see Pending Tasks below."
)
return JsonResponse({"status": success_status})
except AlreadyRunningError:
already_running_status = _(
"This enrollment report is currently being created."
" To view the status of the report, see Pending Instructor Tasks below."
" To view the status of the report, see Pending Tasks below."
" You will be able to download the report when it is complete."
)
return JsonResponse({"status": already_running_status})
Expand Down Expand Up @@ -1420,11 +1420,11 @@ def get_enrollment_report(request, course_id):
try:
instructor_task.api.submit_detailed_enrollment_features_csv(request, course_key)
success_status = _("The detailed enrollment report is being created."
" To view the status of the report, see Pending Instructor Tasks below.")
" To view the status of the report, see Pending Tasks below.")
return JsonResponse({"status": success_status})
except AlreadyRunningError:
already_running_status = _("The detailed enrollment report is being created."
" To view the status of the report, see Pending Instructor Tasks below."
" To view the status of the report, see Pending Tasks below."
" You will be able to download the report when it is complete.")
return JsonResponse({
"status": already_running_status
Expand All @@ -1444,11 +1444,11 @@ def get_exec_summary_report(request, course_id):
try:
instructor_task.api.submit_executive_summary_report(request, course_key)
status_response = _("The executive summary report is being created."
" To view the status of the report, see Pending Instructor Tasks below.")
" To view the status of the report, see Pending Tasks below.")
except AlreadyRunningError:
status_response = _(
"The executive summary report is currently being created."
" To view the status of the report, see Pending Instructor Tasks below."
" To view the status of the report, see Pending Tasks below."
" You will be able to download the report when it is complete."
)
return JsonResponse({
Expand All @@ -1468,11 +1468,11 @@ def get_course_survey_results(request, course_id):
try:
instructor_task.api.submit_course_survey_report(request, course_key)
status_response = _("The survey report is being created."
" To view the status of the report, see Pending Instructor Tasks below.")
" To view the status of the report, see Pending Tasks below.")
except AlreadyRunningError:
status_response = _(
"The survey report is currently being created."
" To view the status of the report, see Pending Instructor Tasks below."
" To view the status of the report, see Pending Tasks below."
" You will be able to download the report when it is complete."
)
return JsonResponse({
Expand Down Expand Up @@ -1503,11 +1503,11 @@ def get_proctored_exam_results(request, course_id):
try:
instructor_task.api.submit_proctored_exam_results_report(request, course_key, query_features)
status_response = _("The proctored exam results report is being created."
" To view the status of the report, see Pending Instructor Tasks below.")
" To view the status of the report, see Pending Tasks below.")
except AlreadyRunningError:
status_response = _(
"The proctored exam results report is currently being created."
" To view the status of the report, see Pending Instructor Tasks below."
" To view the status of the report, see Pending Tasks below."
" You will be able to download the report when it is complete."
)
return JsonResponse({
Expand Down Expand Up @@ -2327,6 +2327,31 @@ def list_financial_report_downloads(_request, course_id):
return JsonResponse(response_payload)


@transaction.non_atomic_requests
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
@require_level('staff')
def export_ora2_data(request, course_id):
"""
Pushes a Celery task which will aggregate ora2 responses for a course into a .csv
"""
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
try:
instructor_task.api.submit_export_ora2_data(request, course_key)
success_status = _("The ORA data report is being generated.")

return JsonResponse({"status": success_status})
except AlreadyRunningError:
already_running_status = _(
"An ORA data report generation task is already in "
"progress. Check the 'Pending 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})


@transaction.non_atomic_requests
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
Expand All @@ -2339,15 +2364,13 @@ def calculate_grades_csv(request, course_id):
try:
instructor_task.api.submit_calculate_grades_csv(request, course_key)
success_status = _("The grade report is being created."
" To view the status of the report, see Pending Instructor Tasks below.")
" To view the status of the report, see Pending Tasks below.")
return JsonResponse({"status": success_status})
except AlreadyRunningError:
already_running_status = _("The grade report is currently being created."
" To view the status of the report, see Pending Instructor Tasks below."
" To view the status of the report, see Pending Tasks below."
" You will be able to download the report when it is complete.")
return JsonResponse({
"status": already_running_status
})
return JsonResponse({"status": already_running_status})


@transaction.non_atomic_requests
Expand All @@ -2366,11 +2389,11 @@ def problem_grade_report(request, course_id):
try:
instructor_task.api.submit_problem_grade_report(request, course_key)
success_status = _("The problem grade report is being created."
" To view the status of the report, see Pending Instructor Tasks below.")
" To view the status of the report, see Pending Tasks below.")
return JsonResponse({"status": success_status})
except AlreadyRunningError:
already_running_status = _("A problem grade report is already being generated."
" To view the status of the report, see Pending Instructor Tasks below."
" To view the status of the report, see Pending Tasks below."
" You will be able to download the report when it is complete.")
return JsonResponse({
"status": already_running_status
Expand Down
2 changes: 2 additions & 0 deletions lms/djangoapps/instructor/views/api_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@
'instructor.views.api.get_exec_summary_report', name="get_exec_summary_report"),
url(r'get_course_survey_results$',
'instructor.views.api.get_course_survey_results', name="get_course_survey_results"),
url(r'export_ora2_data',
'instructor.views.api.export_ora2_data', name="export_ora2_data"),

# Coupon Codes..
url(r'get_coupon_codes',
Expand Down
1 change: 1 addition & 0 deletions lms/djangoapps/instructor/views/instructor_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,7 @@ def _section_data_download(course, access):
'problem_grade_report_url': reverse('problem_grade_report', kwargs={'course_id': unicode(course_key)}),
'course_has_survey': True if course.course_survey_name else False,
'course_survey_results_url': reverse('get_course_survey_results', kwargs={'course_id': unicode(course_key)}),
'export_ora2_data_url': reverse('export_ora2_data', kwargs={'course_id': unicode(course_key)}),
}
return section_data

Expand Down
15 changes: 14 additions & 1 deletion lms/djangoapps/instructor_task/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
exec_summary_report_csv,
course_survey_report_csv,
generate_certificates,
proctored_exam_results_csv
proctored_exam_results_csv,
export_ora2_data,
)

from certificates.models import CertificateGenerationHistory
Expand Down Expand Up @@ -424,6 +425,18 @@ def submit_cohort_students(request, course_key, file_name):
return submit_task(request, task_type, task_class, course_key, task_input, task_key)


def submit_export_ora2_data(request, course_key):
"""
AlreadyRunningError is raised if an ora2 report is already being generated.
"""
task_type = 'export_ora2_data'
task_class = export_ora2_data
task_input = {}
task_key = ''

return submit_task(request, task_type, task_class, course_key, task_input, task_key)


def generate_certificates_for_students(request, course_key, students=None): # pylint: disable=invalid-name
"""
Submits a task to generate certificates for given students enrolled in the course or
Expand Down
13 changes: 12 additions & 1 deletion lms/djangoapps/instructor_task/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@
upload_exec_summary_report,
upload_course_survey_report,
generate_students_certificates,
upload_proctored_exam_results_report
upload_proctored_exam_results_report,
upload_ora2_data,
)


Expand Down Expand Up @@ -290,3 +291,13 @@ def cohort_students(entry_id, xmodule_instance_args):
action_name = ugettext_noop('cohorted')
task_fn = partial(cohort_students_and_upload, xmodule_instance_args)
return run_main_task(entry_id, task_fn, action_name)


@task(base=BaseInstructorTask, routing_key=settings.GRADES_DOWNLOAD_ROUTING_KEY) # pylint: disable=not-callable

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the routing key used for, and should this have its own?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The routing key is used for uploading to S3, I believe. I think it makes sense for the ORA data downloads to live in the same place as all the other grade downloads.

def export_ora2_data(entry_id, xmodule_instance_args):
"""
Generate a CSV of ora2 responses and push it to S3.
"""
action_name = ugettext_noop('generated')
task_fn = partial(upload_ora2_data, xmodule_instance_args)
return run_main_task(entry_id, task_fn, action_name)
Loading