Commit a647169a by Eric Fischer

Merge pull request #11167 from edx/christina/ora-data-download

WIP: Asynchronous download button for ORA2 data
parents f0925509 2b1a7eec
...@@ -912,6 +912,13 @@ class DataDownloadPage(PageObject): ...@@ -912,6 +912,13 @@ class DataDownloadPage(PageObject):
""" """
return self.q(css="#report-downloads-table .file-download-link>a") 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): def wait_for_available_report(self):
""" """
Waits for a downloadable report to be available. Waits for a downloadable report to be available.
......
...@@ -636,6 +636,20 @@ class DataDownloadsTest(BaseInstructorDashboardTest): ...@@ -636,6 +636,20 @@ class DataDownloadsTest(BaseInstructorDashboardTest):
self.verify_report_requested_event(report_name) self.verify_report_requested_event(report_name)
self.verify_report_download(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') @attr('shard_7')
class CertificatesTest(BaseInstructorDashboardTest): class CertificatesTest(BaseInstructorDashboardTest):
......
...@@ -92,7 +92,7 @@ def click_a_button(step, button): # pylint: disable=unused-argument ...@@ -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 # Expect to see a message that grade report is being generated
expected_msg = "The grade report is being created." \ expected_msg = "The grade report is being created." \
" To view the status of the report, see" \ " To view the status of the report, see" \
" Pending Instructor Tasks below." " Pending Tasks below."
world.wait_for_visible('#report-request-response') world.wait_for_visible('#report-request-response')
assert_in( assert_in(
expected_msg, world.css_text('#report-request-response'), expected_msg, world.css_text('#report-request-response'),
......
...@@ -244,6 +244,7 @@ class TestInstructorAPIDenyLevels(SharedModuleStoreTestCase, LoginEnrollmentTest ...@@ -244,6 +244,7 @@ class TestInstructorAPIDenyLevels(SharedModuleStoreTestCase, LoginEnrollmentTest
('get_exec_summary_report', {}), ('get_exec_summary_report', {}),
('get_proctored_exam_results', {}), ('get_proctored_exam_results', {}),
('get_problem_responses', {}), ('get_problem_responses', {}),
('export_ora2_data', {}),
] ]
# Endpoints that only Instructors can access # Endpoints that only Instructors can access
self.instructor_level_endpoints = [ self.instructor_level_endpoints = [
...@@ -322,6 +323,8 @@ class TestInstructorAPIDenyLevels(SharedModuleStoreTestCase, LoginEnrollmentTest ...@@ -322,6 +323,8 @@ class TestInstructorAPIDenyLevels(SharedModuleStoreTestCase, LoginEnrollmentTest
# update_forum_role(self.course.id, staff_member, FORUM_ROLE_ADMINISTRATOR, 'allow') # update_forum_role(self.course.id, staff_member, FORUM_ROLE_ADMINISTRATOR, 'allow')
for endpoint, args in self.staff_level_endpoints: for endpoint, args in self.staff_level_endpoints:
expected_status = 200
# TODO: make these work # TODO: make these work
if endpoint in ['update_forum_role_membership', 'list_forum_members']: if endpoint in ['update_forum_role_membership', 'list_forum_members']:
continue continue
...@@ -333,7 +336,7 @@ class TestInstructorAPIDenyLevels(SharedModuleStoreTestCase, LoginEnrollmentTest ...@@ -333,7 +336,7 @@ class TestInstructorAPIDenyLevels(SharedModuleStoreTestCase, LoginEnrollmentTest
self._access_endpoint( self._access_endpoint(
endpoint, endpoint,
args, args,
200, expected_status,
"Staff member should be allowed to access endpoint " + endpoint "Staff member should be allowed to access endpoint " + endpoint
) )
...@@ -356,6 +359,8 @@ class TestInstructorAPIDenyLevels(SharedModuleStoreTestCase, LoginEnrollmentTest ...@@ -356,6 +359,8 @@ class TestInstructorAPIDenyLevels(SharedModuleStoreTestCase, LoginEnrollmentTest
self.client.login(username=inst.username, password='test') self.client.login(username=inst.username, password='test')
for endpoint, args in self.staff_level_endpoints: for endpoint, args in self.staff_level_endpoints:
expected_status = 200
# TODO: make these work # TODO: make these work
if endpoint in ['update_forum_role_membership']: if endpoint in ['update_forum_role_membership']:
continue continue
...@@ -367,18 +372,20 @@ class TestInstructorAPIDenyLevels(SharedModuleStoreTestCase, LoginEnrollmentTest ...@@ -367,18 +372,20 @@ class TestInstructorAPIDenyLevels(SharedModuleStoreTestCase, LoginEnrollmentTest
self._access_endpoint( self._access_endpoint(
endpoint, endpoint,
args, args,
200, expected_status,
"Instructor should be allowed to access endpoint " + endpoint "Instructor should be allowed to access endpoint " + endpoint
) )
for endpoint, args in self.instructor_level_endpoints: for endpoint, args in self.instructor_level_endpoints:
expected_status = 200
# TODO: make this work # TODO: make this work
if endpoint in ['rescore_problem']: if endpoint in ['rescore_problem']:
continue continue
self._access_endpoint( self._access_endpoint(
endpoint, endpoint,
args, args,
200, expected_status,
"Instructor should be allowed to access endpoint " + endpoint "Instructor should be allowed to access endpoint " + endpoint
) )
...@@ -2866,8 +2873,7 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment ...@@ -2866,8 +2873,7 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment
response = self.client.get(url, {}) response = self.client.get(url, {})
success_status = "The {report_type} report is being created." \ success_status = "The {report_type} report is being created." \
" To view the status of the report, see Pending" \ " To view the status of the report, see Pending" \
" Instructor Tasks" \ " Tasks below".format(report_type=report_type)
" below".format(report_type=report_type)
self.assertIn(success_status, response.content) self.assertIn(success_status, response.content)
@ddt.data(*EXECUTIVE_SUMMARY_DATA) @ddt.data(*EXECUTIVE_SUMMARY_DATA)
...@@ -2888,12 +2894,30 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment ...@@ -2888,12 +2894,30 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment
mock.side_effect = AlreadyRunningError() mock.side_effect = AlreadyRunningError()
response = self.client.get(url, {}) response = self.client.get(url, {})
already_running_status = "The {report_type} report is currently being created." \ 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" \ " You will be able to download the report" \
" when it is" \ " when it is" \
" complete.".format(report_type=report_type) " complete.".format(report_type=report_type)
self.assertIn(already_running_status, response.content) 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): def test_get_student_progress_url(self):
""" Test that progress_url is in the successful response. """ """ Test that progress_url is in the successful response. """
url = reverse('get_student_progress_url', kwargs={'course_id': self.course.id.to_deprecated_string()}) url = reverse('get_student_progress_url', kwargs={'course_id': self.course.id.to_deprecated_string()})
......
...@@ -1290,12 +1290,12 @@ def get_students_features(request, course_id, csv=False): # pylint: disable=red ...@@ -1290,12 +1290,12 @@ def get_students_features(request, course_id, csv=False): # pylint: disable=red
try: try:
instructor_task.api.submit_calculate_students_features_csv(request, course_key, query_features) instructor_task.api.submit_calculate_students_features_csv(request, course_key, query_features)
success_status = _("The enrolled learner profile report is being created." 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}) return JsonResponse({"status": success_status})
except AlreadyRunningError: except AlreadyRunningError:
already_running_status = _( already_running_status = _(
"This enrollment report is currently being created." "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.") " You will be able to download the report when it is complete.")
return JsonResponse({"status": already_running_status}) return JsonResponse({"status": already_running_status})
...@@ -1320,13 +1320,13 @@ def get_students_who_may_enroll(request, course_id): ...@@ -1320,13 +1320,13 @@ def get_students_who_may_enroll(request, course_id):
success_status = _( success_status = _(
"The enrollment report is being created. This report contains" "The enrollment report is being created. This report contains"
" information about learners who can enroll in the course." " 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}) return JsonResponse({"status": success_status})
except AlreadyRunningError: except AlreadyRunningError:
already_running_status = _( already_running_status = _(
"This enrollment report is currently being created." "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." " You will be able to download the report when it is complete."
) )
return JsonResponse({"status": already_running_status}) return JsonResponse({"status": already_running_status})
...@@ -1420,11 +1420,11 @@ def get_enrollment_report(request, course_id): ...@@ -1420,11 +1420,11 @@ def get_enrollment_report(request, course_id):
try: try:
instructor_task.api.submit_detailed_enrollment_features_csv(request, course_key) instructor_task.api.submit_detailed_enrollment_features_csv(request, course_key)
success_status = _("The detailed enrollment report is being created." 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}) return JsonResponse({"status": success_status})
except AlreadyRunningError: except AlreadyRunningError:
already_running_status = _("The detailed enrollment report is being created." 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.") " You will be able to download the report when it is complete.")
return JsonResponse({ return JsonResponse({
"status": already_running_status "status": already_running_status
...@@ -1444,11 +1444,11 @@ def get_exec_summary_report(request, course_id): ...@@ -1444,11 +1444,11 @@ def get_exec_summary_report(request, course_id):
try: try:
instructor_task.api.submit_executive_summary_report(request, course_key) instructor_task.api.submit_executive_summary_report(request, course_key)
status_response = _("The executive summary report is being created." 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: except AlreadyRunningError:
status_response = _( status_response = _(
"The executive summary report is currently being created." "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." " You will be able to download the report when it is complete."
) )
return JsonResponse({ return JsonResponse({
...@@ -1468,11 +1468,11 @@ def get_course_survey_results(request, course_id): ...@@ -1468,11 +1468,11 @@ def get_course_survey_results(request, course_id):
try: try:
instructor_task.api.submit_course_survey_report(request, course_key) instructor_task.api.submit_course_survey_report(request, course_key)
status_response = _("The survey report is being created." 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: except AlreadyRunningError:
status_response = _( status_response = _(
"The survey report is currently being created." "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." " You will be able to download the report when it is complete."
) )
return JsonResponse({ return JsonResponse({
...@@ -1503,11 +1503,11 @@ def get_proctored_exam_results(request, course_id): ...@@ -1503,11 +1503,11 @@ def get_proctored_exam_results(request, course_id):
try: try:
instructor_task.api.submit_proctored_exam_results_report(request, course_key, query_features) instructor_task.api.submit_proctored_exam_results_report(request, course_key, query_features)
status_response = _("The proctored exam results report is being created." 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: except AlreadyRunningError:
status_response = _( status_response = _(
"The proctored exam results report is currently being created." "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." " You will be able to download the report when it is complete."
) )
return JsonResponse({ return JsonResponse({
...@@ -2331,6 +2331,31 @@ def list_financial_report_downloads(_request, course_id): ...@@ -2331,6 +2331,31 @@ def list_financial_report_downloads(_request, course_id):
@ensure_csrf_cookie @ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True) @cache_control(no_cache=True, no_store=True, must_revalidate=True)
@require_level('staff') @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)
@require_level('staff')
def calculate_grades_csv(request, course_id): def calculate_grades_csv(request, course_id):
""" """
AlreadyRunningError is raised if the course's grades are already being updated. AlreadyRunningError is raised if the course's grades are already being updated.
...@@ -2339,15 +2364,13 @@ def calculate_grades_csv(request, course_id): ...@@ -2339,15 +2364,13 @@ def calculate_grades_csv(request, course_id):
try: try:
instructor_task.api.submit_calculate_grades_csv(request, course_key) instructor_task.api.submit_calculate_grades_csv(request, course_key)
success_status = _("The grade report is being created." 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}) return JsonResponse({"status": success_status})
except AlreadyRunningError: except AlreadyRunningError:
already_running_status = _("The grade report is currently being created." 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.") " You will be able to download the report when it is complete.")
return JsonResponse({ return JsonResponse({"status": already_running_status})
"status": already_running_status
})
@transaction.non_atomic_requests @transaction.non_atomic_requests
...@@ -2366,11 +2389,11 @@ def problem_grade_report(request, course_id): ...@@ -2366,11 +2389,11 @@ def problem_grade_report(request, course_id):
try: try:
instructor_task.api.submit_problem_grade_report(request, course_key) instructor_task.api.submit_problem_grade_report(request, course_key)
success_status = _("The problem grade report is being created." 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}) return JsonResponse({"status": success_status})
except AlreadyRunningError: except AlreadyRunningError:
already_running_status = _("A problem grade report is already being generated." 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.") " You will be able to download the report when it is complete.")
return JsonResponse({ return JsonResponse({
"status": already_running_status "status": already_running_status
......
...@@ -117,6 +117,8 @@ urlpatterns = patterns( ...@@ -117,6 +117,8 @@ urlpatterns = patterns(
'instructor.views.api.get_exec_summary_report', name="get_exec_summary_report"), 'instructor.views.api.get_exec_summary_report', name="get_exec_summary_report"),
url(r'get_course_survey_results$', url(r'get_course_survey_results$',
'instructor.views.api.get_course_survey_results', name="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.. # Coupon Codes..
url(r'get_coupon_codes', url(r'get_coupon_codes',
......
...@@ -567,6 +567,7 @@ def _section_data_download(course, access): ...@@ -567,6 +567,7 @@ def _section_data_download(course, access):
'problem_grade_report_url': reverse('problem_grade_report', kwargs={'course_id': unicode(course_key)}), '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_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)}), '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 return section_data
......
...@@ -28,7 +28,8 @@ from instructor_task.tasks import ( ...@@ -28,7 +28,8 @@ from instructor_task.tasks import (
exec_summary_report_csv, exec_summary_report_csv,
course_survey_report_csv, course_survey_report_csv,
generate_certificates, generate_certificates,
proctored_exam_results_csv proctored_exam_results_csv,
export_ora2_data,
) )
from certificates.models import CertificateGenerationHistory from certificates.models import CertificateGenerationHistory
...@@ -424,6 +425,18 @@ def submit_cohort_students(request, course_key, file_name): ...@@ -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) 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 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 Submits a task to generate certificates for given students enrolled in the course or
......
...@@ -44,7 +44,8 @@ from instructor_task.tasks_helper import ( ...@@ -44,7 +44,8 @@ from instructor_task.tasks_helper import (
upload_exec_summary_report, upload_exec_summary_report,
upload_course_survey_report, upload_course_survey_report,
generate_students_certificates, generate_students_certificates,
upload_proctored_exam_results_report upload_proctored_exam_results_report,
upload_ora2_data,
) )
...@@ -290,3 +291,13 @@ def cohort_students(entry_id, xmodule_instance_args): ...@@ -290,3 +291,13 @@ def cohort_students(entry_id, xmodule_instance_args):
action_name = ugettext_noop('cohorted') action_name = ugettext_noop('cohorted')
task_fn = partial(cohort_students_and_upload, xmodule_instance_args) task_fn = partial(cohort_students_and_upload, xmodule_instance_args)
return run_main_task(entry_id, task_fn, action_name) return run_main_task(entry_id, task_fn, action_name)
@task(base=BaseInstructorTask, routing_key=settings.GRADES_DOWNLOAD_ROUTING_KEY) # pylint: disable=not-callable
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)
...@@ -57,6 +57,7 @@ from instructor_analytics.basic import ( ...@@ -57,6 +57,7 @@ from instructor_analytics.basic import (
list_problem_responses list_problem_responses
) )
from instructor_analytics.csvs import format_dictlist from instructor_analytics.csvs import format_dictlist
from openassessment.data import OraAggregateData
from instructor_task.models import ReportStore, InstructorTask, PROGRESS from instructor_task.models import ReportStore, InstructorTask, PROGRESS
from lms.djangoapps.lms_xblock.runtime import LmsPartitionService from lms.djangoapps.lms_xblock.runtime import LmsPartitionService
from openedx.core.djangoapps.course_groups.cohorts import get_cohort from openedx.core.djangoapps.course_groups.cohorts import get_cohort
...@@ -1599,3 +1600,70 @@ def invalidate_generated_certificates(course_id, enrolled_students, certificate_ ...@@ -1599,3 +1600,70 @@ def invalidate_generated_certificates(course_id, enrolled_students, certificate_
download_url='', download_url='',
grade='', grade='',
) )
def upload_ora2_data(
_xmodule_instance_args, _entry_id, course_id, _task_input, action_name
):
"""
Collect ora2 responses and upload them to S3 as a CSV
"""
start_date = datetime.now(UTC)
start_time = time()
num_attempted = 1
num_total = 1
fmt = u'Task: {task_id}, InstructorTask ID: {entry_id}, Course: {course_id}, Input: {task_input}'
task_info_string = fmt.format(
task_id=_xmodule_instance_args.get('task_id') if _xmodule_instance_args is not None else None,
entry_id=_entry_id,
course_id=course_id,
task_input=_task_input
)
TASK_LOG.info(u'%s, Task type: %s, Starting task execution', task_info_string, action_name)
task_progress = TaskProgress(action_name, num_total, start_time)
task_progress.attempted = num_attempted
curr_step = {'step': "Collecting responses"}
TASK_LOG.info(
u'%s, Task type: %s, Current step: %s for all submissions',
task_info_string,
action_name,
curr_step,
)
task_progress.update_task_state(extra_meta=curr_step)
try:
header, datarows = OraAggregateData.collect_ora2_data(course_id)
rows = [header] + [row for row in datarows]
# Update progress to failed regardless of error type
except Exception: # pylint: disable=broad-except
TASK_LOG.exception('Failed to get ORA data.')
task_progress.failed = 1
curr_step = {'step': "Error while collecting data"}
task_progress.update_task_state(extra_meta=curr_step)
return UPDATE_STATUS_FAILED
task_progress.succeeded = 1
curr_step = {'step': "Uploading CSV"}
TASK_LOG.info(
u'%s, Task type: %s, Current step: %s',
task_info_string,
action_name,
curr_step,
)
task_progress.update_task_state(extra_meta=curr_step)
upload_csv_to_report_store(rows, 'ORA_data', course_id, start_date)
curr_step = {'step': 'Finalizing ORA data report'}
task_progress.update_task_state(extra_meta=curr_step)
TASK_LOG.info(u'%s, Task type: %s, Upload complete.', task_info_string, action_name)
return UPDATE_STATUS_SUCCEEDED
""" """
Test for LMS instructor background task queue management Test for LMS instructor background task queue management
""" """
from mock import patch, Mock from mock import patch, Mock, MagicMock
from bulk_email.models import CourseEmail, SEND_TO_ALL from bulk_email.models import CourseEmail, SEND_TO_ALL
from courseware.tests.factories import UserFactory from courseware.tests.factories import UserFactory
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
...@@ -22,16 +22,20 @@ from instructor_task.api import ( ...@@ -22,16 +22,20 @@ from instructor_task.api import (
submit_executive_summary_report, submit_executive_summary_report,
submit_course_survey_report, submit_course_survey_report,
generate_certificates_for_students, generate_certificates_for_students,
regenerate_certificates regenerate_certificates,
submit_export_ora2_data,
) )
from instructor_task.api_helper import AlreadyRunningError from instructor_task.api_helper import AlreadyRunningError
from instructor_task.models import InstructorTask, PROGRESS from instructor_task.models import InstructorTask, PROGRESS
from instructor_task.tests.test_base import (InstructorTaskTestCase, from instructor_task.tasks import export_ora2_data
InstructorTaskCourseTestCase, from instructor_task.tests.test_base import (
InstructorTaskModuleTestCase, InstructorTaskTestCase,
TestReportMixin, InstructorTaskCourseTestCase,
TEST_COURSE_KEY) InstructorTaskModuleTestCase,
TestReportMixin,
TEST_COURSE_KEY,
)
from certificates.models import CertificateStatuses, CertificateGenerationHistory from certificates.models import CertificateStatuses, CertificateGenerationHistory
...@@ -256,6 +260,16 @@ class InstructorTaskCourseSubmitTest(TestReportMixin, InstructorTaskCourseTestCa ...@@ -256,6 +260,16 @@ class InstructorTaskCourseSubmitTest(TestReportMixin, InstructorTaskCourseTestCa
) )
self._test_resubmission(api_call) self._test_resubmission(api_call)
def test_submit_ora2_request_task(self):
request = self.create_task_request(self.instructor)
with patch('instructor_task.api.submit_task') as mock_submit_task:
mock_submit_task.return_value = MagicMock()
submit_export_ora2_data(request, self.course.id)
mock_submit_task.assert_called_once_with(
request, 'export_ora2_data', export_ora2_data, self.course.id, {}, '')
def test_submit_generate_certs_students(self): def test_submit_generate_certs_students(self):
""" """
Tests certificates generation task submission api Tests certificates generation task submission api
......
...@@ -11,6 +11,8 @@ from uuid import uuid4 ...@@ -11,6 +11,8 @@ from uuid import uuid4
from mock import Mock, MagicMock, patch from mock import Mock, MagicMock, patch
from celery.states import SUCCESS, FAILURE from celery.states import SUCCESS, FAILURE
from django.utils.translation import ugettext_noop
from functools import partial
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from opaque_keys.edx.locations import i4xEncoder from opaque_keys.edx.locations import i4xEncoder
...@@ -27,8 +29,12 @@ from instructor_task.tasks import ( ...@@ -27,8 +29,12 @@ from instructor_task.tasks import (
reset_problem_attempts, reset_problem_attempts,
delete_problem_state, delete_problem_state,
generate_certificates, generate_certificates,
export_ora2_data,
)
from instructor_task.tasks_helper import (
UpdateProblemModuleStateError,
upload_ora2_data,
) )
from instructor_task.tasks_helper import UpdateProblemModuleStateError
PROBLEM_URL_NAME = "test_urlname" PROBLEM_URL_NAME = "test_urlname"
...@@ -471,3 +477,31 @@ class TestCertificateGenerationnstructorTask(TestInstructorTasks): ...@@ -471,3 +477,31 @@ class TestCertificateGenerationnstructorTask(TestInstructorTasks):
expected_attempted=1, expected_attempted=1,
expected_total=1 expected_total=1
) )
class TestOra2ResponsesInstructorTask(TestInstructorTasks):
"""Tests instructor task that fetches ora2 response data."""
def test_ora2_missing_current_task(self):
self._test_missing_current_task(export_ora2_data)
def test_ora2_with_failure(self):
self._test_run_with_failure(export_ora2_data, 'We expected this to fail')
def test_ora2_with_long_error_msg(self):
self._test_run_with_long_error_msg(export_ora2_data)
def test_ora2_with_short_error_msg(self):
self._test_run_with_short_error_msg(export_ora2_data)
def test_ora2_runs_task(self):
task_entry = self._create_input_entry()
task_xmodule_args = self._get_xmodule_instance_args()
with patch('instructor_task.tasks.run_main_task') as mock_main_task:
export_ora2_data(task_entry.id, task_xmodule_args)
action_name = ugettext_noop('generated')
task_fn = partial(upload_ora2_data, task_xmodule_args)
mock_main_task.assert_called_once_with_args(task_entry.id, task_fn, action_name)
...@@ -3,10 +3,18 @@ ...@@ -3,10 +3,18 @@
""" """
Unit tests for LMS instructor-initiated background tasks helper functions. Unit tests for LMS instructor-initiated background tasks helper functions.
Tests that CSV grade report generation works with unicode emails. - Tests that CSV grade report generation works with unicode emails.
- Tests all of the existing reports.
""" """
import os
import shutil
from datetime import datetime
import urllib
import ddt import ddt
from freezegun import freeze_time
from mock import Mock, patch from mock import Mock, patch
import tempfile import tempfile
import json import json
...@@ -22,6 +30,15 @@ from course_modes.models import CourseMode ...@@ -22,6 +30,15 @@ from course_modes.models import CourseMode
from courseware.tests.factories import InstructorFactory from courseware.tests.factories import InstructorFactory
from instructor_task.tests.test_base import InstructorTaskCourseTestCase, TestReportMixin, InstructorTaskModuleTestCase from instructor_task.tests.test_base import InstructorTaskCourseTestCase, TestReportMixin, InstructorTaskModuleTestCase
from openedx.core.djangoapps.course_groups.models import CourseUserGroupPartitionGroup, CohortMembership from openedx.core.djangoapps.course_groups.models import CourseUserGroupPartitionGroup, CohortMembership
from django.conf import settings
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from pytz import UTC
from xmodule.modulestore.tests.factories import CourseFactory
from student.tests.factories import UserFactory
from student.models import CourseEnrollment
from xmodule.partitions.partitions import Group, UserPartition
from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory
import openedx.core.djangoapps.user_api.course_tag.api as course_tag_api import openedx.core.djangoapps.user_api.course_tag.api as course_tag_api
from openedx.core.djangoapps.user_api.partition_schemes import RandomUserPartitionScheme from openedx.core.djangoapps.user_api.partition_schemes import RandomUserPartitionScheme
...@@ -45,6 +62,9 @@ from instructor_task.tasks_helper import ( ...@@ -45,6 +62,9 @@ from instructor_task.tasks_helper import (
upload_exec_summary_report, upload_exec_summary_report,
upload_course_survey_report, upload_course_survey_report,
generate_students_certificates, generate_students_certificates,
upload_ora2_data,
UPDATE_STATUS_FAILED,
UPDATE_STATUS_SUCCEEDED,
) )
from instructor_analytics.basic import UNAVAILABLE from instructor_analytics.basic import UNAVAILABLE
from openedx.core.djangoapps.util.testing import ContentGroupTestCase, TestConditionalContent from openedx.core.djangoapps.util.testing import ContentGroupTestCase, TestConditionalContent
...@@ -2012,3 +2032,56 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase): ...@@ -2012,3 +2032,56 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
}, },
result result
) )
class TestInstructorOra2Report(SharedModuleStoreTestCase):
"""
Tests that ORA2 response report generation works.
"""
@classmethod
def setUpClass(cls):
super(TestInstructorOra2Report, cls).setUpClass()
cls.course = CourseFactory.create()
def setUp(self):
super(TestInstructorOra2Report, self).setUp()
self.current_task = Mock()
self.current_task.update_state = Mock()
def tearDown(self):
super(TestInstructorOra2Report, self).tearDown()
if os.path.exists(settings.GRADES_DOWNLOAD['ROOT_PATH']):
shutil.rmtree(settings.GRADES_DOWNLOAD['ROOT_PATH'])
def test_report_fails_if_error(self):
with patch('instructor_task.tasks_helper.OraAggregateData.collect_ora2_data') as mock_collect_data:
mock_collect_data.side_effect = KeyError
with patch('instructor_task.tasks_helper._get_current_task') as mock_current_task:
mock_current_task.return_value = self.current_task
response = upload_ora2_data(None, None, self.course.id, None, 'generated')
self.assertEqual(response, UPDATE_STATUS_FAILED)
@freeze_time('2001-01-01 00:00:00')
def test_report_stores_results(self):
test_header = ['field1', 'field2']
test_rows = [['row1_field1', 'row1_field2'], ['row2_field1', 'row2_field2']]
with patch('instructor_task.tasks_helper._get_current_task') as mock_current_task:
mock_current_task.return_value = self.current_task
with patch('instructor_task.tasks_helper.OraAggregateData.collect_ora2_data') as mock_collect_data:
mock_collect_data.return_value = (test_header, test_rows)
with patch('instructor_task.models.LocalFSReportStore.store_rows') as mock_store_rows:
return_val = upload_ora2_data(None, None, self.course.id, None, 'generated')
# pylint: disable=maybe-no-member
timestamp_str = datetime.now(UTC).strftime('%Y-%m-%d-%H%M')
course_id_string = urllib.quote(self.course.id.to_deprecated_string().replace('/', '_'))
filename = u'{}_ORA_data_{}.csv'.format(course_id_string, timestamp_str)
self.assertEqual(return_val, UPDATE_STATUS_SUCCEEDED)
mock_store_rows.assert_called_once_with(self.course.id, filename, [test_header] + test_rows)
...@@ -2230,6 +2230,8 @@ BADGR_BASE_URL = "http://localhost:8005" ...@@ -2230,6 +2230,8 @@ BADGR_BASE_URL = "http://localhost:8005"
BADGR_ISSUER_SLUG = "example-issuer" BADGR_ISSUER_SLUG = "example-issuer"
###################### Grade Downloads ###################### ###################### Grade Downloads ######################
# These keys are used for all of our asynchronous downloadable files, including
# the ones that contain information other than grades.
GRADES_DOWNLOAD_ROUTING_KEY = HIGH_MEM_QUEUE GRADES_DOWNLOAD_ROUTING_KEY = HIGH_MEM_QUEUE
GRADES_DOWNLOAD = { GRADES_DOWNLOAD = {
...@@ -2244,7 +2246,6 @@ FINANCIAL_REPORTS = { ...@@ -2244,7 +2246,6 @@ FINANCIAL_REPORTS = {
'ROOT_PATH': '/tmp/edx-s3/financial_reports', 'ROOT_PATH': '/tmp/edx-s3/financial_reports',
} }
#### PASSWORD POLICY SETTINGS ##### #### PASSWORD POLICY SETTINGS #####
PASSWORD_MIN_LENGTH = 8 PASSWORD_MIN_LENGTH = 8
PASSWORD_MAX_LENGTH = None PASSWORD_MAX_LENGTH = None
......
...@@ -83,6 +83,7 @@ class DataDownload ...@@ -83,6 +83,7 @@ class DataDownload
@$grade_config_btn = @$section.find("input[name='dump-gradeconf']'") @$grade_config_btn = @$section.find("input[name='dump-gradeconf']'")
@$calculate_grades_csv_btn = @$section.find("input[name='calculate-grades-csv']'") @$calculate_grades_csv_btn = @$section.find("input[name='calculate-grades-csv']'")
@$problem_grade_report_csv_btn = @$section.find("input[name='problem-grade-report']'") @$problem_grade_report_csv_btn = @$section.find("input[name='problem-grade-report']'")
@$async_report_btn = @$section.find("input[class='async-report-btn']'")
# response areas # response areas
@$download = @$section.find '.data-download-container' @$download = @$section.find '.data-download-container'
...@@ -236,27 +237,26 @@ class DataDownload ...@@ -236,27 +237,26 @@ class DataDownload
@clear_display() @clear_display()
@$download_display_text.html data['grading_config_summary'] @$download_display_text.html data['grading_config_summary']
@$calculate_grades_csv_btn.click (e) => @$async_report_btn.click (e) =>
@onClickGradeDownload @$calculate_grades_csv_btn, gettext("Error generating grades. Please try again.") # Clear any CSS styling from the request-response areas
#$(".msg-confirm").css({"display":"none"})
@$problem_grade_report_csv_btn.click (e) => #$(".msg-error").css({"display":"none"})
@onClickGradeDownload @$problem_grade_report_csv_btn, gettext("Error generating problem grade report. Please try again.") @clear_display()
url = $(e.target).data 'endpoint'
onClickGradeDownload: (button, errorMessage) -> $.ajax
# Clear any CSS styling from the request-response areas dataType: 'json'
#$(".msg-confirm").css({"display":"none"}) url: url
#$(".msg-error").css({"display":"none"}) error: std_ajax_err =>
@clear_display() if e.target.name == 'calculate-grades-csv'
url = button.data 'endpoint' @$grades_request_response_error.text gettext("Error generating grades. Please try again.")
$.ajax else if e.target.name == 'problem-grade-report'
dataType: 'json' @$grades_request_response_error.text gettext("Error generating problem grade report. Please try again.")
url: url else if e.target.name == 'export-ora2-data'
error: (std_ajax_err) => @$grades_request_response_error.text gettext("Error generating ORA data report. Please try again.")
@$reports_request_response_error.text errorMessage $(".msg-error").css({"display":"block"})
$(".msg-error").css({"display":"block"}) success: (data) =>
success: (data) => @$reports_request_response.text data['status']
@$reports_request_response.text data['status'] $(".msg-confirm").css({"display":"block"})
$(".msg-confirm").css({"display":"block"})
# handler for when the section title is clicked. # handler for when the section title is clicked.
onClickTitle: -> onClickTitle: ->
......
...@@ -75,10 +75,11 @@ ...@@ -75,10 +75,11 @@
%if settings.FEATURES.get('ALLOW_COURSE_STAFF_GRADE_DOWNLOADS') or section_data['access']['admin']: %if settings.FEATURES.get('ALLOW_COURSE_STAFF_GRADE_DOWNLOADS') or section_data['access']['admin']:
<p>${_("Click to generate a CSV grade report for all currently enrolled students.")}</p> <p>${_("Click to generate a CSV grade report for all currently enrolled students.")}</p>
<p>
<p><input type="button" name="calculate-grades-csv" value="${_("Generate Grade Report")}" data-endpoint="${ section_data['calculate_grades_csv_url'] }"/></p> <input type="button" name="calculate-grades-csv" class="async-report-btn" value="${_("Generate Grade Report")}" data-endpoint="${ section_data['calculate_grades_csv_url'] }"/>
<input type="button" name="problem-grade-report" class="async-report-btn" value="${_("Generate Problem Grade Report")}" data-endpoint="${ section_data['problem_grade_report_url'] }"/>
<p><input type="button" name="problem-grade-report" value="${_("Generate Problem Grade Report")}" data-endpoint="${ section_data['problem_grade_report_url'] }"/></p> <input type="button" name="export-ora2-data" class="async-report-btn" value="${_("Generate ORA Data Report")}" data-endpoint="${ section_data['export_ora2_data_url'] }"/>
</p>
%endif %endif
<div class="request-response msg msg-confirm copy" id="report-request-response"></div> <div class="request-response msg msg-confirm copy" id="report-request-response"></div>
......
...@@ -76,8 +76,8 @@ git+https://github.com/edx/XBlock.git@xblock-0.4.5#egg=XBlock==0.4.5 ...@@ -76,8 +76,8 @@ git+https://github.com/edx/XBlock.git@xblock-0.4.5#egg=XBlock==0.4.5
-e git+https://github.com/edx/event-tracking.git@0.2.1#egg=event-tracking==0.2.1 -e git+https://github.com/edx/event-tracking.git@0.2.1#egg=event-tracking==0.2.1
-e git+https://github.com/edx/django-splash.git@v0.2#egg=django-splash==0.2 -e git+https://github.com/edx/django-splash.git@v0.2#egg=django-splash==0.2
-e git+https://github.com/edx/acid-block.git@e46f9cda8a03e121a00c7e347084d142d22ebfb7#egg=acid-xblock -e git+https://github.com/edx/acid-block.git@e46f9cda8a03e121a00c7e347084d142d22ebfb7#egg=acid-xblock
git+https://github.com/edx/edx-ora2.git@1.0.1#egg=ora2==1.0.1 git+https://github.com/edx/edx-ora2.git@1.1.0#egg=ora2==1.1.0
-e git+https://github.com/edx/edx-submissions.git@1.0.0#egg=edx-submissions==1.0.0 -e git+https://github.com/edx/edx-submissions.git@1.1.0#egg=edx-submissions==1.1.0
git+https://github.com/edx/ease.git@release-2015-07-14#egg=ease==0.1.3 git+https://github.com/edx/ease.git@release-2015-07-14#egg=ease==0.1.3
git+https://github.com/edx/i18n-tools.git@v0.2#egg=i18n-tools==v0.2 git+https://github.com/edx/i18n-tools.git@v0.2#egg=i18n-tools==v0.2
git+https://github.com/edx/edx-val.git@0.0.9#egg=edxval==0.0.9 git+https://github.com/edx/edx-val.git@0.0.9#egg=edxval==0.0.9
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment