Repository: e-valuation/EvaP

Issue:
grade 2.0 is sometimes yellow, sometimes green in the export
in the last export i saw some grades of 2.0 which were green, others were yellow with no apparent reason.



Discussion:
This might be due to one grade being slightly below 2.0, e.g. 1.96. The color green is generated, but excel rounds to 2.0 when displaying the grade. One possible fix might be to round the grade before deciding on the color.

Sounds good.

confirmed @PFischbeck's assumption with a freshly generated export.

instead of rounding to the nearest neighbor, like excel does, we could also round towards zero and tell excel to round towards zero as well with some function. while the first solution might be easier, the second one is more consistent with e.g. the bachelor's grade (which afaik gets rounded towards zero).

Yes, the final grade is rounded towards zero, but grades for individual courses aren't. So please round to the nearest number with one decimal in this case.


Resolving patch:
diff --git a/evap/results/exporters.py b/evap/results/exporters.py
--- a/evap/results/exporters.py
+++ b/evap/results/exporters.py
@@ -10,9 +10,66 @@
 
 
 class ExcelExporter(object):
+
     def __init__(self, semester):
         self.semester = semester
 
+    styles = {
+        'default':       xlwt.Style.default_style,
+        'avg':           xlwt.easyxf('alignment: horiz centre; font: bold on; borders: left medium, top medium, bottom medium'),
+        'variance_low':  xlwt.easyxf('alignment: horiz centre; borders: right medium', num_format_str="0.0"),
+        'variance_med':  xlwt.easyxf('pattern: pattern solid, fore_colour gray25; alignment: horiz centre; borders: right medium', num_format_str="0.0"),
+        'variance_high': xlwt.easyxf('pattern: pattern solid, fore_colour gray40; alignment: horiz centre; borders: right medium', num_format_str="0.0"),
+        'headline':      xlwt.easyxf('font: bold on, height 400; alignment: horiz centre, vert centre, wrap on', num_format_str="0.0"),
+        'course':        xlwt.easyxf('alignment: horiz centre, wrap on, rota 90; borders: left medium, top medium'),
+        'course_unfinished': xlwt.easyxf('alignment: horiz centre, wrap on, rota 90; borders: left medium, top medium; font: italic on'),
+        'total_answers': xlwt.easyxf('alignment: horiz centre; borders: left medium, bottom medium, right medium'),
+        'bold':          xlwt.easyxf('font: bold on'),
+        'border_left':   xlwt.easyxf('borders: left medium'),
+        'border_right':  xlwt.easyxf('borders: right medium'),
+        'border_top_bottom_right': xlwt.easyxf('borders: top medium, bottom medium, right medium')}
+
+    grade_color_palette = [["custom_dark_green",  0x20, (120, 241, 89)],
+                               ["custom_light_green", 0x21, (188, 241, 89)],
+                               ["custom_yellow",      0x22, (241, 226, 89)],
+                               ["custom_orange",      0x23, (241, 158, 89)],
+                               ["custom_red",         0x24, (241,  89, 89)]]
+
+    # Adding evaP colors to palette
+    for index, c in enumerate(grade_color_palette):
+        xlwt.add_palette_colour(c[0], c[1])
+        styles['grade_' + str(index)] = xlwt.easyxf('pattern: pattern solid, fore_colour '+c[0]+'; alignment: horiz centre; font: bold on; borders: left medium', num_format_str="0.0")
+
+
+    @classmethod
+    def add_color_palette_to_workbook(cls, workbook):
+        for c in cls.grade_color_palette:
+            workbook.set_colour_RGB(c[1], *c[2])
+
+    @staticmethod
+    def grade_to_style(grade):
+        rounded_grade = round(grade, 1)
+        if grade < 1.5:
+            return 'grade_0'
+        elif grade < 2.5:
+            return 'grade_1'
+        elif grade < 3.5:
+            return 'grade_2'
+        elif grade < 4.5:
+            return 'grade_3'
+        else:
+            return 'grade_4'
+
+    @staticmethod
+    def variance_to_style(variance):
+        rounded_variance = round(variance, 1)
+        if rounded_variance < 0.5:
+            return 'variance_low'
+        elif rounded_variance < 1.0:
+            return 'variance_med'
+        else:
+            return 'variance_high'
+
     def export(self, response, all=False):
         courses_with_results = list()
         for course in self.semester.course_set.filter(state="published").all():
@@ -38,67 +95,25 @@ def export(self, response, all=False):
         self.row = 0
         self.col = 0
 
-        # Adding evaP colors to palette
-        xlwt.add_palette_colour("custom_dark_green", 0x20)
-        self.workbook.set_colour_RGB(0x20, 120, 241, 89)
-        xlwt.add_palette_colour("custom_light_green", 0x21)
-        self.workbook.set_colour_RGB(0x21, 188, 241, 89)
-        xlwt.add_palette_colour("custom_yellow", 0x22)
-        self.workbook.set_colour_RGB(0x22, 241, 226, 89)
-        xlwt.add_palette_colour("custom_orange", 0x23)
-        self.workbook.set_colour_RGB(0x23, 241, 158, 89)
-        xlwt.add_palette_colour("custom_red", 0x24)
-        self.workbook.set_colour_RGB(0x24, 241, 89, 89)
-
-        # formatting for average grades
-        avg_style = xlwt.easyxf('alignment: horiz centre; font: bold on; borders: left medium, top medium, bottom medium')
-        avg_style_very_good = xlwt.easyxf('pattern: pattern solid, fore_colour custom_dark_green; alignment: horiz centre; font: bold on; borders: left medium', num_format_str="0.0")
-        avg_style_good = xlwt.easyxf('pattern: pattern solid, fore_colour custom_light_green; alignment: horiz centre; font: bold on; borders: left medium', num_format_str="0.0")
-        avg_style_medium = xlwt.easyxf('pattern: pattern solid, fore_colour custom_yellow; alignment: horiz centre; font: bold on; borders: left medium', num_format_str="0.0")
-        avg_style_bad = xlwt.easyxf('pattern: pattern solid, fore_colour custom_orange; alignment: horiz centre; font: bold on; borders: left medium', num_format_str="0.0")
-        avg_style_very_bad = xlwt.easyxf('pattern: pattern solid, fore_colour custom_red; alignment: horiz centre; font: bold on; borders: left medium', num_format_str="0.0")
-
-        # formatting for variances
-        var_style_good = xlwt.easyxf('alignment: horiz centre; borders: right medium', num_format_str="0.0")
-        var_style_medium = xlwt.easyxf('pattern: pattern solid, fore_colour gray25; alignment: horiz centre; borders: right medium', num_format_str="0.0")
-        var_style_bad = xlwt.easyxf('pattern: pattern solid, fore_colour gray40; alignment: horiz centre; borders: right medium', num_format_str="0.0")
-
-        # formatting for overall grades
-        over_style_very_good = xlwt.easyxf('pattern: pattern solid, fore_colour custom_dark_green; alignment: horiz centre; font: bold on; borders: left medium, right medium', num_format_str="0.0")
-        over_style_good = xlwt.easyxf('pattern: pattern solid, fore_colour custom_light_green; alignment: horiz centre; font: bold on; borders: left medium, right medium', num_format_str="0.0")
-        over_style_medium = xlwt.easyxf('pattern: pattern solid, fore_colour custom_yellow; alignment: horiz centre; font: bold on; borders: left medium, right medium', num_format_str="0.0")
-        over_style_bad = xlwt.easyxf('pattern: pattern solid, fore_colour custom_orange; alignment: horiz centre; font: bold on; borders: left medium, right medium', num_format_str="0.0")
-        over_style_very_bad = xlwt.easyxf('pattern: pattern solid, fore_colour custom_red; alignment: horiz centre; font: bold on; borders: left medium, right medium', num_format_str="0.0")
-
-        # formatting for special fields
-        headline_style = xlwt.easyxf('font: bold on, height 400; alignment: horiz centre, vert centre, wrap on', num_format_str="0.0")
-        course_style = xlwt.easyxf('alignment: horiz centre, wrap on, rota 90; borders: left medium, top medium')
-        course_unfinished_style = xlwt.easyxf('alignment: horiz centre, wrap on, rota 90; borders: left medium, top medium; font: italic on')
-        total_answers_style = xlwt.easyxf('alignment: horiz centre; borders: left medium, bottom medium, right medium')
-
-        # general formattings
-        bold_style = xlwt.easyxf('font: bold on')
-        border_left_style = xlwt.easyxf('borders: left medium')
-        border_right_style = xlwt.easyxf('borders: right medium')
-        border_top_bottom_right_style = xlwt.easyxf('borders: top medium, bottom medium, right medium')
-
-        self.writec(_(u"Evaluation {0} - created on {1}").format(self.semester.name, datetime.date.today()), headline_style)
+        
+        self.add_color_palette_to_workbook(self.workbook)
+
+        self.writec(_(u"Evaluation {0} - created on {1}").format(self.semester.name, datetime.date.today()), "headline")
         for course, results in courses_with_results:
             if course.state == "published":
-                self.writec(course.name, course_style, cols=2)
+                self.writec(course.name, "course", cols=2)
             else:
-                self.writec(course.name, course_unfinished_style, cols=2)
+                self.writec(course.name, "course_unfinished", cols=2)
 
         self.writen()
         for course, results in courses_with_results:
-            self.writec("Average", avg_style)
-            self.writec("Variance", border_top_bottom_right_style)
+            self.writec("Average", "avg")
+            self.writec("Variance", "border_top_bottom_right")
 
         for questionnaire in questionnaires:
-            self.writen(questionnaire.name, bold_style)
+            self.writen(questionnaire.name, "bold")
             for course, results in courses_with_results:
-                self.writec(None, border_left_style)
-                self.writec(None, border_right_style)
+                self.write_two_empty_cells_with_borders()
 
             for question_index, question in enumerate(questionnaire.question_set.all()):
                 if question.is_text_question():
@@ -123,93 +138,59 @@ def export(self, response, all=False):
                                     break
                         if values and (enough_answers or all):
                             avg = sum(values) / len(values)
-                            if avg < 1.5:
-                                self.writec(avg, avg_style_very_good)
-                            elif avg < 2.5:
-                                self.writec(avg, avg_style_good)
-                            elif avg < 3.5:
-                                self.writec(avg, avg_style_medium)
-                            elif avg < 4.5:
-                                self.writec(avg, avg_style_bad)
-                            else:
-                                self.writec(avg, avg_style_very_bad)
+                            self.writec(avg, ExcelExporter.grade_to_style(avg));
 
                             var = sum(variances) / len(variances)
-                            if var < 0.5:
-                                self.writec(var, var_style_good)
-                            elif var < 1:
-                                self.writec(var, var_style_medium)
-                            else:
-                                self.writec(var, var_style_bad)
+                            self.writec(var, ExcelExporter.variance_to_style(var))
                         else:
-                            self.writec(None, border_left_style)
-                            self.writec(None, border_right_style)
+                            self.write_two_empty_cells_with_borders()
                     else:
-                        self.writec(None, border_left_style)
-                        self.writec(None, border_right_style)
+                        self.write_two_empty_cells_with_borders()
             self.writen(None)
             for course, results in courses_with_results:
-                    self.writec(None, border_left_style)
-                    self.writec(None, border_right_style)
+                    self.write_two_empty_cells_with_borders()
 
-        self.writen(_(u"Overall Average Grade"), bold_style)
+        self.writen(_(u"Overall Average Grade"), "bold")
         for course, results in courses_with_results:
             avg, med = calculate_average_and_medium_grades(course)
             if avg:
-                if avg < 1.5:
-                    self.writec(avg, over_style_very_good, cols=2)
-                elif avg < 2.5:
-                    self.writec(avg, over_style_good, cols=2)
-                elif avg < 3.5:
-                    self.writec(avg, over_style_medium, cols=2)
-                elif avg < 4.5:
-                    self.writec(avg, over_style_bad, cols=2)
-                else:
-                    self.writec(avg, over_style_very_bad, cols=2)
+                self.writec(avg, ExcelExporter.grade_to_style(avg), cols=2)
             else:
-                self.writec(None, border_left_style)
-                self.writec(None, border_right_style)
+                self.write_two_empty_cells_with_borders()
 
-        self.writen(_(u"Overall Median Grade"), bold_style)
+        self.writen(_(u"Overall Median Grade"), "bold")
         for course, results in courses_with_results:
             avg, med = calculate_average_and_medium_grades(course)
             if med:
-                if med < 1.5:
-                    self.writec(med, over_style_very_good, cols=2)
-                elif med < 2.5:
-                    self.writec(med, over_style_good, cols=2)
-                elif med < 3.5:
-                    self.writec(med, over_style_medium, cols=2)
-                elif med < 4.5:
-                    self.writec(med, over_style_bad, cols=2)
-                else:
-                    self.writec(med, over_style_very_bad, cols=2)
+                self.writec(med, ExcelExporter.grade_to_style(med), cols=2)
             else:
-                self.writec(None, border_left_style)
-                self.writec(None, border_right_style)
+                self.write_two_empty_cells_with_borders()
 
-        self.writen(_(u"Total Answers"), bold_style)
+        self.writen(_(u"Total Answers"), "bold")
         for course, results in courses_with_results:
-            self.writec(course.num_voters, total_answers_style, cols=2)
+            self.writec(course.num_voters, "total_answers", cols=2)
 
         self.workbook.save(response)
 
-    def writen(self, *args, **kwargs):
+
+    def write_two_empty_cells_with_borders(self):
+        self.writec(None, "border_left")
+        self.writec(None, "border_right")
+
+    def writen(self, label="", style_name="default"):
         """Write the cell at the beginning of the next row."""
         self.col = 0
         self.row += 1
-        self.writec(*args, **kwargs)
+        self.writec(label, style_name)
 
-    def writec(self, *args, **kwargs):
+    def writec(self, label, style_name, rows=1, cols=1):
         """Write the cell in the next column of the current line."""
-        self._write(*args, **kwargs)
+        self._write(label, ExcelExporter.styles[style_name], rows, cols )
         self.col += 1
 
-    def _write(self, *args, **kwargs):
-        rows = kwargs.pop('rows', 1)
-        cols = kwargs.pop('cols', 1)
+    def _write(self, label, style, rows, cols):
         if rows > 1 or cols > 1:
-            self.sheet.write_merge(self.row, self.row+rows-1, self.col, self.col+cols-1, *args, **kwargs)
+            self.sheet.write_merge(self.row, self.row+rows-1, self.col, self.col+cols-1, label, style)
             self.col += cols - 1
         else:
-            self.sheet.write(self.row, self.col, *args, **kwargs)
+            self.sheet.write(self.row, self.col, label, style)


Test patch: