Add new functionality to generate ora summary report
This commit is contained in:
@@ -154,6 +154,7 @@ INSTRUCTOR_POST_ENDPOINTS = {
|
||||
'change_due_date',
|
||||
'export_ora2_data',
|
||||
'export_ora2_submission_files',
|
||||
'export_ora2_summary',
|
||||
'get_grading_config',
|
||||
'get_problem_responses',
|
||||
'get_proctored_exam_results',
|
||||
@@ -426,6 +427,7 @@ class TestInstructorAPIDenyLevels(SharedModuleStoreTestCase, LoginEnrollmentTest
|
||||
('get_problem_responses', {}),
|
||||
('export_ora2_data', {}),
|
||||
('export_ora2_submission_files', {}),
|
||||
('export_ora2_summary', {}),
|
||||
('rescore_problem',
|
||||
{'problem_to_reset': self.problem_urlname, 'unique_student_identifier': self.user.email}),
|
||||
('override_problem_score',
|
||||
@@ -2853,6 +2855,26 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment
|
||||
|
||||
self.assertContains(response, already_running_status, status_code=400)
|
||||
|
||||
def test_get_ora2_summary_responses_success(self):
|
||||
url = reverse('export_ora2_summary', kwargs={'course_id': str(self.course.id)})
|
||||
|
||||
with patch('lms.djangoapps.instructor_task.api.submit_export_ora2_summary') as mock_submit_ora2_task:
|
||||
mock_submit_ora2_task.return_value = True
|
||||
response = self.client.post(url, {})
|
||||
success_status = "The ORA summary report is being created."
|
||||
self.assertContains(response, success_status)
|
||||
|
||||
def test_get_ora2_summary_responses_already_running(self):
|
||||
url = reverse('export_ora2_summary', kwargs={'course_id': str(self.course.id)})
|
||||
task_type = 'export_ora2_summary'
|
||||
already_running_status = generate_already_running_error_message(task_type)
|
||||
|
||||
with patch('lms.djangoapps.instructor_task.api.submit_export_ora2_summary') as mock_submit_ora2_task:
|
||||
mock_submit_ora2_task.side_effect = AlreadyRunningError(already_running_status)
|
||||
response = self.client.post(url, {})
|
||||
|
||||
self.assertContains(response, already_running_status, status_code=400)
|
||||
|
||||
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': str(self.course.id)})
|
||||
|
||||
@@ -2028,6 +2028,24 @@ def export_ora2_data(request, course_id):
|
||||
return JsonResponse({"status": success_status})
|
||||
|
||||
|
||||
@transaction.non_atomic_requests
|
||||
@require_POST
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_course_permission(permissions.CAN_RESEARCH)
|
||||
@common_exceptions_400
|
||||
def export_ora2_summary(request, course_id):
|
||||
"""
|
||||
Pushes a Celery task which will aggregate a summary students' progress in ora2 tasks for a course into a .csv
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
report_type = _('ORA summary')
|
||||
task_api.submit_export_ora2_summary(request, course_key)
|
||||
success_status = SUCCESS_MESSAGE_TEMPLATE.format(report_type=report_type)
|
||||
|
||||
return JsonResponse({"status": success_status})
|
||||
|
||||
|
||||
@transaction.non_atomic_requests
|
||||
@require_POST
|
||||
@ensure_csrf_cookie
|
||||
|
||||
@@ -53,6 +53,7 @@ urlpatterns = [
|
||||
# Reports..
|
||||
url(r'^get_course_survey_results$', api.get_course_survey_results, name='get_course_survey_results'),
|
||||
url(r'^export_ora2_data', api.export_ora2_data, name='export_ora2_data'),
|
||||
url(r'^export_ora2_summary', api.export_ora2_summary, name='export_ora2_summary'),
|
||||
|
||||
url(r'^export_ora2_submission_files', api.export_ora2_submission_files,
|
||||
name='export_ora2_submission_files'),
|
||||
|
||||
@@ -633,6 +633,7 @@ def _section_data_download(course, access):
|
||||
'export_ora2_submission_files_url': reverse(
|
||||
'export_ora2_submission_files', kwargs={'course_id': str(course_key)}
|
||||
),
|
||||
'export_ora2_summary_url': reverse('export_ora2_summary', kwargs={'course_id': str(course_key)}),
|
||||
}
|
||||
if not access.get('data_researcher'):
|
||||
section_data['is_hidden'] = True
|
||||
|
||||
@@ -36,6 +36,7 @@ from lms.djangoapps.instructor_task.tasks import (
|
||||
delete_problem_state,
|
||||
export_ora2_data,
|
||||
export_ora2_submission_files,
|
||||
export_ora2_summary,
|
||||
generate_certificates,
|
||||
override_problem_score,
|
||||
proctored_exam_results_csv,
|
||||
@@ -463,6 +464,18 @@ def submit_export_ora2_submission_files(request, course_key):
|
||||
return submit_task(request, task_type, task_class, course_key, task_input, task_key)
|
||||
|
||||
|
||||
def submit_export_ora2_summary(request, course_key):
|
||||
"""
|
||||
AlreadyRunningError is raised if an ora2 report is already being generated.
|
||||
"""
|
||||
task_type = 'export_ora2_summary'
|
||||
task_class = export_ora2_summary
|
||||
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, student_set=None, specific_student_id=None):
|
||||
"""
|
||||
Submits a task to generate certificates for given students enrolled in the course.
|
||||
|
||||
@@ -38,6 +38,7 @@ from lms.djangoapps.instructor_task.tasks_helper.misc import (
|
||||
upload_course_survey_report,
|
||||
upload_ora2_data,
|
||||
upload_ora2_submission_files,
|
||||
upload_ora2_summary,
|
||||
upload_proctored_exam_results_report
|
||||
)
|
||||
from lms.djangoapps.instructor_task.tasks_helper.module_state import (
|
||||
@@ -316,3 +317,14 @@ def export_ora2_submission_files(entry_id, xmodule_instance_args):
|
||||
action_name = ugettext_noop('compressed')
|
||||
task_fn = partial(upload_ora2_submission_files, xmodule_instance_args)
|
||||
return run_main_task(entry_id, task_fn, action_name)
|
||||
|
||||
|
||||
@shared_task(base=BaseInstructorTask)
|
||||
@set_code_owner_attribute
|
||||
def export_ora2_summary(entry_id, xmodule_instance_args):
|
||||
"""
|
||||
Generate a CSV of ora2/student summaries and push it to S3.
|
||||
"""
|
||||
action_name = ugettext_noop('generated')
|
||||
task_fn = partial(upload_ora2_summary, xmodule_instance_args)
|
||||
return run_main_task(entry_id, task_fn, action_name)
|
||||
|
||||
@@ -275,6 +275,32 @@ def upload_ora2_data(
|
||||
Collect ora2 responses and upload them to S3 as a CSV
|
||||
"""
|
||||
|
||||
return _upload_ora2_data_common(
|
||||
_xmodule_instance_args, _entry_id, course_id, _task_input, action_name,
|
||||
'data', OraAggregateData.collect_ora2_data
|
||||
)
|
||||
|
||||
|
||||
def upload_ora2_summary(
|
||||
_xmodule_instance_args, _entry_id, course_id, _task_input, action_name
|
||||
):
|
||||
"""
|
||||
Collect ora2/student summaries and upload them to file storage as a CSV
|
||||
"""
|
||||
|
||||
return _upload_ora2_data_common(
|
||||
_xmodule_instance_args, _entry_id, course_id, _task_input, action_name,
|
||||
'summary', OraAggregateData.collect_ora2_summary
|
||||
)
|
||||
|
||||
|
||||
def _upload_ora2_data_common(
|
||||
_xmodule_instance_args, _entry_id, course_id, _task_input, action_name,
|
||||
report_name, csv_gen_func
|
||||
):
|
||||
"""
|
||||
Common code for uploading data or summary csv report.
|
||||
"""
|
||||
start_date = datetime.now(UTC)
|
||||
start_time = time()
|
||||
|
||||
@@ -304,8 +330,10 @@ def upload_ora2_data(
|
||||
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] # lint-amnesty, pylint: disable=unnecessary-comprehension
|
||||
header, datarows = csv_gen_func(course_id)
|
||||
rows = [header]
|
||||
for row in datarows:
|
||||
rows.append(row)
|
||||
# Update progress to failed regardless of error type
|
||||
except Exception: # pylint: disable=broad-except
|
||||
TASK_LOG.exception('Failed to get ORA data.')
|
||||
@@ -326,9 +354,9 @@ def upload_ora2_data(
|
||||
)
|
||||
task_progress.update_task_state(extra_meta=curr_step)
|
||||
|
||||
upload_csv_to_report_store(rows, 'ORA_data', course_id, start_date)
|
||||
upload_csv_to_report_store(rows, 'ORA_{}'.format(report_name), course_id, start_date)
|
||||
|
||||
curr_step = {'step': 'Finalizing ORA data report'}
|
||||
curr_step = {'step': 'Finalizing ORA {} report'.format(report_name)}
|
||||
task_progress.update_task_state(extra_meta=curr_step)
|
||||
TASK_LOG.info('%s, Task type: %s, Upload complete.', task_info_string, action_name)
|
||||
|
||||
|
||||
@@ -25,13 +25,12 @@ from lms.djangoapps.instructor_task.tasks import (
|
||||
delete_problem_state,
|
||||
export_ora2_data,
|
||||
export_ora2_submission_files,
|
||||
export_ora2_summary,
|
||||
generate_certificates,
|
||||
override_problem_score,
|
||||
rescore_problem,
|
||||
reset_problem_attempts
|
||||
)
|
||||
from lms.djangoapps.instructor_task.tasks_helper.misc import \
|
||||
upload_ora2_data # lint-amnesty, pylint: disable=unused-import
|
||||
from lms.djangoapps.instructor_task.tests.factories import InstructorTaskFactory
|
||||
from lms.djangoapps.instructor_task.tests.test_base import InstructorTaskModuleTestCase
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
@@ -715,3 +714,33 @@ class TestOra2ExportSubmissionFilesInstructorTask(TestInstructorTasks):
|
||||
assert args[0] == task_entry.id
|
||||
assert callable(args[1])
|
||||
assert args[2] == action_name
|
||||
|
||||
|
||||
class TestOra2SummaryInstructorTask(TestInstructorTasks):
|
||||
"""Tests instructor task that fetches ora2 response summary."""
|
||||
|
||||
def test_ora2_missing_current_task(self):
|
||||
self._test_missing_current_task(export_ora2_summary)
|
||||
|
||||
def test_ora2_with_failure(self):
|
||||
self._test_run_with_failure(export_ora2_summary, 'We expected this to fail')
|
||||
|
||||
def test_ora2_with_long_error_msg(self):
|
||||
self._test_run_with_long_error_msg(export_ora2_summary)
|
||||
|
||||
def test_ora2_with_short_error_msg(self):
|
||||
self._test_run_with_short_error_msg(export_ora2_summary)
|
||||
|
||||
def test_ora2_runs_task(self):
|
||||
task_entry = self._create_input_entry()
|
||||
task_xmodule_args = self._get_xmodule_instance_args()
|
||||
|
||||
with patch('lms.djangoapps.instructor_task.tasks.run_main_task') as mock_main_task:
|
||||
export_ora2_summary(task_entry.id, task_xmodule_args)
|
||||
action_name = ugettext_noop('generated')
|
||||
|
||||
assert mock_main_task.call_count == 1
|
||||
args = mock_main_task.call_args[0]
|
||||
assert args[0] == task_entry.id
|
||||
assert callable(args[1])
|
||||
assert args[2] == action_name
|
||||
|
||||
@@ -50,7 +50,8 @@ from lms.djangoapps.instructor_task.tasks_helper.misc import (
|
||||
cohort_students_and_upload,
|
||||
upload_course_survey_report,
|
||||
upload_ora2_data,
|
||||
upload_ora2_submission_files
|
||||
upload_ora2_submission_files,
|
||||
upload_ora2_summary
|
||||
)
|
||||
from lms.djangoapps.instructor_task.tests.test_base import (
|
||||
InstructorTaskCourseTestCase,
|
||||
@@ -2537,6 +2538,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
|
||||
]
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestInstructorOra2Report(SharedModuleStoreTestCase):
|
||||
"""
|
||||
Tests that ORA2 response report generation works.
|
||||
@@ -2557,17 +2559,20 @@ class TestInstructorOra2Report(SharedModuleStoreTestCase):
|
||||
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(
|
||||
'lms.djangoapps.instructor_task.tasks_helper.misc.OraAggregateData.collect_ora2_data'
|
||||
) as mock_collect_data:
|
||||
@ddt.data(
|
||||
('lms.djangoapps.instructor_task.tasks_helper.misc.OraAggregateData.collect_ora2_data', upload_ora2_data),
|
||||
('lms.djangoapps.instructor_task.tasks_helper.misc.OraAggregateData.collect_ora2_summary', upload_ora2_summary),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_report_fails_if_error(self, data_collector_module, upload_func):
|
||||
with patch(data_collector_module) as mock_collect_data:
|
||||
mock_collect_data.side_effect = KeyError
|
||||
|
||||
with patch('lms.djangoapps.instructor_task.tasks_helper.runner._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')
|
||||
assert response == UPDATE_STATUS_FAILED
|
||||
response = upload_func(None, None, self.course.id, None, 'generated')
|
||||
self.assertEqual(response, UPDATE_STATUS_FAILED)
|
||||
|
||||
def test_report_stores_results(self):
|
||||
with ExitStack() as stack:
|
||||
@@ -2628,6 +2633,30 @@ class TestInstructorOra2AttachmentsExport(SharedModuleStoreTestCase):
|
||||
response = upload_ora2_submission_files(None, None, self.course.id, None, 'compressed')
|
||||
assert response == UPDATE_STATUS_FAILED
|
||||
|
||||
def test_summary_report_stores_results(self):
|
||||
with freeze_time('2001-01-01 00:00:00'):
|
||||
test_header = ['field1', 'field2']
|
||||
test_rows = [['row1_field1', 'row1_field2'], ['row2_field1', 'row2_field2']]
|
||||
|
||||
with patch('lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task') as mock_current_task:
|
||||
mock_current_task.return_value = self.current_task
|
||||
|
||||
with patch(
|
||||
'lms.djangoapps.instructor_task.tasks_helper.misc.OraAggregateData.collect_ora2_summary'
|
||||
) as mock_collect_summary:
|
||||
mock_collect_summary.return_value = (test_header, test_rows)
|
||||
with patch(
|
||||
'lms.djangoapps.instructor_task.models.DjangoStorageReportStore.store_rows'
|
||||
) as mock_store_rows:
|
||||
return_val = upload_ora2_summary(None, None, self.course.id, None, 'generated')
|
||||
|
||||
timestamp_str = datetime.now(UTC).strftime('%Y-%m-%d-%H%M')
|
||||
course_id_string = quote(str(self.course.id).replace('/', '_'))
|
||||
filename = '{}_ORA_summary_{}.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)
|
||||
|
||||
def test_export_fails_if_error_on_create_zip_step(self):
|
||||
with ExitStack() as stack:
|
||||
mock_current_task = stack.enter_context(
|
||||
|
||||
@@ -95,6 +95,7 @@ from openedx.core.djangolib.markup import HTML, Text
|
||||
<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'] }"/>
|
||||
<input type="button" name="export-ora2-data" class="async-report-btn" value="${_("Generate ORA Data Report")}" data-endpoint="${ section_data['export_ora2_data_url'] }"/>
|
||||
<input type="button" name="export-ora2-summary" class="async-report-btn" value="${_("Generate ORA Summary Report")}" data-endpoint="${ section_data['export_ora2_summary_url'] }"/>
|
||||
</p>
|
||||
|
||||
<p>${_("Click to generate a ZIP file that contains all submission texts and attachments.")}</p>
|
||||
|
||||
Reference in New Issue
Block a user