Add the ability for self-service course survey reports
This commit is contained in:
@@ -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')
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 = '<input name="field1"></input>'
|
||||
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):
|
||||
"""
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -40,6 +40,11 @@
|
||||
<p><input type="button" name="proctored-exam-results-report" value="${_("Generate Proctored Exam Results Report")}" data-endpoint="${ section_data['list_proctored_results_url'] }"/></p>
|
||||
%endif
|
||||
|
||||
%if section_data['course_has_survey']:
|
||||
<p>${_("Click to generate a CSV file of survey results for this course.")}</p>
|
||||
<p><input type="button" name="survey-results-report" value="${_("Generate Survey Results Report")}" data-endpoint="${ section_data['course_survey_results_url'] }"/></p>
|
||||
%endif
|
||||
|
||||
<p>${_("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).")}</p>
|
||||
|
||||
<p>
|
||||
|
||||
Reference in New Issue
Block a user