Commit 5a11f75a by Afzal Wali Committed by Chris Dodge

Add the ability for self-service course survey reports

parent d210ca6e
......@@ -1361,6 +1361,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')
def get_proctored_exam_results(request, course_id):
"""
get the proctored exam resultsreport for the particular course.
......
......@@ -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
)
......@@ -228,6 +229,18 @@ def exec_summary_report_csv(entry_id, xmodule_instance_args):
@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):
"""
Compute proctored exam results report for a course and upload the
......
......@@ -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
......@@ -954,6 +956,99 @@ class TestExecutiveSummaryReport(TestReportMixin, InstructorTaskCourseTestCase):
@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):
"""
Tests that CSV student profile report generation works.
......
......@@ -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>
......
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