Asynchronous download button for ORA2 data
Conflicts: lms/djangoapps/instructor/tests/test_api.py lms/djangoapps/instructor/utils.py lms/djangoapps/instructor/views/api.py lms/djangoapps/instructor/views/api_urls.py lms/djangoapps/instructor/views/instructor_dashboard.py lms/djangoapps/instructor_task/api.py lms/djangoapps/instructor_task/tasks.py lms/djangoapps/instructor_task/tasks_helper.py lms/djangoapps/instructor_task/tests/test_api.py lms/djangoapps/instructor_task/tests/test_tasks.py lms/djangoapps/instructor_task/tests/test_tasks_helper.py lms/envs/aws.py lms/envs/common.py lms/static/coffee/src/instructor_dashboard/data_download.coffee lms/templates/instructor/instructor_dashboard_2/data_download.html
This commit is contained in:
committed by
Eric Fischer
parent
2c3b728c08
commit
2b1a7eece2
@@ -912,6 +912,13 @@ class DataDownloadPage(PageObject):
|
||||
"""
|
||||
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.
|
||||
|
||||
@@ -636,6 +636,20 @@ class DataDownloadsTest(BaseInstructorDashboardTest):
|
||||
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):
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -244,6 +244,7 @@ class TestInstructorAPIDenyLevels(SharedModuleStoreTestCase, LoginEnrollmentTest
|
||||
('get_exec_summary_report', {}),
|
||||
('get_proctored_exam_results', {}),
|
||||
('get_problem_responses', {}),
|
||||
('export_ora2_data', {}),
|
||||
]
|
||||
# Endpoints that only Instructors can access
|
||||
self.instructor_level_endpoints = [
|
||||
@@ -322,6 +323,8 @@ class TestInstructorAPIDenyLevels(SharedModuleStoreTestCase, LoginEnrollmentTest
|
||||
# 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
|
||||
@@ -333,7 +336,7 @@ class TestInstructorAPIDenyLevels(SharedModuleStoreTestCase, LoginEnrollmentTest
|
||||
self._access_endpoint(
|
||||
endpoint,
|
||||
args,
|
||||
200,
|
||||
expected_status,
|
||||
"Staff member should be allowed to access endpoint " + endpoint
|
||||
)
|
||||
|
||||
@@ -356,6 +359,8 @@ class TestInstructorAPIDenyLevels(SharedModuleStoreTestCase, LoginEnrollmentTest
|
||||
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
|
||||
@@ -367,18 +372,20 @@ class TestInstructorAPIDenyLevels(SharedModuleStoreTestCase, LoginEnrollmentTest
|
||||
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
|
||||
)
|
||||
|
||||
@@ -2866,8 +2873,7 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment
|
||||
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)
|
||||
@@ -2888,12 +2894,30 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment
|
||||
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()})
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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})
|
||||
@@ -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
|
||||
@@ -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({
|
||||
@@ -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({
|
||||
@@ -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({
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -117,6 +117,8 @@ urlpatterns = patterns(
|
||||
'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',
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -28,7 +28,8 @@ from instructor_task.tasks import (
|
||||
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
|
||||
@@ -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
|
||||
|
||||
@@ -44,7 +44,8 @@ from instructor_task.tasks_helper import (
|
||||
upload_exec_summary_report,
|
||||
upload_course_survey_report,
|
||||
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):
|
||||
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
|
||||
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 (
|
||||
list_problem_responses
|
||||
)
|
||||
from instructor_analytics.csvs import format_dictlist
|
||||
from openassessment.data import OraAggregateData
|
||||
from instructor_task.models import ReportStore, InstructorTask, PROGRESS
|
||||
from lms.djangoapps.lms_xblock.runtime import LmsPartitionService
|
||||
from openedx.core.djangoapps.course_groups.cohorts import get_cohort
|
||||
@@ -1599,3 +1600,70 @@ def invalidate_generated_certificates(course_id, enrolled_students, certificate_
|
||||
download_url='',
|
||||
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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
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 courseware.tests.factories import UserFactory
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
@@ -22,16 +22,20 @@ from instructor_task.api import (
|
||||
submit_executive_summary_report,
|
||||
submit_course_survey_report,
|
||||
generate_certificates_for_students,
|
||||
regenerate_certificates
|
||||
regenerate_certificates,
|
||||
submit_export_ora2_data,
|
||||
)
|
||||
|
||||
from instructor_task.api_helper import AlreadyRunningError
|
||||
from instructor_task.models import InstructorTask, PROGRESS
|
||||
from instructor_task.tests.test_base import (InstructorTaskTestCase,
|
||||
InstructorTaskCourseTestCase,
|
||||
InstructorTaskModuleTestCase,
|
||||
TestReportMixin,
|
||||
TEST_COURSE_KEY)
|
||||
from instructor_task.tasks import export_ora2_data
|
||||
from instructor_task.tests.test_base import (
|
||||
InstructorTaskTestCase,
|
||||
InstructorTaskCourseTestCase,
|
||||
InstructorTaskModuleTestCase,
|
||||
TestReportMixin,
|
||||
TEST_COURSE_KEY,
|
||||
)
|
||||
from certificates.models import CertificateStatuses, CertificateGenerationHistory
|
||||
|
||||
|
||||
@@ -256,6 +260,16 @@ class InstructorTaskCourseSubmitTest(TestReportMixin, InstructorTaskCourseTestCa
|
||||
)
|
||||
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):
|
||||
"""
|
||||
Tests certificates generation task submission api
|
||||
|
||||
@@ -11,6 +11,8 @@ from uuid import uuid4
|
||||
from mock import Mock, MagicMock, patch
|
||||
|
||||
from celery.states import SUCCESS, FAILURE
|
||||
from django.utils.translation import ugettext_noop
|
||||
from functools import partial
|
||||
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from opaque_keys.edx.locations import i4xEncoder
|
||||
@@ -27,8 +29,12 @@ from instructor_task.tasks import (
|
||||
reset_problem_attempts,
|
||||
delete_problem_state,
|
||||
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"
|
||||
|
||||
@@ -471,3 +477,31 @@ class TestCertificateGenerationnstructorTask(TestInstructorTasks):
|
||||
expected_attempted=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 @@
|
||||
"""
|
||||
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
|
||||
from freezegun import freeze_time
|
||||
from mock import Mock, patch
|
||||
import tempfile
|
||||
import json
|
||||
@@ -22,6 +30,15 @@ from course_modes.models import CourseMode
|
||||
from courseware.tests.factories import InstructorFactory
|
||||
from instructor_task.tests.test_base import InstructorTaskCourseTestCase, TestReportMixin, InstructorTaskModuleTestCase
|
||||
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
|
||||
import openedx.core.djangoapps.user_api.course_tag.api as course_tag_api
|
||||
from openedx.core.djangoapps.user_api.partition_schemes import RandomUserPartitionScheme
|
||||
@@ -45,6 +62,9 @@ from instructor_task.tasks_helper import (
|
||||
upload_exec_summary_report,
|
||||
upload_course_survey_report,
|
||||
generate_students_certificates,
|
||||
upload_ora2_data,
|
||||
UPDATE_STATUS_FAILED,
|
||||
UPDATE_STATUS_SUCCEEDED,
|
||||
)
|
||||
from instructor_analytics.basic import UNAVAILABLE
|
||||
from openedx.core.djangoapps.util.testing import ContentGroupTestCase, TestConditionalContent
|
||||
@@ -2012,3 +2032,56 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
|
||||
},
|
||||
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"
|
||||
BADGR_ISSUER_SLUG = "example-issuer"
|
||||
|
||||
###################### 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 = {
|
||||
@@ -2244,7 +2246,6 @@ FINANCIAL_REPORTS = {
|
||||
'ROOT_PATH': '/tmp/edx-s3/financial_reports',
|
||||
}
|
||||
|
||||
|
||||
#### PASSWORD POLICY SETTINGS #####
|
||||
PASSWORD_MIN_LENGTH = 8
|
||||
PASSWORD_MAX_LENGTH = None
|
||||
|
||||
@@ -83,6 +83,7 @@ class DataDownload
|
||||
@$grade_config_btn = @$section.find("input[name='dump-gradeconf']'")
|
||||
@$calculate_grades_csv_btn = @$section.find("input[name='calculate-grades-csv']'")
|
||||
@$problem_grade_report_csv_btn = @$section.find("input[name='problem-grade-report']'")
|
||||
@$async_report_btn = @$section.find("input[class='async-report-btn']'")
|
||||
|
||||
# response areas
|
||||
@$download = @$section.find '.data-download-container'
|
||||
@@ -236,27 +237,26 @@ class DataDownload
|
||||
@clear_display()
|
||||
@$download_display_text.html data['grading_config_summary']
|
||||
|
||||
@$calculate_grades_csv_btn.click (e) =>
|
||||
@onClickGradeDownload @$calculate_grades_csv_btn, gettext("Error generating grades. Please try again.")
|
||||
|
||||
@$problem_grade_report_csv_btn.click (e) =>
|
||||
@onClickGradeDownload @$problem_grade_report_csv_btn, gettext("Error generating problem grade report. Please try again.")
|
||||
|
||||
onClickGradeDownload: (button, errorMessage) ->
|
||||
# Clear any CSS styling from the request-response areas
|
||||
#$(".msg-confirm").css({"display":"none"})
|
||||
#$(".msg-error").css({"display":"none"})
|
||||
@clear_display()
|
||||
url = button.data 'endpoint'
|
||||
$.ajax
|
||||
dataType: 'json'
|
||||
url: url
|
||||
error: (std_ajax_err) =>
|
||||
@$reports_request_response_error.text errorMessage
|
||||
$(".msg-error").css({"display":"block"})
|
||||
success: (data) =>
|
||||
@$reports_request_response.text data['status']
|
||||
$(".msg-confirm").css({"display":"block"})
|
||||
@$async_report_btn.click (e) =>
|
||||
# Clear any CSS styling from the request-response areas
|
||||
#$(".msg-confirm").css({"display":"none"})
|
||||
#$(".msg-error").css({"display":"none"})
|
||||
@clear_display()
|
||||
url = $(e.target).data 'endpoint'
|
||||
$.ajax
|
||||
dataType: 'json'
|
||||
url: url
|
||||
error: std_ajax_err =>
|
||||
if e.target.name == 'calculate-grades-csv'
|
||||
@$grades_request_response_error.text gettext("Error generating grades. Please try again.")
|
||||
else if e.target.name == 'problem-grade-report'
|
||||
@$grades_request_response_error.text gettext("Error generating problem grade report. Please try again.")
|
||||
else if e.target.name == 'export-ora2-data'
|
||||
@$grades_request_response_error.text gettext("Error generating ORA data report. Please try again.")
|
||||
$(".msg-error").css({"display":"block"})
|
||||
success: (data) =>
|
||||
@$reports_request_response.text data['status']
|
||||
$(".msg-confirm").css({"display":"block"})
|
||||
|
||||
# handler for when the section title is clicked.
|
||||
onClickTitle: ->
|
||||
|
||||
@@ -75,10 +75,11 @@
|
||||
|
||||
%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><input type="button" name="calculate-grades-csv" value="${_("Generate Grade Report")}" data-endpoint="${ section_data['calculate_grades_csv_url'] }"/></p>
|
||||
|
||||
<p><input type="button" name="problem-grade-report" value="${_("Generate Problem Grade Report")}" data-endpoint="${ section_data['problem_grade_report_url'] }"/></p>
|
||||
<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'] }"/>
|
||||
<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
|
||||
|
||||
<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
|
||||
-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/acid-block.git@e46f9cda8a03e121a00c7e347084d142d22ebfb7#egg=acid-xblock
|
||||
git+https://github.com/edx/edx-ora2.git@1.0.1#egg=ora2==1.0.1
|
||||
-e git+https://github.com/edx/edx-submissions.git@1.0.0#egg=edx-submissions==1.0.0
|
||||
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.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/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
|
||||
|
||||
Reference in New Issue
Block a user