We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Disable results link for courses without published votes
Currently in the results overview, you can click on courses which have no published votes. The course page is not helpful then, so the link should be disabled.

if the course has comments but no votes, the link should be enabled for the respective contributors.


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `evap/results/views.py`
Content:
```
1 from django.conf import settings
2 from django.http import HttpResponse
3 from django.shortcuts import get_object_or_404, render_to_response
4 from django.template import RequestContext
5 from django.utils.translation import get_language
6 
7 from evap.evaluation.auth import login_required, fsr_required
8 from evap.evaluation.models import Semester
9 from evap.evaluation.tools import calculate_results, calculate_average_grade, TextResult
10 
11 from evap.results.exporters import ExcelExporter
12 
13 
14 @login_required
15 def index(request):
16     semesters = Semester.get_all_with_published_courses()
17 
18     return render_to_response(
19         "results_index.html",
20         dict(semesters=semesters),
21         context_instance=RequestContext(request))
22 
23 
24 @login_required
25 def semester_detail(request, semester_id):
26     semester = get_object_or_404(Semester, id=semester_id)
27     courses = list(semester.course_set.filter(state="published"))
28 
29     # annotate each course object with its grade
30     for course in courses:
31         # first, make sure that there is no preexisting grade attribute
32         course.grade = calculate_average_grade(course)
33 
34     return render_to_response(
35         "results_semester_detail.html",
36         dict(
37             semester=semester,
38             courses=courses
39         ),
40         context_instance=RequestContext(request))
41 
42 
43 @fsr_required
44 def semester_export(request, semester_id):
45     semester = get_object_or_404(Semester, id=semester_id)
46 
47     filename = "Evaluation-%s-%s.xls" % (semester.name, get_language())
48 
49     response = HttpResponse(mimetype="application/vnd.ms-excel")
50     response["Content-Disposition"] = "attachment; filename=\"%s\"" % filename
51 
52     exporter = ExcelExporter(semester)
53 
54     if 'all' in request.GET:
55         exporter.export(response, True)
56     else:
57         exporter.export(response)
58 
59     return response
60 
61 
62 @login_required
63 def course_detail(request, semester_id, course_id):
64     semester = get_object_or_404(Semester, id=semester_id)
65     course = get_object_or_404(semester.course_set.filter(state="published"), id=course_id)
66 
67     sections = calculate_results(course, request.user.is_staff)
68 
69     if (request.user.is_staff == False): # if user is not a student representative
70     # remove TextResults if user is neither the evaluated person (or a delegate) nor responsible for the course (or a delegate)
71         for section in sections:
72             if not user_can_see_textresults(request.user, course, section):
73                 for index, result in list(enumerate(section.results))[::-1]:
74                     if isinstance(section.results[index], TextResult):
75                         del section.results[index]
76 
77         # remove empty sections
78         sections = [section for section in sections if section.results]
79 
80     # check whether results are published
81     published = course.num_voters >= settings.MIN_ANSWER_COUNT and float(course.num_voters) / course.num_participants >= settings.MIN_ANSWER_PERCENTAGE
82 
83     # show a publishing warning to fsr members when the results are not publicly available
84     warning = (not published) and request.user.is_staff
85 
86     return render_to_response(
87         "results_course_detail.html",
88         dict(
89             course=course,
90             sections=sections,
91             warning=warning
92         ),
93         context_instance=RequestContext(request))
94 
95 
96 def user_can_see_textresults(user, course, section):
97     if section.contributor == user:
98         return True
99     if course.is_user_responsible_or_delegate(user):
100         return True
101 
102     represented_userprofiles = user.represented_users.all()
103     represented_users = [profile.user for profile in represented_userprofiles]
104     if section.contributor in represented_users:
105         return True
106 
107     return False
108
```

Path: `evap/evaluation/tools.py`
Content:
```
1 from django.conf import settings
2 from django.core.cache import cache
3 from django.db.models import Min, Count
4 from django.utils.datastructures import SortedDict
5 from django.utils.translation import ugettext_lazy as _
6 from evap.evaluation.models import GradeAnswer, TextAnswer
7 
8 
9 from collections import namedtuple
10 
11 GRADE_NAMES = {
12     1: _(u"Strongly agree"),
13     2: _(u"Agree"),
14     3: _(u"Neither agree nor disagree"),
15     4: _(u"Disagree"),
16     5: _(u"Strongly disagree"),
17     6: _(u"no answer"),
18 }
19 
20 STATES_ORDERED = SortedDict((
21     ('new', _('new')),
22     ('prepared', _('prepared')),
23     ('lecturerApproved', _('lecturer approved')),
24     ('approved', _('approved')),
25     ('inEvaluation', _('in evaluation')),
26     ('evaluated', _('evaluated')),
27     ('reviewed', _('reviewed')),
28     ('published', _('published'))
29 ))
30 
31 
32 # see calculate_results
33 ResultSection = namedtuple('ResultSection', ('questionnaire', 'contributor', 'results', 'average'))
34 GradeResult = namedtuple('GradeResult', ('question', 'count', 'average', 'variance', 'distribution', 'show'))
35 TextResult = namedtuple('TextResult', ('question', 'texts'))
36 
37 
38 def avg(iterable):
39     """Simple arithmetic average function. Returns `None` if the length of
40     `iterable` is 0 or no items except None exist."""
41     items = [item for item in iterable if item is not None]
42     if len(items) == 0:
43         return None
44     return float(sum(items)) / len(items)
45 
46 
47 def calculate_results(course, staff_member=False):
48     """Calculates the result data for a single course. Returns a list of
49     `ResultSection` tuples. Each of those tuples contains the questionnaire, the
50     contributor (or None), a list of single result elements and the average grade
51     for that section (or None). The result elements are either `GradeResult` or
52     `TextResult` instances."""
53 
54     # return cached results if available
55     cache_key = str.format('evap.fsr.results.views.calculate_results-{:d}-{:d}', course.id, staff_member)
56     prior_results = cache.get(cache_key)
57     if prior_results:
58         return prior_results
59 
60     # check if grades for the course will be published
61     show = staff_member or (course.num_voters >= settings.MIN_ANSWER_COUNT and float(course.num_voters) / course.num_participants >= settings.MIN_ANSWER_PERCENTAGE)
62 
63     # there will be one section per relevant questionnaire--contributor pair
64     sections = []
65 
66     for questionnaire, contribution in questionnaires_and_contributions(course):
67         # will contain one object per question
68         results = []
69         for question in questionnaire.question_set.all():
70             if question.is_grade_question():
71                 # gather all numeric answers as a simple list
72                 answers = GradeAnswer.objects.filter(
73                     contribution__course=course,
74                     contribution__contributor=contribution.contributor,
75                     question=question
76                     ).values_list('answer', flat=True)
77 
78                 # calculate average and distribution
79                 if answers:
80                     # average
81                     average = avg(answers)
82                     # variance
83                     variance = avg((average - answer) ** 2 for answer in answers)
84                     # calculate relative distribution (histogram) of answers:
85                     # set up a sorted dictionary with a count of zero for each grade
86                     distribution = SortedDict()
87                     for i in range(1, 6):
88                         distribution[i] = 0
89                     # count the answers
90                     for answer in answers:
91                         distribution[answer] += 1
92                     # divide by the number of answers to get relative 0..1 values
93                     for k in distribution:
94                         distribution[k] = float(distribution[k]) / len(answers) * 100.0
95                 else:
96                     average = None
97                     variance = None
98                     distribution = None
99 
100                 # produce the result element
101                 results.append(GradeResult(
102                     question=question,
103                     count=len(answers),
104                     average=average,
105                     variance=variance,
106                     distribution=distribution,
107                     show=show
108                 ))
109 
110             elif question.is_text_question():
111                 # gather text answers for this question
112                 answers = TextAnswer.objects.filter(
113                     contribution__course=course,
114                     contribution__contributor=contribution.contributor,
115                     question=question,
116                     hidden=False
117                     )
118                 # only add to the results if answers exist at all
119                 if answers:
120                     results.append(TextResult(
121                         question=question,
122                         texts=[answer.answer for answer in answers]
123                     ))
124 
125         # skip section if there were no questions with results
126         if not results:
127             continue
128 
129         # compute average grade for this section, will return None if
130         # no GradeResults exist in this section
131         average_grade = avg([result.average for result
132                                             in results
133                                             if isinstance(result, GradeResult)])
134         sections.append(ResultSection(questionnaire, contribution.contributor, results, average_grade))
135 
136     # store results into cache
137     # XXX: What would be a good timeout here? Once public, data is not going to
138     #      change anyway.
139     cache.set(cache_key, sections, 24 * 60 * 60)
140 
141     return sections
142 
143 
144 def calculate_average_grade(course):
145     """Determines the final grade for a course."""
146     generic_grades = []
147     personal_grades = []
148 
149     for questionnaire, contributor, results, average in calculate_results(course):
150         if average:
151             (personal_grades if contributor else generic_grades).append(average)
152 
153     if not generic_grades:
154         # not final grade without any generic grade
155         return None
156     elif not personal_grades:
157         # determine final grade by using the average of the generic grades
158         return avg(generic_grades)
159     else:
160         # determine final grade by building the equally-weighted average of the
161         # generic and person-specific averages
162         return avg((avg(generic_grades), avg(personal_grades)))
163 
164 
165 def questionnaires_and_contributions(course):
166     """Yields tuples of (questionnaire, contribution) for the given course."""
167     result = []
168 
169     for contribution in course.contributions.annotate(Min("questionnaires__index")).order_by("questionnaires__is_for_contributors", "questionnaires__index__min"):
170         for questionnaire in contribution.questionnaires.all():
171             result.append((questionnaire, contribution))
172 
173     # sort questionnaires without contributors first
174     result.sort(key=lambda t: t[1].contributor is not None)
175 
176     return result
177
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/evap/evaluation/tools.py b/evap/evaluation/tools.py
--- a/evap/evaluation/tools.py
+++ b/evap/evaluation/tools.py
@@ -44,6 +44,9 @@
     return float(sum(items)) / len(items)
 
 
+def can_publish_grades(course, staff_member=False):
+    return staff_member or (course.num_voters >= settings.MIN_ANSWER_COUNT and float(course.num_voters) / course.num_participants >= settings.MIN_ANSWER_PERCENTAGE)
+
 def calculate_results(course, staff_member=False):
     """Calculates the result data for a single course. Returns a list of
     `ResultSection` tuples. Each of those tuples contains the questionnaire, the
@@ -58,7 +61,7 @@
         return prior_results
 
     # check if grades for the course will be published
-    show = staff_member or (course.num_voters >= settings.MIN_ANSWER_COUNT and float(course.num_voters) / course.num_participants >= settings.MIN_ANSWER_PERCENTAGE)
+    show = can_publish_grades(course, staff_member)
 
     # there will be one section per relevant questionnaire--contributor pair
     sections = []
diff --git a/evap/results/views.py b/evap/results/views.py
--- a/evap/results/views.py
+++ b/evap/results/views.py
@@ -6,7 +6,7 @@
 
 from evap.evaluation.auth import login_required, fsr_required
 from evap.evaluation.models import Semester
-from evap.evaluation.tools import calculate_results, calculate_average_grade, TextResult
+from evap.evaluation.tools import calculate_results, calculate_average_grade, TextResult, can_publish_grades
 
 from evap.results.exporters import ExcelExporter
 
@@ -30,6 +30,7 @@
     for course in courses:
         # first, make sure that there is no preexisting grade attribute
         course.grade = calculate_average_grade(course)
+        course.can_publish_grades = can_publish_grades(course, request.user.is_staff)
 
     return render_to_response(
         "results_semester_detail.html",

{"golden_diff": "diff --git a/evap/evaluation/tools.py b/evap/evaluation/tools.py\n--- a/evap/evaluation/tools.py\n+++ b/evap/evaluation/tools.py\n@@ -44,6 +44,9 @@\n     return float(sum(items)) / len(items)\n \n \n+def can_publish_grades(course, staff_member=False):\n+    return staff_member or (course.num_voters >= settings.MIN_ANSWER_COUNT and float(course.num_voters) / course.num_participants >= settings.MIN_ANSWER_PERCENTAGE)\n+\n def calculate_results(course, staff_member=False):\n     \"\"\"Calculates the result data for a single course. Returns a list of\n     `ResultSection` tuples. Each of those tuples contains the questionnaire, the\n@@ -58,7 +61,7 @@\n         return prior_results\n \n     # check if grades for the course will be published\n-    show = staff_member or (course.num_voters >= settings.MIN_ANSWER_COUNT and float(course.num_voters) / course.num_participants >= settings.MIN_ANSWER_PERCENTAGE)\n+    show = can_publish_grades(course, staff_member)\n \n     # there will be one section per relevant questionnaire--contributor pair\n     sections = []\ndiff --git a/evap/results/views.py b/evap/results/views.py\n--- a/evap/results/views.py\n+++ b/evap/results/views.py\n@@ -6,7 +6,7 @@\n \n from evap.evaluation.auth import login_required, fsr_required\n from evap.evaluation.models import Semester\n-from evap.evaluation.tools import calculate_results, calculate_average_grade, TextResult\n+from evap.evaluation.tools import calculate_results, calculate_average_grade, TextResult, can_publish_grades\n \n from evap.results.exporters import ExcelExporter\n \n@@ -30,6 +30,7 @@\n     for course in courses:\n         # first, make sure that there is no preexisting grade attribute\n         course.grade = calculate_average_grade(course)\n+        course.can_publish_grades = can_publish_grades(course, request.user.is_staff)\n \n     return render_to_response(\n         \"results_semester_detail.html\",\n", "issue": "Disable results link for courses without published votes\nCurrently in the results overview, you can click on courses which have no published votes. The course page is not helpful then, so the link should be disabled.\n\nif the course has comments but no votes, the link should be enabled for the respective contributors.\n\n", "before_files": [{"content": "from django.conf import settings\nfrom django.http import HttpResponse\nfrom django.shortcuts import get_object_or_404, render_to_response\nfrom django.template import RequestContext\nfrom django.utils.translation import get_language\n\nfrom evap.evaluation.auth import login_required, fsr_required\nfrom evap.evaluation.models import Semester\nfrom evap.evaluation.tools import calculate_results, calculate_average_grade, TextResult\n\nfrom evap.results.exporters import ExcelExporter\n\n\n@login_required\ndef index(request):\n    semesters = Semester.get_all_with_published_courses()\n\n    return render_to_response(\n        \"results_index.html\",\n        dict(semesters=semesters),\n        context_instance=RequestContext(request))\n\n\n@login_required\ndef semester_detail(request, semester_id):\n    semester = get_object_or_404(Semester, id=semester_id)\n    courses = list(semester.course_set.filter(state=\"published\"))\n\n    # annotate each course object with its grade\n    for course in courses:\n        # first, make sure that there is no preexisting grade attribute\n        course.grade = calculate_average_grade(course)\n\n    return render_to_response(\n        \"results_semester_detail.html\",\n        dict(\n            semester=semester,\n            courses=courses\n        ),\n        context_instance=RequestContext(request))\n\n\n@fsr_required\ndef semester_export(request, semester_id):\n    semester = get_object_or_404(Semester, id=semester_id)\n\n    filename = \"Evaluation-%s-%s.xls\" % (semester.name, get_language())\n\n    response = HttpResponse(mimetype=\"application/vnd.ms-excel\")\n    response[\"Content-Disposition\"] = \"attachment; filename=\\\"%s\\\"\" % filename\n\n    exporter = ExcelExporter(semester)\n\n    if 'all' in request.GET:\n        exporter.export(response, True)\n    else:\n        exporter.export(response)\n\n    return response\n\n\n@login_required\ndef course_detail(request, semester_id, course_id):\n    semester = get_object_or_404(Semester, id=semester_id)\n    course = get_object_or_404(semester.course_set.filter(state=\"published\"), id=course_id)\n\n    sections = calculate_results(course, request.user.is_staff)\n\n    if (request.user.is_staff == False): # if user is not a student representative\n    # remove TextResults if user is neither the evaluated person (or a delegate) nor responsible for the course (or a delegate)\n        for section in sections:\n            if not user_can_see_textresults(request.user, course, section):\n                for index, result in list(enumerate(section.results))[::-1]:\n                    if isinstance(section.results[index], TextResult):\n                        del section.results[index]\n\n        # remove empty sections\n        sections = [section for section in sections if section.results]\n\n    # check whether results are published\n    published = course.num_voters >= settings.MIN_ANSWER_COUNT and float(course.num_voters) / course.num_participants >= settings.MIN_ANSWER_PERCENTAGE\n\n    # show a publishing warning to fsr members when the results are not publicly available\n    warning = (not published) and request.user.is_staff\n\n    return render_to_response(\n        \"results_course_detail.html\",\n        dict(\n            course=course,\n            sections=sections,\n            warning=warning\n        ),\n        context_instance=RequestContext(request))\n\n\ndef user_can_see_textresults(user, course, section):\n    if section.contributor == user:\n        return True\n    if course.is_user_responsible_or_delegate(user):\n        return True\n\n    represented_userprofiles = user.represented_users.all()\n    represented_users = [profile.user for profile in represented_userprofiles]\n    if section.contributor in represented_users:\n        return True\n\n    return False\n", "path": "evap/results/views.py"}, {"content": "from django.conf import settings\nfrom django.core.cache import cache\nfrom django.db.models import Min, Count\nfrom django.utils.datastructures import SortedDict\nfrom django.utils.translation import ugettext_lazy as _\nfrom evap.evaluation.models import GradeAnswer, TextAnswer\n\n\nfrom collections import namedtuple\n\nGRADE_NAMES = {\n    1: _(u\"Strongly agree\"),\n    2: _(u\"Agree\"),\n    3: _(u\"Neither agree nor disagree\"),\n    4: _(u\"Disagree\"),\n    5: _(u\"Strongly disagree\"),\n    6: _(u\"no answer\"),\n}\n\nSTATES_ORDERED = SortedDict((\n    ('new', _('new')),\n    ('prepared', _('prepared')),\n    ('lecturerApproved', _('lecturer approved')),\n    ('approved', _('approved')),\n    ('inEvaluation', _('in evaluation')),\n    ('evaluated', _('evaluated')),\n    ('reviewed', _('reviewed')),\n    ('published', _('published'))\n))\n\n\n# see calculate_results\nResultSection = namedtuple('ResultSection', ('questionnaire', 'contributor', 'results', 'average'))\nGradeResult = namedtuple('GradeResult', ('question', 'count', 'average', 'variance', 'distribution', 'show'))\nTextResult = namedtuple('TextResult', ('question', 'texts'))\n\n\ndef avg(iterable):\n    \"\"\"Simple arithmetic average function. Returns `None` if the length of\n    `iterable` is 0 or no items except None exist.\"\"\"\n    items = [item for item in iterable if item is not None]\n    if len(items) == 0:\n        return None\n    return float(sum(items)) / len(items)\n\n\ndef calculate_results(course, staff_member=False):\n    \"\"\"Calculates the result data for a single course. Returns a list of\n    `ResultSection` tuples. Each of those tuples contains the questionnaire, the\n    contributor (or None), a list of single result elements and the average grade\n    for that section (or None). The result elements are either `GradeResult` or\n    `TextResult` instances.\"\"\"\n\n    # return cached results if available\n    cache_key = str.format('evap.fsr.results.views.calculate_results-{:d}-{:d}', course.id, staff_member)\n    prior_results = cache.get(cache_key)\n    if prior_results:\n        return prior_results\n\n    # check if grades for the course will be published\n    show = staff_member or (course.num_voters >= settings.MIN_ANSWER_COUNT and float(course.num_voters) / course.num_participants >= settings.MIN_ANSWER_PERCENTAGE)\n\n    # there will be one section per relevant questionnaire--contributor pair\n    sections = []\n\n    for questionnaire, contribution in questionnaires_and_contributions(course):\n        # will contain one object per question\n        results = []\n        for question in questionnaire.question_set.all():\n            if question.is_grade_question():\n                # gather all numeric answers as a simple list\n                answers = GradeAnswer.objects.filter(\n                    contribution__course=course,\n                    contribution__contributor=contribution.contributor,\n                    question=question\n                    ).values_list('answer', flat=True)\n\n                # calculate average and distribution\n                if answers:\n                    # average\n                    average = avg(answers)\n                    # variance\n                    variance = avg((average - answer) ** 2 for answer in answers)\n                    # calculate relative distribution (histogram) of answers:\n                    # set up a sorted dictionary with a count of zero for each grade\n                    distribution = SortedDict()\n                    for i in range(1, 6):\n                        distribution[i] = 0\n                    # count the answers\n                    for answer in answers:\n                        distribution[answer] += 1\n                    # divide by the number of answers to get relative 0..1 values\n                    for k in distribution:\n                        distribution[k] = float(distribution[k]) / len(answers) * 100.0\n                else:\n                    average = None\n                    variance = None\n                    distribution = None\n\n                # produce the result element\n                results.append(GradeResult(\n                    question=question,\n                    count=len(answers),\n                    average=average,\n                    variance=variance,\n                    distribution=distribution,\n                    show=show\n                ))\n\n            elif question.is_text_question():\n                # gather text answers for this question\n                answers = TextAnswer.objects.filter(\n                    contribution__course=course,\n                    contribution__contributor=contribution.contributor,\n                    question=question,\n                    hidden=False\n                    )\n                # only add to the results if answers exist at all\n                if answers:\n                    results.append(TextResult(\n                        question=question,\n                        texts=[answer.answer for answer in answers]\n                    ))\n\n        # skip section if there were no questions with results\n        if not results:\n            continue\n\n        # compute average grade for this section, will return None if\n        # no GradeResults exist in this section\n        average_grade = avg([result.average for result\n                                            in results\n                                            if isinstance(result, GradeResult)])\n        sections.append(ResultSection(questionnaire, contribution.contributor, results, average_grade))\n\n    # store results into cache\n    # XXX: What would be a good timeout here? Once public, data is not going to\n    #      change anyway.\n    cache.set(cache_key, sections, 24 * 60 * 60)\n\n    return sections\n\n\ndef calculate_average_grade(course):\n    \"\"\"Determines the final grade for a course.\"\"\"\n    generic_grades = []\n    personal_grades = []\n\n    for questionnaire, contributor, results, average in calculate_results(course):\n        if average:\n            (personal_grades if contributor else generic_grades).append(average)\n\n    if not generic_grades:\n        # not final grade without any generic grade\n        return None\n    elif not personal_grades:\n        # determine final grade by using the average of the generic grades\n        return avg(generic_grades)\n    else:\n        # determine final grade by building the equally-weighted average of the\n        # generic and person-specific averages\n        return avg((avg(generic_grades), avg(personal_grades)))\n\n\ndef questionnaires_and_contributions(course):\n    \"\"\"Yields tuples of (questionnaire, contribution) for the given course.\"\"\"\n    result = []\n\n    for contribution in course.contributions.annotate(Min(\"questionnaires__index\")).order_by(\"questionnaires__is_for_contributors\", \"questionnaires__index__min\"):\n        for questionnaire in contribution.questionnaires.all():\n            result.append((questionnaire, contribution))\n\n    # sort questionnaires without contributors first\n    result.sort(key=lambda t: t[1].contributor is not None)\n\n    return result\n", "path": "evap/evaluation/tools.py"}], "after_files": [{"content": "from django.conf import settings\nfrom django.http import HttpResponse\nfrom django.shortcuts import get_object_or_404, render_to_response\nfrom django.template import RequestContext\nfrom django.utils.translation import get_language\n\nfrom evap.evaluation.auth import login_required, fsr_required\nfrom evap.evaluation.models import Semester\nfrom evap.evaluation.tools import calculate_results, calculate_average_grade, TextResult, can_publish_grades\n\nfrom evap.results.exporters import ExcelExporter\n\n\n@login_required\ndef index(request):\n    semesters = Semester.get_all_with_published_courses()\n\n    return render_to_response(\n        \"results_index.html\",\n        dict(semesters=semesters),\n        context_instance=RequestContext(request))\n\n\n@login_required\ndef semester_detail(request, semester_id):\n    semester = get_object_or_404(Semester, id=semester_id)\n    courses = list(semester.course_set.filter(state=\"published\"))\n\n    # annotate each course object with its grade\n    for course in courses:\n        # first, make sure that there is no preexisting grade attribute\n        course.grade = calculate_average_grade(course)\n        course.can_publish_grades = can_publish_grades(course, request.user.is_staff)\n\n    return render_to_response(\n        \"results_semester_detail.html\",\n        dict(\n            semester=semester,\n            courses=courses\n        ),\n        context_instance=RequestContext(request))\n\n\n@fsr_required\ndef semester_export(request, semester_id):\n    semester = get_object_or_404(Semester, id=semester_id)\n\n    filename = \"Evaluation-%s-%s.xls\" % (semester.name, get_language())\n\n    response = HttpResponse(mimetype=\"application/vnd.ms-excel\")\n    response[\"Content-Disposition\"] = \"attachment; filename=\\\"%s\\\"\" % filename\n\n    exporter = ExcelExporter(semester)\n\n    if 'all' in request.GET:\n        exporter.export(response, True)\n    else:\n        exporter.export(response)\n\n    return response\n\n\n@login_required\ndef course_detail(request, semester_id, course_id):\n    semester = get_object_or_404(Semester, id=semester_id)\n    course = get_object_or_404(semester.course_set.filter(state=\"published\"), id=course_id)\n\n    sections = calculate_results(course, request.user.is_staff)\n\n    if (request.user.is_staff == False): # if user is not a student representative\n    # remove TextResults if user is neither the evaluated person (or a delegate) nor responsible for the course (or a delegate)\n        for section in sections:\n            if not user_can_see_textresults(request.user, course, section):\n                for index, result in list(enumerate(section.results))[::-1]:\n                    if isinstance(section.results[index], TextResult):\n                        del section.results[index]\n\n        # remove empty sections\n        sections = [section for section in sections if section.results]\n\n    # check whether results are published\n    published = course.num_voters >= settings.MIN_ANSWER_COUNT and float(course.num_voters) / course.num_participants >= settings.MIN_ANSWER_PERCENTAGE\n\n    # show a publishing warning to fsr members when the results are not publicly available\n    warning = (not published) and request.user.is_staff\n\n    return render_to_response(\n        \"results_course_detail.html\",\n        dict(\n            course=course,\n            sections=sections,\n            warning=warning\n        ),\n        context_instance=RequestContext(request))\n\n\ndef user_can_see_textresults(user, course, section):\n    if section.contributor == user:\n        return True\n    if course.is_user_responsible_or_delegate(user):\n        return True\n\n    represented_userprofiles = user.represented_users.all()\n    represented_users = [profile.user for profile in represented_userprofiles]\n    if section.contributor in represented_users:\n        return True\n\n    return False\n", "path": "evap/results/views.py"}, {"content": "from django.conf import settings\nfrom django.core.cache import cache\nfrom django.db.models import Min, Count\nfrom django.utils.datastructures import SortedDict\nfrom django.utils.translation import ugettext_lazy as _\nfrom evap.evaluation.models import GradeAnswer, TextAnswer\n\n\nfrom collections import namedtuple\n\nGRADE_NAMES = {\n    1: _(u\"Strongly agree\"),\n    2: _(u\"Agree\"),\n    3: _(u\"Neither agree nor disagree\"),\n    4: _(u\"Disagree\"),\n    5: _(u\"Strongly disagree\"),\n    6: _(u\"no answer\"),\n}\n\nSTATES_ORDERED = SortedDict((\n    ('new', _('new')),\n    ('prepared', _('prepared')),\n    ('lecturerApproved', _('lecturer approved')),\n    ('approved', _('approved')),\n    ('inEvaluation', _('in evaluation')),\n    ('evaluated', _('evaluated')),\n    ('reviewed', _('reviewed')),\n    ('published', _('published'))\n))\n\n\n# see calculate_results\nResultSection = namedtuple('ResultSection', ('questionnaire', 'contributor', 'results', 'average'))\nGradeResult = namedtuple('GradeResult', ('question', 'count', 'average', 'variance', 'distribution', 'show'))\nTextResult = namedtuple('TextResult', ('question', 'texts'))\n\n\ndef avg(iterable):\n    \"\"\"Simple arithmetic average function. Returns `None` if the length of\n    `iterable` is 0 or no items except None exist.\"\"\"\n    items = [item for item in iterable if item is not None]\n    if len(items) == 0:\n        return None\n    return float(sum(items)) / len(items)\n\n\ndef can_publish_grades(course, staff_member=False):\n    return staff_member or (course.num_voters >= settings.MIN_ANSWER_COUNT and float(course.num_voters) / course.num_participants >= settings.MIN_ANSWER_PERCENTAGE)\n\ndef calculate_results(course, staff_member=False):\n    \"\"\"Calculates the result data for a single course. Returns a list of\n    `ResultSection` tuples. Each of those tuples contains the questionnaire, the\n    contributor (or None), a list of single result elements and the average grade\n    for that section (or None). The result elements are either `GradeResult` or\n    `TextResult` instances.\"\"\"\n\n    # return cached results if available\n    cache_key = str.format('evap.fsr.results.views.calculate_results-{:d}-{:d}', course.id, staff_member)\n    prior_results = cache.get(cache_key)\n    if prior_results:\n        return prior_results\n\n    # check if grades for the course will be published\n    show = can_publish_grades(course, staff_member)\n\n    # there will be one section per relevant questionnaire--contributor pair\n    sections = []\n\n    for questionnaire, contribution in questionnaires_and_contributions(course):\n        # will contain one object per question\n        results = []\n        for question in questionnaire.question_set.all():\n            if question.is_grade_question():\n                # gather all numeric answers as a simple list\n                answers = GradeAnswer.objects.filter(\n                    contribution__course=course,\n                    contribution__contributor=contribution.contributor,\n                    question=question\n                    ).values_list('answer', flat=True)\n\n                # calculate average and distribution\n                if answers:\n                    # average\n                    average = avg(answers)\n                    # variance\n                    variance = avg((average - answer) ** 2 for answer in answers)\n                    # calculate relative distribution (histogram) of answers:\n                    # set up a sorted dictionary with a count of zero for each grade\n                    distribution = SortedDict()\n                    for i in range(1, 6):\n                        distribution[i] = 0\n                    # count the answers\n                    for answer in answers:\n                        distribution[answer] += 1\n                    # divide by the number of answers to get relative 0..1 values\n                    for k in distribution:\n                        distribution[k] = float(distribution[k]) / len(answers) * 100.0\n                else:\n                    average = None\n                    variance = None\n                    distribution = None\n\n                # produce the result element\n                results.append(GradeResult(\n                    question=question,\n                    count=len(answers),\n                    average=average,\n                    variance=variance,\n                    distribution=distribution,\n                    show=show\n                ))\n\n            elif question.is_text_question():\n                # gather text answers for this question\n                answers = TextAnswer.objects.filter(\n                    contribution__course=course,\n                    contribution__contributor=contribution.contributor,\n                    question=question,\n                    hidden=False\n                    )\n                # only add to the results if answers exist at all\n                if answers:\n                    results.append(TextResult(\n                        question=question,\n                        texts=[answer.answer for answer in answers]\n                    ))\n\n        # skip section if there were no questions with results\n        if not results:\n            continue\n\n        # compute average grade for this section, will return None if\n        # no GradeResults exist in this section\n        average_grade = avg([result.average for result\n                                            in results\n                                            if isinstance(result, GradeResult)])\n        sections.append(ResultSection(questionnaire, contribution.contributor, results, average_grade))\n\n    # store results into cache\n    # XXX: What would be a good timeout here? Once public, data is not going to\n    #      change anyway.\n    cache.set(cache_key, sections, 24 * 60 * 60)\n\n    return sections\n\n\ndef calculate_average_grade(course):\n    \"\"\"Determines the final grade for a course.\"\"\"\n    generic_grades = []\n    personal_grades = []\n\n    for questionnaire, contributor, results, average in calculate_results(course):\n        if average:\n            (personal_grades if contributor else generic_grades).append(average)\n\n    if not generic_grades:\n        # not final grade without any generic grade\n        return None\n    elif not personal_grades:\n        # determine final grade by using the average of the generic grades\n        return avg(generic_grades)\n    else:\n        # determine final grade by building the equally-weighted average of the\n        # generic and person-specific averages\n        return avg((avg(generic_grades), avg(personal_grades)))\n\n\ndef questionnaires_and_contributions(course):\n    \"\"\"Yields tuples of (questionnaire, contribution) for the given course.\"\"\"\n    result = []\n\n    for contribution in course.contributions.annotate(Min(\"questionnaires__index\")).order_by(\"questionnaires__is_for_contributors\", \"questionnaires__index__min\"):\n        for questionnaire in contribution.questionnaires.all():\n            result.append((questionnaire, contribution))\n\n    # sort questionnaires without contributors first\n    result.sort(key=lambda t: t[1].contributor is not None)\n\n    return result\n", "path": "evap/evaluation/tools.py"}]}