Repository: e-valuation/EvaP

Issue:
Add degrees to result export
The result export should include the degree and course type for each course in separate lines in the excel sheet right below the title of the course.

Furthermore, it should be possible to select for which degrees the results shall be exported. In addition to the current course type selection a degree selection needs to be added. A course will then be included in a specific sheet if it is in one of the selected degrees *and* in one of the selected course types.


Discussion:
regarding the export part, the exporter is kind of hard to read, but the actual change should be small. in the exporter there's a small loop that iterates over all courses and outputs their names (the exporter works line by line!), and below that, another loop would have to be added that outputs the degree.

regarding the degree selection, that should work analogously to the course type selection.




@Irliung do you still want to work on this or shall i remove your assignment?

Resolving patch:
diff --git a/evap/results/exporters.py b/evap/results/exporters.py
--- a/evap/results/exporters.py
+++ b/evap/results/exporters.py
@@ -4,7 +4,7 @@
 
 import xlwt
 
-from evap.evaluation.models import CourseType
+from evap.evaluation.models import CourseType, Degree
 from evap.results.tools import (collect_results, calculate_average_course_distribution, calculate_average_distribution,
                                 distribution_to_grade, get_grade_color)
 
@@ -76,13 +76,13 @@ def filter_text_and_heading_questions(self, questions):
 
         return filtered_questions
 
-    def export(self, response, course_types_list, include_not_enough_voters=False, include_unpublished=False):
+    def export(self, response, selection_list, include_not_enough_voters=False, include_unpublished=False):
         self.workbook = xlwt.Workbook()
         self.init_styles(self.workbook)
         counter = 1
         course_results_exist = False
 
-        for course_types in course_types_list:
+        for degrees, course_types in selection_list:
             self.sheet = self.workbook.add_sheet("Sheet " + str(counter))
             counter += 1
             self.row = 0
@@ -94,7 +94,9 @@ def export(self, response, course_types_list, include_not_enough_voters=False, i
                 evaluation_states.extend(['evaluated', 'reviewed'])
 
             used_questionnaires = set()
-            for evaluation in self.semester.evaluations.filter(state__in=evaluation_states, course__type__in=course_types).all():
+            for evaluation in self.semester.evaluations.filter(
+                state__in=evaluation_states, course__degrees__in=degrees, course__type__in=course_types
+            ).distinct():
                 if evaluation.is_single_result:
                     continue
                 if not evaluation.can_publish_rating_results and not include_not_enough_voters:
@@ -115,8 +117,11 @@ def export(self, response, course_types_list, include_not_enough_voters=False, i
             evaluations_with_results.sort(key=lambda cr: (cr[0].course.type.order, cr[0].full_name))
             used_questionnaires = sorted(used_questionnaires)
 
-            course_type_names = [ct.name for ct in CourseType.objects.filter(pk__in=course_types)]
-            writec(self, _("Evaluation {0}\n\n{1}").format(self.semester.name, ", ".join(course_type_names)), "headline")
+            degree_names = [degree.name for degree in Degree.objects.filter(pk__in=degrees)]
+            course_type_names = [course_type.name for course_type in CourseType.objects.filter(pk__in=course_types)]
+            writec(self, _("Evaluation {}\n\n{}\n\n{}").format(
+                self.semester.name, ", ".join(degree_names), ", ".join(course_type_names)
+            ), "headline")
 
             for evaluation, results in evaluations_with_results:
                 writec(self, evaluation.full_name, "evaluation")
diff --git a/evap/staff/forms.py b/evap/staff/forms.py
--- a/evap/staff/forms.py
+++ b/evap/staff/forms.py
@@ -767,8 +767,16 @@ def clean_original_answer(self):
 class ExportSheetForm(forms.Form):
     def __init__(self, semester, *args, **kwargs):
         super(ExportSheetForm, self).__init__(*args, **kwargs)
+        degrees = Degree.objects.filter(courses__semester=semester).distinct()
+        degree_tuples = [(degree.pk, degree.name) for degree in degrees]
+        self.fields['selected_degrees'] = forms.MultipleChoiceField(
+            choices=degree_tuples,
+            required=True,
+            widget=forms.CheckboxSelectMultiple(),
+            label=_("Degrees")
+        )
         course_types = CourseType.objects.filter(courses__semester=semester).distinct()
-        course_type_tuples = [(ct.pk, ct.name) for ct in course_types]
+        course_type_tuples = [(course_type.pk, course_type.name) for course_type in course_types]
         self.fields['selected_course_types'] = forms.MultipleChoiceField(
             choices=course_type_tuples,
             required=True,
diff --git a/evap/staff/views.py b/evap/staff/views.py
--- a/evap/staff/views.py
+++ b/evap/staff/views.py
@@ -466,15 +466,16 @@ def semester_export(request, semester_id):
     if formset.is_valid():
         include_not_enough_voters = request.POST.get('include_not_enough_voters') == 'on'
         include_unpublished = request.POST.get('include_unpublished') == 'on'
-        course_types_list = []
+        selection_list = []
         for form in formset:
-            if 'selected_course_types' in form.cleaned_data:
-                course_types_list.append(form.cleaned_data['selected_course_types'])
+            selection_list.append((form.cleaned_data['selected_degrees'], form.cleaned_data['selected_course_types']))
 
         filename = "Evaluation-{}-{}.xls".format(semester.name, get_language())
         response = HttpResponse(content_type="application/vnd.ms-excel")
         response["Content-Disposition"] = "attachment; filename=\"{}\"".format(filename)
-        ExcelExporter(semester).export(response, course_types_list, include_not_enough_voters, include_unpublished)
+        ExcelExporter(semester).export(
+            response, selection_list, include_not_enough_voters, include_unpublished
+        )
         return response
     else:
         return render(request, "staff_semester_export.html", dict(semester=semester, formset=formset))


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
@@ -4,7 +4,7 @@
 from django.test import TestCase
 from django.utils import translation
 
-from evap.evaluation.models import (Contribution, Course, CourseType, Evaluation, Question, Questionnaire,
+from evap.evaluation.models import (Contribution, Course, CourseType, Degree, Evaluation, Question, Questionnaire,
                                     RatingAnswerCounter, Semester, UserProfile)
 from evap.results.exporters import ExcelExporter
 
@@ -26,7 +26,14 @@ def test_grade_color_calculation(self):
         self.assertEqual(exporter.normalize_number(2.8), 2.8)
 
     def test_questionnaire_ordering(self):
-        evaluation = mommy.make(Evaluation, state='published', _participant_count=2, _voter_count=2)
+        degree = mommy.make(Degree)
+        evaluation = mommy.make(
+            Evaluation,
+            course=mommy.make(Course, degrees=[degree]),
+            state='published',
+            _participant_count=2,
+            _voter_count=2
+        )
 
         questionnaire_1 = mommy.make(Questionnaire, order=1, type=Questionnaire.TOP)
         questionnaire_2 = mommy.make(Questionnaire, order=4, type=Questionnaire.TOP)
@@ -46,7 +53,12 @@ def test_questionnaire_ordering(self):
         mommy.make(RatingAnswerCounter, question=question_4, contribution=evaluation.general_contribution, answer=3, count=100)
 
         binary_content = BytesIO()
-        ExcelExporter(evaluation.course.semester).export(binary_content, [[evaluation.course.type.id]], True, True)
+        ExcelExporter(evaluation.course.semester).export(
+            binary_content,
+            [([course_degree.id for course_degree in evaluation.course.degrees.all()], [evaluation.course.type.id])],
+            True,
+            True
+        )
         binary_content.seek(0)
         workbook = xlrd.open_workbook(file_contents=binary_content.read())
 
@@ -63,7 +75,14 @@ def test_questionnaire_ordering(self):
         self.assertEqual(workbook.sheets()[0].row_values(14)[0], question_4.text)
 
     def test_heading_question_filtering(self):
-        evaluation = mommy.make(Evaluation, state='published', _participant_count=2, _voter_count=2)
+        degree = mommy.make(Degree)
+        evaluation = mommy.make(
+            Evaluation,
+            course=mommy.make(Course, degrees=[degree]),
+            state='published',
+            _participant_count=2,
+            _voter_count=2
+        )
         contributor = mommy.make(UserProfile)
         evaluation.general_contribution.questionnaires.set([mommy.make(Questionnaire)])
 
@@ -77,7 +96,12 @@ def test_heading_question_filtering(self):
         mommy.make(RatingAnswerCounter, question=likert_question, contribution=contribution, answer=3, count=100)
 
         binary_content = BytesIO()
-        ExcelExporter(evaluation.course.semester).export(binary_content, [[evaluation.course.type.id]], True, True)
+        ExcelExporter(evaluation.course.semester).export(
+            binary_content,
+            [([course_degree.id for course_degree in evaluation.course.degrees.all()], [evaluation.course.type.id])],
+            True,
+            True
+        )
         binary_content.seek(0)
         workbook = xlrd.open_workbook(file_contents=binary_content.read())
 
@@ -89,19 +113,29 @@ def test_heading_question_filtering(self):
     def test_view_excel_file_sorted(self):
         semester = mommy.make(Semester)
         course_type = mommy.make(CourseType)
-        mommy.make(Evaluation, state='published', course=mommy.make(Course, type=course_type, semester=semester, name_de="A", name_en="B"),
-                   name_de='Evaluation1', name_en='Evaluation1')
-
-        mommy.make(Evaluation, state='published', course=mommy.make(Course, type=course_type, semester=semester, name_de="B", name_en="A"),
-                   name_de='Evaluation2', name_en='Evaluation2')
+        degree = mommy.make(Degree)
+        mommy.make(
+            Evaluation,
+            state='published',
+            course=mommy.make(Course, degrees=[degree], type=course_type, semester=semester, name_de="A", name_en="B"),
+            name_de='Evaluation1',
+            name_en='Evaluation1'
+        )
+        mommy.make(
+            Evaluation,
+            state='published',
+            course=mommy.make(Course, degrees=[degree], type=course_type, semester=semester, name_de="B", name_en="A"),
+            name_de='Evaluation2',
+            name_en='Evaluation2'
+        )
 
         content_de = BytesIO()
         with translation.override("de"):
-            ExcelExporter(semester).export(content_de, [[course_type.id]], True, True)
+            ExcelExporter(semester).export(content_de, [([degree.id], [course_type.id])], True, True)
 
         content_en = BytesIO()
         with translation.override("en"):
-            ExcelExporter(semester).export(content_en, [[course_type.id]], True, True)
+            ExcelExporter(semester).export(content_en, [([degree.id], [course_type.id])], True, True)
 
         content_de.seek(0)
         content_en.seek(0)
@@ -116,11 +150,22 @@ def test_view_excel_file_sorted(self):
         self.assertEqual(workbook.sheets()[0].row_values(0)[2], "B – Evaluation1")
 
     def test_course_type_ordering(self):
+        degree = mommy.make(Degree)
         course_type_1 = mommy.make(CourseType, order=1)
         course_type_2 = mommy.make(CourseType, order=2)
         semester = mommy.make(Semester)
-        evaluation_1 = mommy.make(Evaluation, course=mommy.make(Course, semester=semester, type=course_type_1), state='published', _participant_count=2, _voter_count=2)
-        evaluation_2 = mommy.make(Evaluation, course=mommy.make(Course, semester=semester, type=course_type_2), state='published', _participant_count=2, _voter_count=2)
+        evaluation_1 = mommy.make(Evaluation,
+            course=mommy.make(Course, semester=semester, degrees=[degree], type=course_type_1),
+            state='published',
+            _participant_count=2,
+            _voter_count=2
+        )
+        evaluation_2 = mommy.make(Evaluation,
+            course=mommy.make(Course, semester=semester, degrees=[degree], type=course_type_2),
+            state='published',
+            _participant_count=2,
+            _voter_count=2
+        )
 
         questionnaire = mommy.make(Questionnaire)
         question = mommy.make(Question, type=Question.LIKERT, questionnaire=questionnaire)
@@ -132,7 +177,7 @@ def test_course_type_ordering(self):
         mommy.make(RatingAnswerCounter, question=question, contribution=evaluation_2.general_contribution, answer=3, count=2)
 
         binary_content = BytesIO()
-        ExcelExporter(semester).export(binary_content, [[course_type_1.id, course_type_2.id]], True, True)
+        ExcelExporter(semester).export(binary_content, [([degree.id], [course_type_1.id, course_type_2.id])], True, True)
         binary_content.seek(0)
         workbook = xlrd.open_workbook(file_contents=binary_content.read())
 
@@ -143,7 +188,7 @@ def test_course_type_ordering(self):
         course_type_2.save()
 
         binary_content = BytesIO()
-        ExcelExporter(semester).export(binary_content, [[course_type_1.id, course_type_2.id]], True, True)
+        ExcelExporter(semester).export(binary_content, [([degree.id], [course_type_1.id, course_type_2.id])], True, True)
         binary_content.seek(0)
         workbook = xlrd.open_workbook(file_contents=binary_content.read())
 
diff --git a/evap/staff/tests/test_views.py b/evap/staff/tests/test_views.py
--- a/evap/staff/tests/test_views.py
+++ b/evap/staff/tests/test_views.py
@@ -675,22 +675,29 @@ class TestSemesterExportView(WebTest):
     def setUpTestData(cls):
         mommy.make(UserProfile, username='manager', groups=[Group.objects.get(name='Manager')])
         cls.semester = mommy.make(Semester, pk=1)
+        cls.degree = mommy.make(Degree)
         cls.course_type = mommy.make(CourseType)
-        cls.evaluation = mommy.make(Evaluation, course=mommy.make(Course, type=cls.course_type, semester=cls.semester))
+        cls.evaluation = mommy.make(
+            Evaluation,
+            course=mommy.make(Course, degrees=[cls.degree], type=cls.course_type, semester=cls.semester)
+        )
 
     def test_view_downloads_excel_file(self):
         page = self.app.get(self.url, user='manager')
         form = page.forms["semester-export-form"]
 
-        # Check one course type.
+        # Check one degree and course type.
+        form.set('form-0-selected_degrees', 'id_form-0-selected_degrees_0')
         form.set('form-0-selected_course_types', 'id_form-0-selected_course_types_0')
 
         response = form.submit()
 
         # Load response as Excel file and check its heading for correctness.
         workbook = xlrd.open_workbook(file_contents=response.content)
-        self.assertEqual(workbook.sheets()[0].row_values(0)[0],
-                         'Evaluation {0}\n\n{1}'.format(self.semester.name, ", ".join([self.course_type.name])))
+        self.assertEqual(
+            workbook.sheets()[0].row_values(0)[0],
+            'Evaluation {}\n\n{}\n\n{}'.format(self.semester.name, self.degree.name, self.course_type.name)
+        )
 
 
 class TestSemesterRawDataExportView(WebTestWith200Check):