From 5a11f75a48e1a18bc45b8e0fa4e8fb2eb8b67a11 Mon Sep 17 00:00:00 2001 From: Afzal Wali Date: Wed, 28 Oct 2015 15:43:13 +0500 Subject: [PATCH] Add the ability for self-service course survey reports --- lms/djangoapps/instructor/views/api.py | 23 +++++ lms/djangoapps/instructor/views/api_urls.py | 2 + .../instructor/views/instructor_dashboard.py | 3 +- lms/djangoapps/instructor_task/api.py | 15 +++ lms/djangoapps/instructor_task/tasks.py | 13 +++ .../instructor_task/tasks_helper.py | 57 +++++++++++ .../instructor_task/tests/test_api.py | 7 ++ .../tests/test_tasks_helper.py | 95 +++++++++++++++++++ .../instructor_dashboard/data_download.coffee | 20 ++++ .../instructor_dashboard_2/data_download.html | 5 + 10 files changed, 239 insertions(+), 1 deletion(-) diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index a6911eba1b..5546540387 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -1358,6 +1358,29 @@ def get_exec_summary_report(request, course_id): }) +@ensure_csrf_cookie +@cache_control(no_cache=True, no_store=True, must_revalidate=True) +@require_level('staff') +def get_course_survey_results(request, course_id): + """ + get the survey results report for the particular course. + """ + course_key = SlashSeparatedCourseKey.from_deprecated_string(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.") + except AlreadyRunningError: + status_response = _( + "The survey report is currently being created." + " To view the status of the report, see Pending Instructor Tasks below." + " You will be able to download the report when it is complete." + ) + return JsonResponse({ + "status": status_response + }) + + @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) @require_level('staff') diff --git a/lms/djangoapps/instructor/views/api_urls.py b/lms/djangoapps/instructor/views/api_urls.py index 5a94d78abb..086499d5b3 100644 --- a/lms/djangoapps/instructor/views/api_urls.py +++ b/lms/djangoapps/instructor/views/api_urls.py @@ -115,6 +115,8 @@ urlpatterns = patterns( 'instructor.views.api.get_enrollment_report', name="get_enrollment_report"), url(r'get_exec_summary_report$', '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"), # Coupon Codes.. url(r'get_coupon_codes', diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py index 82d48c48bd..72d760e2e1 100644 --- a/lms/djangoapps/instructor/views/instructor_dashboard.py +++ b/lms/djangoapps/instructor/views/instructor_dashboard.py @@ -169,7 +169,6 @@ def instructor_dashboard_2(request, course_id): 'disable_buttons': disable_buttons, 'analytics_dashboard_message': analytics_dashboard_message } - return render_to_response('instructor/instructor_dashboard_2/instructor_dashboard_2.html', context) @@ -516,6 +515,8 @@ def _section_data_download(course, access): 'list_report_downloads_url': reverse('list_report_downloads', kwargs={'course_id': unicode(course_key)}), 'calculate_grades_csv_url': reverse('calculate_grades_csv', 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_survey_results_url': reverse('get_course_survey_results', kwargs={'course_id': unicode(course_key)}), } return section_data diff --git a/lms/djangoapps/instructor_task/api.py b/lms/djangoapps/instructor_task/api.py index 5ad4cc07de..9c8620205e 100644 --- a/lms/djangoapps/instructor_task/api.py +++ b/lms/djangoapps/instructor_task/api.py @@ -26,6 +26,7 @@ from instructor_task.tasks import ( enrollment_report_features_csv, calculate_may_enroll_csv, exec_summary_report_csv, + course_survey_report_csv, generate_certificates, proctored_exam_results_csv ) @@ -436,6 +437,20 @@ def submit_executive_summary_report(request, course_key): # pylint: disable=inv return submit_task(request, task_type, task_class, course_key, task_input, task_key) +def submit_course_survey_report(request, course_key): # pylint: disable=invalid-name + """ + Submits a task to generate a HTML File containing the executive summary report. + + Raises AlreadyRunningError if HTML File is already being updated. + """ + task_type = 'course_survey_report' + task_class = course_survey_report_csv + task_input = {} + task_key = "" + + return submit_task(request, task_type, task_class, course_key, task_input, task_key) + + def submit_proctored_exam_results_report(request, course_key, features): # pylint: disable=invalid-name """ Submits a task to generate a HTML File containing the executive summary report. diff --git a/lms/djangoapps/instructor_task/tasks.py b/lms/djangoapps/instructor_task/tasks.py index 7f61b8ba52..37f4b89aac 100644 --- a/lms/djangoapps/instructor_task/tasks.py +++ b/lms/djangoapps/instructor_task/tasks.py @@ -42,6 +42,7 @@ from instructor_task.tasks_helper import ( upload_enrollment_report, upload_may_enroll_csv, upload_exec_summary_report, + upload_course_survey_report, generate_students_certificates, upload_proctored_exam_results_report ) @@ -227,6 +228,18 @@ def exec_summary_report_csv(entry_id, 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 course_survey_report_csv(entry_id, xmodule_instance_args): + """ + Compute the survey report for a course and upload the + generated report to an S3 bucket for download. + """ + # Translators: This is a past-tense verb that is inserted into task progress messages as {action}. + action_name = ugettext_noop('generated') + task_fn = partial(upload_course_survey_report, 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 proctored_exam_results_csv(entry_id, xmodule_instance_args): """ diff --git a/lms/djangoapps/instructor_task/tasks_helper.py b/lms/djangoapps/instructor_task/tasks_helper.py index e494c9a2b6..a35199fd8c 100644 --- a/lms/djangoapps/instructor_task/tasks_helper.py +++ b/lms/djangoapps/instructor_task/tasks_helper.py @@ -29,6 +29,7 @@ from shoppingcart.models import ( PaidCourseRegistration, CourseRegCodeItem, InvoiceTransaction, Invoice, CouponRedemption, RegistrationCodeRedemption, CourseRegistrationCode ) +from survey.models import SurveyAnswer from track.views import task_track from util.file import course_filename_prefix_generator, UniversalNewlineIterator @@ -1307,6 +1308,62 @@ def upload_exec_summary_report(_xmodule_instance_args, _entry_id, course_id, _ta return task_progress.update_task_state(extra_meta=current_step) +def upload_course_survey_report(_xmodule_instance_args, _entry_id, course_id, _task_input, action_name): # pylint: disable=invalid-name + """ + For a given `course_id`, generate a html report containing the survey results for a course. + """ + start_time = time() + start_date = datetime.now(UTC) + num_reports = 1 + task_progress = TaskProgress(action_name, num_reports, start_time) + + current_step = {'step': 'Gathering course survey report information'} + task_progress.update_task_state(extra_meta=current_step) + + distinct_survey_fields_queryset = SurveyAnswer.objects.filter(course_key=course_id).values('field_name').distinct() + survey_fields = [] + for unique_field_row in distinct_survey_fields_queryset: + survey_fields.append(unique_field_row['field_name']) + survey_fields.sort() + + user_survey_answers = OrderedDict() + survey_answers_for_course = SurveyAnswer.objects.filter(course_key=course_id) + + for survey_field_record in survey_answers_for_course: + user_id = survey_field_record.user.id + if user_id not in user_survey_answers.keys(): + user_survey_answers[user_id] = {} + + user_survey_answers[user_id][survey_field_record.field_name] = survey_field_record.field_value + + header = ["User ID", "User Name", "Email"] + header.extend(survey_fields) + csv_rows = [] + + for user_id in user_survey_answers.keys(): + row = [] + row.append(user_id) + user_obj = User.objects.get(id=user_id) + row.append(user_obj.username) + row.append(user_obj.email) + for survey_field in survey_fields: + row.append(user_survey_answers[user_id].get(survey_field, '')) + csv_rows.append(row) + + task_progress.attempted = task_progress.succeeded = len(csv_rows) + task_progress.skipped = task_progress.total - task_progress.attempted + + csv_rows.insert(0, header) + + current_step = {'step': 'Uploading CSV'} + task_progress.update_task_state(extra_meta=current_step) + + # Perform the upload + upload_csv_to_report_store(csv_rows, 'course_survey_results', course_id, start_date) + + return task_progress.update_task_state(extra_meta=current_step) + + def upload_proctored_exam_results_report(_xmodule_instance_args, _entry_id, course_id, _task_input, action_name): # pylint: disable=invalid-name """ For a given `course_id`, generate a CSV file containing diff --git a/lms/djangoapps/instructor_task/tests/test_api.py b/lms/djangoapps/instructor_task/tests/test_api.py index 113aeeb440..6ac8a7bb94 100644 --- a/lms/djangoapps/instructor_task/tests/test_api.py +++ b/lms/djangoapps/instructor_task/tests/test_api.py @@ -20,6 +20,7 @@ from instructor_task.api import ( submit_detailed_enrollment_features_csv, submit_calculate_may_enroll_csv, submit_executive_summary_report, + submit_course_survey_report, generate_certificates_for_all_students, ) @@ -231,6 +232,12 @@ class InstructorTaskCourseSubmitTest(TestReportMixin, InstructorTaskCourseTestCa ) self._test_resubmission(api_call) + def test_submit_course_survey_report(self): + api_call = lambda: submit_course_survey_report( + self.create_task_request(self.instructor), self.course.id + ) + self._test_resubmission(api_call) + def test_submit_calculate_may_enroll(self): api_call = lambda: submit_calculate_may_enroll_csv( self.create_task_request(self.instructor), diff --git a/lms/djangoapps/instructor_task/tests/test_tasks_helper.py b/lms/djangoapps/instructor_task/tests/test_tasks_helper.py index 9cefcb357f..cc79806ad5 100644 --- a/lms/djangoapps/instructor_task/tests/test_tasks_helper.py +++ b/lms/djangoapps/instructor_task/tests/test_tasks_helper.py @@ -32,6 +32,7 @@ from verify_student.tests.factories import SoftwareSecurePhotoVerificationFactor from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.partitions.partitions import Group, UserPartition from instructor_task.models import ReportStore +from survey.models import SurveyForm, SurveyAnswer from instructor_task.tasks_helper import ( cohort_students_and_upload, upload_problem_responses_csv, @@ -41,6 +42,7 @@ from instructor_task.tasks_helper import ( upload_may_enroll_csv, upload_enrollment_report, upload_exec_summary_report, + upload_course_survey_report, generate_students_certificates, ) from instructor_analytics.basic import UNAVAILABLE @@ -953,6 +955,99 @@ class TestExecutiveSummaryReport(TestReportMixin, InstructorTaskCourseTestCase): self.assertTrue(data in html_file_data) +@ddt.ddt +class TestCourseSurveyReport(TestReportMixin, InstructorTaskCourseTestCase): + """ + Tests that Course Survey report generation works. + """ + def setUp(self): + super(TestCourseSurveyReport, self).setUp() + self.course = CourseFactory.create() + + self.question1 = "question1" + self.question2 = "question2" + self.question3 = "question3" + self.answer1 = "answer1" + self.answer2 = "answer2" + self.answer3 = "answer3" + + self.student1 = UserFactory() + self.student2 = UserFactory() + + self.test_survey_name = 'TestSurvey' + self.test_form = '' + self.survey_form = SurveyForm.create(self.test_survey_name, self.test_form) + + self.survey1 = SurveyAnswer.objects.create(user=self.student1, form=self.survey_form, course_key=self.course.id, + field_name=self.question1, field_value=self.answer1) + self.survey2 = SurveyAnswer.objects.create(user=self.student1, form=self.survey_form, course_key=self.course.id, + field_name=self.question2, field_value=self.answer2) + self.survey3 = SurveyAnswer.objects.create(user=self.student2, form=self.survey_form, course_key=self.course.id, + field_name=self.question1, field_value=self.answer3) + self.survey4 = SurveyAnswer.objects.create(user=self.student2, form=self.survey_form, course_key=self.course.id, + field_name=self.question2, field_value=self.answer2) + self.survey5 = SurveyAnswer.objects.create(user=self.student2, form=self.survey_form, course_key=self.course.id, + field_name=self.question3, field_value=self.answer1) + + def test_successfully_generate_course_survey_report(self): + """ + Test that successfully generates the course survey report. + """ + task_input = {'features': []} + with patch('instructor_task.tasks_helper._get_current_task'): + result = upload_course_survey_report( + None, None, self.course.id, + task_input, 'generating course survey report' + ) + self.assertDictContainsSubset({'attempted': 2, 'succeeded': 2, 'failed': 0}, result) + + @patch.dict('django.conf.settings.FEATURES', {'ENABLE_PAID_COURSE_REGISTRATION': True}) + def test_generate_course_survey_report(self): + """ + test to generate course survey report + and then test the report authenticity. + """ + + task_input = {'features': []} + with patch('instructor_task.tasks_helper._get_current_task'): + result = upload_course_survey_report( + None, None, self.course.id, + task_input, 'generating course survey report' + ) + + report_store = ReportStore.from_config(config_name='GRADES_DOWNLOAD') + header_row = ",".join(['User ID', 'User Name', 'Email', self.question1, self.question2, self.question3]) + student1_row = ",".join([ + str(self.student1.id), # pylint: disable=no-member + self.student1.username, + self.student1.email, + self.answer1, + self.answer2 + ]) + student2_row = ",".join([ + str(self.student2.id), # pylint: disable=no-member + self.student2.username, + self.student2.email, + self.answer3, + self.answer2, + self.answer1 + ]) + expected_data = [header_row, student1_row, student2_row] + + self.assertDictContainsSubset({'attempted': 2, 'succeeded': 2, 'failed': 0}, result) + self._verify_csv_file_report(report_store, expected_data) + + def _verify_csv_file_report(self, report_store, expected_data): + """ + Verify course survey data. + """ + report_csv_filename = report_store.links_for(self.course.id)[0][0] + with open(report_store.path_to(self.course.id, report_csv_filename)) as csv_file: + csv_file_data = csv_file.read() + for data in expected_data: + self.assertIn(data, csv_file_data) + + @ddt.ddt class TestStudentReport(TestReportMixin, InstructorTaskCourseTestCase): """ diff --git a/lms/static/coffee/src/instructor_dashboard/data_download.coffee b/lms/static/coffee/src/instructor_dashboard/data_download.coffee index a4de311fce..032ac9e02c 100644 --- a/lms/static/coffee/src/instructor_dashboard/data_download.coffee +++ b/lms/static/coffee/src/instructor_dashboard/data_download.coffee @@ -75,6 +75,7 @@ class DataDownload @$list_studs_btn = @$section.find("input[name='list-profiles']'") @$list_studs_csv_btn = @$section.find("input[name='list-profiles-csv']'") @$list_proctored_exam_results_csv_btn = @$section.find("input[name='proctored-exam-results-report']'") + @$survey_results_csv_btn = @$section.find("input[name='survey-results-report']'") @$list_may_enroll_csv_btn = @$section.find("input[name='list-may-enroll-csv']") @$list_problem_responses_csv_input = @$section.find("input[name='problem-location']") @$list_problem_responses_csv_btn = @$section.find("input[name='list-problem-responses-csv']") @@ -121,6 +122,25 @@ class DataDownload @$reports_request_response.text data['status'] $(".msg-confirm").css({"display":"block"}) + # attach click handlers + # The list_proctored_exam_results case is always CSV + @$survey_results_csv_btn.click (e) => + url = @$survey_results_csv_btn.data 'endpoint' + # display html from survey results config endpoint + $.ajax + dataType: 'json' + url: url + error: (std_ajax_err) => + @clear_display() + @$reports_request_response_error.text gettext( + "Error generating survey results. Please try again." + ) + $(".msg-error").css({"display":"block"}) + success: (data) => + @clear_display() + @$reports_request_response.text data['status'] + $(".msg-confirm").css({"display":"block"}) + # this handler binds to both the download # and the csv button @$list_studs_csv_btn.click (e) => diff --git a/lms/templates/instructor/instructor_dashboard_2/data_download.html b/lms/templates/instructor/instructor_dashboard_2/data_download.html index 07c43b3faf..0355656249 100644 --- a/lms/templates/instructor/instructor_dashboard_2/data_download.html +++ b/lms/templates/instructor/instructor_dashboard_2/data_download.html @@ -40,6 +40,11 @@

%endif + %if section_data['course_has_survey']: +

${_("Click to generate a CSV file of survey results for this course.")}

+

+ %endif +

${_("To generate a CSV file that lists all student answers to a given problem, enter the location of the problem (from its Staff Debug Info).")}