Repository: e-valuation/EvaP

Issue:
Result exporter is not tested
there are some tests in staff/tests/test_views that test the exporter, but it seems like it is not creating any questionnaires. large portions of the results exporter are not covered therefore.


Discussion:

Resolving patch:
diff --git a/evap/results/exporters.py b/evap/results/exporters.py
--- a/evap/results/exporters.py
+++ b/evap/results/exporters.py
@@ -85,6 +85,10 @@ def export(self, response, semesters, selection_list, include_not_enough_voters=
         # always be tightly coupled based on the layout of the sheet. We thus think that one big method
         # containing the business logic is okay here
         # pylint: disable=too-many-locals, too-many-nested-blocks, too-many-branches, too-many-statements
+
+        # We want to throw early here, since workbook.save() will throw an IndexError otherwise.
+        assert len(selection_list) > 0
+
         workbook = xlwt.Workbook()
         self.init_styles(workbook)
         counter = 1
@@ -114,7 +118,8 @@ def export(self, response, semesters, selection_list, include_not_enough_voters=
                 results = OrderedDict()
                 for contribution_result in collect_results(evaluation).contribution_results:
                     for questionnaire_result in contribution_result.questionnaire_results:
-                        if all(not question_result.question.is_rating_question or question_result.counts is None for question_result in questionnaire_result.question_results):
+                        # RatingQuestion.counts is a tuple of integers or None, if this tuple is all zero, we want to exclude it
+                        if all(not question_result.question.is_rating_question or question_result.counts is None or sum(question_result.counts) == 0 for question_result in questionnaire_result.question_results):
                             continue
                         if not contributor or contribution_result.contributor is None or contribution_result.contributor == contributor:
                             results.setdefault(questionnaire_result.questionnaire.id, []).extend(questionnaire_result.question_results)


Test patch:
diff --git a/evap/results/tests/test_exporters.py b/evap/results/tests/test_exporters.py
--- a/evap/results/tests/test_exporters.py
+++ b/evap/results/tests/test_exporters.py
@@ -202,6 +202,161 @@ def test_course_type_ordering(self):
         self.assertEqual(workbook.sheets()[0].row_values(0)[1], evaluation_2.full_name + "\n")
         self.assertEqual(workbook.sheets()[0].row_values(0)[2], evaluation_1.full_name + "\n")
 
+    def test_multiple_sheets(self):
+        binary_content = BytesIO()
+        semester = baker.make(Semester)
+        ExcelExporter().export(binary_content, [semester], [([], []), ([], [])])
+
+        binary_content.seek(0)
+        workbook = xlrd.open_workbook(file_contents=binary_content.read())
+
+        self.assertEqual(len(workbook.sheets()), 2)
+
+    @staticmethod
+    def get_export_sheet(semester, degree, course_types, include_unpublished=True, include_not_enough_voters=True):
+        binary_content = BytesIO()
+        ExcelExporter().export(
+            binary_content,
+            [semester],
+            [([degree.id], course_types)],
+            include_unpublished=include_unpublished,
+            include_not_enough_voters=include_not_enough_voters,
+        )
+        binary_content.seek(0)
+        workbook = xlrd.open_workbook(file_contents=binary_content.read())
+        return workbook.sheets()[0]
+
+    def test_include_unpublished(self):
+        semester = baker.make(Semester)
+        degree = baker.make(Degree)
+        published_evaluation = baker.make(Evaluation, state="published", course__semester=semester, course__degrees=[degree], course__type__order=1)
+        unpublished_evaluation = baker.make(Evaluation, state="reviewed", course__semester=semester, course__degrees=[degree], course__type__order=2)
+        course_types = [published_evaluation.course.type.id, unpublished_evaluation.course.type.id]
+
+        # First, make sure that the unpublished does not appear
+        sheet = self.get_export_sheet(include_unpublished=False, semester=semester, degree=degree, course_types=course_types)
+        self.assertEqual(len(sheet.row_values(0)), 2)
+        self.assertEqual(
+            sheet.row_values(0)[1][:-1],
+            published_evaluation.full_name
+        )
+
+        # Now, make sure that it appears when wanted
+        sheet = self.get_export_sheet(include_unpublished=True, semester=semester, degree=degree, course_types=course_types)
+        self.assertEqual(len(sheet.row_values(0)), 3)
+        # These two should be ordered according to evaluation.course.type.order
+        self.assertEqual(sheet.row_values(0)[1][:-1], published_evaluation.full_name)
+        self.assertEqual(sheet.row_values(0)[2][:-1], unpublished_evaluation.full_name)
+
+    def test_include_not_enough_voters(self):
+        semester = baker.make(Semester)
+        degree = baker.make(Degree)
+        enough_voters_evaluation = baker.make(
+            Evaluation,
+            state="published",
+            course__semester=semester,
+            course__degrees=[degree],
+            _voter_count=1000,
+            _participant_count=1000,
+        )
+        not_enough_voters_evaluation = baker.make(
+            Evaluation,
+            state="published",
+            course__semester=semester,
+            course__degrees=[degree],
+            _voter_count=1,
+            _participant_count=1000,
+        )
+
+        course_types = [enough_voters_evaluation.course.type.id, not_enough_voters_evaluation.course.type.id]
+
+        # First, make sure that the one with only a single voter does not appear
+        sheet = self.get_export_sheet(semester, degree, course_types, include_not_enough_voters=False)
+        self.assertEqual(len(sheet.row_values(0)), 2)
+        self.assertEqual(
+            sheet.row_values(0)[1][:-1],
+            enough_voters_evaluation.full_name
+        )
+
+        # Now, check with the option enabled
+        sheet = self.get_export_sheet(semester, degree, course_types, include_not_enough_voters=True)
+        self.assertEqual(len(sheet.row_values(0)), 3)
+        self.assertEqual(
+                {enough_voters_evaluation.full_name, not_enough_voters_evaluation.full_name},
+                {sheet.row_values(0)[1][:-1], sheet.row_values(0)[2][:-1]}
+        )
+
+    def test_no_degree_or_course_type(self):
+        evaluation = baker.make(Evaluation)
+        with self.assertRaises(AssertionError):
+            ExcelExporter().export(BytesIO(), [evaluation.course.semester], [])
+
+    def test_exclude_single_result(self):
+        degree = baker.make(Degree)
+        evaluation = baker.make(Evaluation, is_single_result=True, state="published", course__degrees=[degree])
+        sheet = self.get_export_sheet(evaluation.course.semester, degree, [evaluation.course.type.id])
+        self.assertEqual(len(sheet.row_values(0)), 1, "There should be no column for the evaluation, only the row description")
+
+    def test_exclude_used_but_unanswered_questionnaires(self):
+        degree = baker.make(Degree)
+        evaluation = baker.make(Evaluation, _voter_count=10, _participant_count=10, state="published", course__degrees=[degree])
+        used_questionnaire = baker.make(Questionnaire)
+        used_question = baker.make(Question, type=Question.LIKERT, questionnaire=used_questionnaire)
+        unused_questionnaire = baker.make(Questionnaire)
+        unused_question = baker.make(Question, type=Question.LIKERT, questionnaire=unused_questionnaire)
+        baker.make(RatingAnswerCounter, question=used_question, contribution=evaluation.general_contribution, answer=3, count=10)
+        evaluation.general_contribution.questionnaires.set([used_questionnaire, unused_questionnaire])
+
+        sheet = self.get_export_sheet(evaluation.course.semester, degree, [evaluation.course.type.id])
+        self.assertEqual(sheet.row_values(4)[0], used_questionnaire.name)
+        self.assertEqual(sheet.row_values(5)[0], used_question.text)
+        self.assertNotIn(unused_questionnaire.name, sheet.col_values(0))
+        self.assertNotIn(unused_question.text, sheet.col_values(0))
+
+    def test_degree_course_type_name(self):
+        degree = baker.make(Degree, name_en="Celsius")
+        course_type = baker.make(CourseType, name_en="LetsPlay")
+        evaluation = baker.make(Evaluation, course__degrees=[degree], course__type=course_type, state="published")
+
+        sheet = self.get_export_sheet(evaluation.course.semester, degree, [course_type.id])
+        self.assertEqual(sheet.col_values(1)[1:3], [degree.name, course_type.name])
+
+    def test_multiple_evaluations(self):
+        semester = baker.make(Semester)
+        degree = baker.make(Degree)
+        evaluation1 = baker.make(Evaluation, course__semester=semester, course__degrees=[degree], state="published")
+        evaluation2 = baker.make(Evaluation, course__semester=semester, course__degrees=[degree], state="published")
+
+        sheet = self.get_export_sheet(semester, degree, [evaluation1.course.type.id, evaluation2.course.type.id])
+
+        self.assertEqual(
+            set(sheet.row_values(0)[1:]),
+            set((evaluation1.full_name + "\n", evaluation2.full_name + "\n"))
+        )
+
+    def test_correct_grades_and_bottom_numbers(self):
+        degree = baker.make(Degree)
+        evaluation = baker.make(Evaluation, _voter_count=5, _participant_count=10, course__degrees=[degree], state="published")
+        questionnaire1 = baker.make(Questionnaire, order=1)
+        questionnaire2 = baker.make(Questionnaire, order=2)
+        question1 = baker.make(Question, type=Question.LIKERT, questionnaire=questionnaire1)
+        question2 = baker.make(Question, type=Question.LIKERT, questionnaire=questionnaire2)
+        baker.make(RatingAnswerCounter, answer=1, count=1, question=question1, contribution=evaluation.general_contribution)
+        baker.make(RatingAnswerCounter, answer=3, count=1, question=question1, contribution=evaluation.general_contribution)
+        baker.make(RatingAnswerCounter, answer=2, count=1, question=question2, contribution=evaluation.general_contribution)
+        baker.make(RatingAnswerCounter, answer=4, count=1, question=question2, contribution=evaluation.general_contribution)
+
+        evaluation.general_contribution.questionnaires.set([questionnaire1, questionnaire2])
+
+        sheet = self.get_export_sheet(evaluation.course.semester, degree, [evaluation.course.type.id])
+
+        self.assertEqual(sheet.row_values(5)[1], 2.0)       # question 1 average
+        self.assertEqual(sheet.row_values(8)[1], 3.0)       # question 2 average
+        self.assertEqual(sheet.row_values(10)[1], 2.5)      # Average grade
+        self.assertEqual(sheet.row_values(11)[1], "5/10")   # Voters / Participants
+        self.assertEqual(sheet.row_values(12)[1], "50%")    # Voter percentage
+
+
     def test_contributor_result_export(self):
         degree = baker.make(Degree)
         contributor = baker.make(UserProfile)