We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
users should not be able to get 1000% on an exam, unfortunately

### Observed behavior

reported by @jtamiace re: @radinamatic's apparent good luck:

![image](https://user-images.githubusercontent.com/2367265/52095361-812c2280-2577-11e9-86d6-8f6cff4e0f5b.png)

### Expected behavior

exams are scored between 0 and 100

### User-facing consequences

????

### Errors and logs

unknown

### Steps to reproduce

see http://kolibribeta.learningequality.org/coach/#/fa4cbfeda32c0c0fbf1832fc1ddd10c3/reports/learners

### Context

k 0.12.0 alpha 7


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `kolibri/plugins/coach/class_summary_api.py`
Content:
```
1 from django.db.models import Max
2 from django.db.models import Sum
3 from django.shortcuts import get_object_or_404
4 from rest_framework import serializers
5 from rest_framework import viewsets
6 from rest_framework.response import Response
7 
8 from kolibri.core.auth import models as auth_models
9 from kolibri.core.content.models import ContentNode
10 from kolibri.core.exams.models import Exam
11 from kolibri.core.lessons.models import Lesson
12 from kolibri.core.logger import models as logger_models
13 from kolibri.core.notifications.models import LearnerProgressNotification
14 from kolibri.core.notifications.models import NotificationEventType
15 
16 
17 # Intended to match  NotificationEventType
18 NOT_STARTED = "NotStarted"
19 STARTED = "Started"
20 HELP_NEEDED = "HelpNeeded"
21 COMPLETED = "Completed"
22 
23 
24 def content_status_serializer(lesson_data, learners_data, classroom):
25 
26     # First generate a unique set of content node ids from all the lessons
27     lesson_node_ids = set()
28     for lesson in lesson_data:
29         lesson_node_ids |= set(lesson.get("node_ids"))
30 
31     # Now create a map of content_id to node_id so that we can map between lessons, and notifications
32     # which use the node id, and summary logs, which use content_id
33     content_map = {n[0]: n[1] for n in ContentNode.objects.filter(id__in=lesson_node_ids).values_list("content_id", "id")}
34 
35     # Get all the values we need from the summary logs to be able to summarize current status on the
36     # relevant content items.
37     content_log_values = logger_models.ContentSummaryLog.objects.filter(
38         content_id__in=set(content_map.keys()), user__in=[learner["id"] for learner in learners_data]
39     ).values("user_id", "content_id", "end_timestamp", "time_spent", "progress")
40 
41     # In order to make the lookup speedy, generate a unique key for each user/node that we find
42     # listed in the needs help notifications that are relevant. We can then just check
43     # existence of this key in the set in order to see whether this user has been flagged as needing
44     # help.
45     lookup_key = "{user_id}-{node_id}"
46     needs_help = {
47         lookup_key.format(user_id=n[0], node_id=n[1]): n[2] for n in LearnerProgressNotification.objects.filter(
48             classroom_id=classroom.id,
49             notification_event=NotificationEventType.Help,
50             lesson_id__in=[lesson["id"] for lesson in lesson_data],
51         ).values_list("user_id", "contentnode_id", "timestamp")
52     }
53 
54     # In case a previously flagged learner has since completed an exercise, check all the completed
55     # notifications also
56     completed = {
57         lookup_key.format(user_id=n[0], node_id=n[1]): n[2] for n in LearnerProgressNotification.objects.filter(
58             classroom_id=classroom.id,
59             notification_event=NotificationEventType.Completed,
60             lesson_id__in=[lesson["id"] for lesson in lesson_data],
61         ).values_list("user_id", "contentnode_id", "timestamp")
62     }
63 
64     def get_status(log):
65         """
66         Read the dict from a content summary log values query and return the status
67         In the case that we have found a needs help notification for the user and content node
68         in question, return that they need help, otherwise return status based on their
69         current progress.
70         """
71         content_id = log["content_id"]
72         if content_id in content_map:
73             # Don't try to lookup anything if we don't know the content_id
74             # node_id mapping - might happen if a channel has since been deleted
75             key = lookup_key.format(user_id=log["user_id"], node_id=content_map[content_id])
76             if key in needs_help:
77                 # Now check if we have not already registered completion of the content node
78                 # or if we have and the timestamp is earlier than that on the needs_help event
79                 if key not in completed or completed[key] < needs_help[key]:
80                     return HELP_NEEDED
81         if log["progress"] == 1:
82             return COMPLETED
83         elif log["progress"] == 0:
84             return NOT_STARTED
85         return STARTED
86 
87     def map_content_logs(log):
88         """
89         Parse the content logs to return objects in the expected format.
90         """
91         return {
92             "learner_id": log["user_id"],
93             "content_id": log["content_id"],
94             "status": get_status(log),
95             "last_activity": log["end_timestamp"],
96             "time_spent": log["time_spent"],
97         }
98 
99     return map(map_content_logs, content_log_values)
100 
101 
102 class ExamStatusSerializer(serializers.ModelSerializer):
103     status = serializers.SerializerMethodField()
104     exam_id = serializers.PrimaryKeyRelatedField(source="exam", read_only=True)
105     learner_id = serializers.PrimaryKeyRelatedField(source="user", read_only=True)
106     last_activity = serializers.CharField()
107     num_correct = serializers.SerializerMethodField()
108 
109     def get_status(self, exam_log):
110         if exam_log.closed:
111             return COMPLETED
112         else:
113             return STARTED
114 
115     def get_num_correct(self, exam_log):
116         return (
117             exam_log.attemptlogs.values_list('item')
118             .order_by('completion_timestamp')
119             .distinct()
120             .aggregate(Sum('correct'))
121             .get('correct__sum')
122         )
123 
124     class Meta:
125         model = logger_models.ExamLog
126         fields = ("exam_id", "learner_id", "status", "last_activity", "num_correct")
127 
128 
129 class GroupSerializer(serializers.ModelSerializer):
130     member_ids = serializers.SerializerMethodField()
131 
132     def get_member_ids(self, group):
133         return group.get_members().values_list("id", flat=True)
134 
135     class Meta:
136         model = auth_models.LearnerGroup
137         fields = ("id", "name", "member_ids")
138 
139 
140 class UserSerializer(serializers.ModelSerializer):
141     name = serializers.CharField(source="full_name")
142 
143     class Meta:
144         model = auth_models.FacilityUser
145         fields = ("id", "name", "username")
146 
147 
148 class LessonNodeIdsField(serializers.Field):
149     def to_representation(self, values):
150         return [value["contentnode_id"] for value in values]
151 
152 
153 class LessonAssignmentsField(serializers.RelatedField):
154     def to_representation(self, assignment):
155         return assignment.collection.id
156 
157 
158 class LessonSerializer(serializers.ModelSerializer):
159     active = serializers.BooleanField(source="is_active")
160     node_ids = LessonNodeIdsField(default=[], source="resources")
161 
162     # classrooms are in here, and filtered out later
163     groups = LessonAssignmentsField(
164         many=True, read_only=True, source="lesson_assignments"
165     )
166 
167     class Meta:
168         model = Lesson
169         fields = ("id", "title", "active", "node_ids", "groups")
170 
171 
172 class ExamQuestionSourcesField(serializers.Field):
173     def to_representation(self, values):
174         return values
175 
176 
177 class ExamAssignmentsField(serializers.RelatedField):
178     def to_representation(self, assignment):
179         return assignment.collection.id
180 
181 
182 class ExamSerializer(serializers.ModelSerializer):
183 
184     question_sources = ExamQuestionSourcesField(default=[])
185 
186     # classes are in here, and filtered out later
187     groups = ExamAssignmentsField(many=True, read_only=True, source="assignments")
188 
189     class Meta:
190         model = Exam
191         fields = ("id", "title", "active", "question_sources", "groups", "data_model_version")
192 
193 
194 class ContentSerializer(serializers.ModelSerializer):
195     node_id = serializers.CharField(source="id")
196 
197     class Meta:
198         model = ContentNode
199         fields = ("node_id", "content_id", "title", "kind")
200 
201 
202 def data(Serializer, queryset):
203     return Serializer(queryset, many=True).data
204 
205 
206 class ClassSummaryViewSet(viewsets.ViewSet):
207     def retrieve(self, request, pk):
208         classroom = get_object_or_404(auth_models.Classroom, id=pk)
209         query_learners = classroom.get_members()
210         query_lesson = Lesson.objects.filter(collection=pk)
211         query_exams = Exam.objects.filter(collection=pk)
212         query_exam_logs = logger_models.ExamLog.objects.filter(
213             exam__in=query_exams
214         ).annotate(last_activity=Max("attemptlogs__end_timestamp"))
215 
216         lesson_data = data(LessonSerializer, query_lesson)
217         exam_data = data(ExamSerializer, query_exams)
218 
219         # filter classes out of exam assignments
220         for exam in exam_data:
221             exam["groups"] = [g for g in exam["groups"] if g != pk]
222 
223         # filter classes out of lesson assignments
224         for lesson in lesson_data:
225             lesson["groups"] = [g for g in lesson["groups"] if g != pk]
226 
227         all_node_ids = set()
228         for lesson in lesson_data:
229             all_node_ids |= set(lesson.get("node_ids"))
230         for exam in exam_data:
231             exam_node_ids = [question['exercise_id'] for question in exam.get("question_sources")]
232             all_node_ids |= set(exam_node_ids)
233 
234         query_content = ContentNode.objects.filter(id__in=all_node_ids)
235 
236         learners_data = data(UserSerializer, query_learners)
237 
238         output = {
239             "id": pk,
240             "name": classroom.name,
241             "coaches": data(UserSerializer, classroom.get_coaches()),
242             "learners": learners_data,
243             "groups": data(GroupSerializer, classroom.get_learner_groups()),
244             "exams": exam_data,
245             "exam_learner_status": data(ExamStatusSerializer, query_exam_logs),
246             "content": data(ContentSerializer, query_content),
247             "content_learner_status": content_status_serializer(lesson_data, learners_data, classroom),
248             "lessons": lesson_data,
249         }
250 
251         return Response(output)
252
```
--- 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/kolibri/plugins/coach/class_summary_api.py b/kolibri/plugins/coach/class_summary_api.py
--- a/kolibri/plugins/coach/class_summary_api.py
+++ b/kolibri/plugins/coach/class_summary_api.py
@@ -188,7 +188,7 @@
 
     class Meta:
         model = Exam
-        fields = ("id", "title", "active", "question_sources", "groups", "data_model_version")
+        fields = ("id", "title", "active", "question_sources", "groups", "data_model_version", "question_count")
 
 
 class ContentSerializer(serializers.ModelSerializer):

{"golden_diff": "diff --git a/kolibri/plugins/coach/class_summary_api.py b/kolibri/plugins/coach/class_summary_api.py\n--- a/kolibri/plugins/coach/class_summary_api.py\n+++ b/kolibri/plugins/coach/class_summary_api.py\n@@ -188,7 +188,7 @@\n \n     class Meta:\n         model = Exam\n-        fields = (\"id\", \"title\", \"active\", \"question_sources\", \"groups\", \"data_model_version\")\n+        fields = (\"id\", \"title\", \"active\", \"question_sources\", \"groups\", \"data_model_version\", \"question_count\")\n \n \n class ContentSerializer(serializers.ModelSerializer):\n", "issue": "users should not be able to get 1000% on an exam, unfortunately\n\r\n### Observed behavior\r\n\r\nreported by @jtamiace re: @radinamatic's apparent good luck:\r\n\r\n![image](https://user-images.githubusercontent.com/2367265/52095361-812c2280-2577-11e9-86d6-8f6cff4e0f5b.png)\r\n\r\n### Expected behavior\r\n\r\nexams are scored between 0 and 100\r\n\r\n### User-facing consequences\r\n\r\n????\r\n\r\n### Errors and logs\r\n\r\nunknown\r\n\r\n### Steps to reproduce\r\n\r\nsee http://kolibribeta.learningequality.org/coach/#/fa4cbfeda32c0c0fbf1832fc1ddd10c3/reports/learners\r\n\r\n### Context\r\n\r\nk 0.12.0 alpha 7\r\n\n", "before_files": [{"content": "from django.db.models import Max\nfrom django.db.models import Sum\nfrom django.shortcuts import get_object_or_404\nfrom rest_framework import serializers\nfrom rest_framework import viewsets\nfrom rest_framework.response import Response\n\nfrom kolibri.core.auth import models as auth_models\nfrom kolibri.core.content.models import ContentNode\nfrom kolibri.core.exams.models import Exam\nfrom kolibri.core.lessons.models import Lesson\nfrom kolibri.core.logger import models as logger_models\nfrom kolibri.core.notifications.models import LearnerProgressNotification\nfrom kolibri.core.notifications.models import NotificationEventType\n\n\n# Intended to match  NotificationEventType\nNOT_STARTED = \"NotStarted\"\nSTARTED = \"Started\"\nHELP_NEEDED = \"HelpNeeded\"\nCOMPLETED = \"Completed\"\n\n\ndef content_status_serializer(lesson_data, learners_data, classroom):\n\n    # First generate a unique set of content node ids from all the lessons\n    lesson_node_ids = set()\n    for lesson in lesson_data:\n        lesson_node_ids |= set(lesson.get(\"node_ids\"))\n\n    # Now create a map of content_id to node_id so that we can map between lessons, and notifications\n    # which use the node id, and summary logs, which use content_id\n    content_map = {n[0]: n[1] for n in ContentNode.objects.filter(id__in=lesson_node_ids).values_list(\"content_id\", \"id\")}\n\n    # Get all the values we need from the summary logs to be able to summarize current status on the\n    # relevant content items.\n    content_log_values = logger_models.ContentSummaryLog.objects.filter(\n        content_id__in=set(content_map.keys()), user__in=[learner[\"id\"] for learner in learners_data]\n    ).values(\"user_id\", \"content_id\", \"end_timestamp\", \"time_spent\", \"progress\")\n\n    # In order to make the lookup speedy, generate a unique key for each user/node that we find\n    # listed in the needs help notifications that are relevant. We can then just check\n    # existence of this key in the set in order to see whether this user has been flagged as needing\n    # help.\n    lookup_key = \"{user_id}-{node_id}\"\n    needs_help = {\n        lookup_key.format(user_id=n[0], node_id=n[1]): n[2] for n in LearnerProgressNotification.objects.filter(\n            classroom_id=classroom.id,\n            notification_event=NotificationEventType.Help,\n            lesson_id__in=[lesson[\"id\"] for lesson in lesson_data],\n        ).values_list(\"user_id\", \"contentnode_id\", \"timestamp\")\n    }\n\n    # In case a previously flagged learner has since completed an exercise, check all the completed\n    # notifications also\n    completed = {\n        lookup_key.format(user_id=n[0], node_id=n[1]): n[2] for n in LearnerProgressNotification.objects.filter(\n            classroom_id=classroom.id,\n            notification_event=NotificationEventType.Completed,\n            lesson_id__in=[lesson[\"id\"] for lesson in lesson_data],\n        ).values_list(\"user_id\", \"contentnode_id\", \"timestamp\")\n    }\n\n    def get_status(log):\n        \"\"\"\n        Read the dict from a content summary log values query and return the status\n        In the case that we have found a needs help notification for the user and content node\n        in question, return that they need help, otherwise return status based on their\n        current progress.\n        \"\"\"\n        content_id = log[\"content_id\"]\n        if content_id in content_map:\n            # Don't try to lookup anything if we don't know the content_id\n            # node_id mapping - might happen if a channel has since been deleted\n            key = lookup_key.format(user_id=log[\"user_id\"], node_id=content_map[content_id])\n            if key in needs_help:\n                # Now check if we have not already registered completion of the content node\n                # or if we have and the timestamp is earlier than that on the needs_help event\n                if key not in completed or completed[key] < needs_help[key]:\n                    return HELP_NEEDED\n        if log[\"progress\"] == 1:\n            return COMPLETED\n        elif log[\"progress\"] == 0:\n            return NOT_STARTED\n        return STARTED\n\n    def map_content_logs(log):\n        \"\"\"\n        Parse the content logs to return objects in the expected format.\n        \"\"\"\n        return {\n            \"learner_id\": log[\"user_id\"],\n            \"content_id\": log[\"content_id\"],\n            \"status\": get_status(log),\n            \"last_activity\": log[\"end_timestamp\"],\n            \"time_spent\": log[\"time_spent\"],\n        }\n\n    return map(map_content_logs, content_log_values)\n\n\nclass ExamStatusSerializer(serializers.ModelSerializer):\n    status = serializers.SerializerMethodField()\n    exam_id = serializers.PrimaryKeyRelatedField(source=\"exam\", read_only=True)\n    learner_id = serializers.PrimaryKeyRelatedField(source=\"user\", read_only=True)\n    last_activity = serializers.CharField()\n    num_correct = serializers.SerializerMethodField()\n\n    def get_status(self, exam_log):\n        if exam_log.closed:\n            return COMPLETED\n        else:\n            return STARTED\n\n    def get_num_correct(self, exam_log):\n        return (\n            exam_log.attemptlogs.values_list('item')\n            .order_by('completion_timestamp')\n            .distinct()\n            .aggregate(Sum('correct'))\n            .get('correct__sum')\n        )\n\n    class Meta:\n        model = logger_models.ExamLog\n        fields = (\"exam_id\", \"learner_id\", \"status\", \"last_activity\", \"num_correct\")\n\n\nclass GroupSerializer(serializers.ModelSerializer):\n    member_ids = serializers.SerializerMethodField()\n\n    def get_member_ids(self, group):\n        return group.get_members().values_list(\"id\", flat=True)\n\n    class Meta:\n        model = auth_models.LearnerGroup\n        fields = (\"id\", \"name\", \"member_ids\")\n\n\nclass UserSerializer(serializers.ModelSerializer):\n    name = serializers.CharField(source=\"full_name\")\n\n    class Meta:\n        model = auth_models.FacilityUser\n        fields = (\"id\", \"name\", \"username\")\n\n\nclass LessonNodeIdsField(serializers.Field):\n    def to_representation(self, values):\n        return [value[\"contentnode_id\"] for value in values]\n\n\nclass LessonAssignmentsField(serializers.RelatedField):\n    def to_representation(self, assignment):\n        return assignment.collection.id\n\n\nclass LessonSerializer(serializers.ModelSerializer):\n    active = serializers.BooleanField(source=\"is_active\")\n    node_ids = LessonNodeIdsField(default=[], source=\"resources\")\n\n    # classrooms are in here, and filtered out later\n    groups = LessonAssignmentsField(\n        many=True, read_only=True, source=\"lesson_assignments\"\n    )\n\n    class Meta:\n        model = Lesson\n        fields = (\"id\", \"title\", \"active\", \"node_ids\", \"groups\")\n\n\nclass ExamQuestionSourcesField(serializers.Field):\n    def to_representation(self, values):\n        return values\n\n\nclass ExamAssignmentsField(serializers.RelatedField):\n    def to_representation(self, assignment):\n        return assignment.collection.id\n\n\nclass ExamSerializer(serializers.ModelSerializer):\n\n    question_sources = ExamQuestionSourcesField(default=[])\n\n    # classes are in here, and filtered out later\n    groups = ExamAssignmentsField(many=True, read_only=True, source=\"assignments\")\n\n    class Meta:\n        model = Exam\n        fields = (\"id\", \"title\", \"active\", \"question_sources\", \"groups\", \"data_model_version\")\n\n\nclass ContentSerializer(serializers.ModelSerializer):\n    node_id = serializers.CharField(source=\"id\")\n\n    class Meta:\n        model = ContentNode\n        fields = (\"node_id\", \"content_id\", \"title\", \"kind\")\n\n\ndef data(Serializer, queryset):\n    return Serializer(queryset, many=True).data\n\n\nclass ClassSummaryViewSet(viewsets.ViewSet):\n    def retrieve(self, request, pk):\n        classroom = get_object_or_404(auth_models.Classroom, id=pk)\n        query_learners = classroom.get_members()\n        query_lesson = Lesson.objects.filter(collection=pk)\n        query_exams = Exam.objects.filter(collection=pk)\n        query_exam_logs = logger_models.ExamLog.objects.filter(\n            exam__in=query_exams\n        ).annotate(last_activity=Max(\"attemptlogs__end_timestamp\"))\n\n        lesson_data = data(LessonSerializer, query_lesson)\n        exam_data = data(ExamSerializer, query_exams)\n\n        # filter classes out of exam assignments\n        for exam in exam_data:\n            exam[\"groups\"] = [g for g in exam[\"groups\"] if g != pk]\n\n        # filter classes out of lesson assignments\n        for lesson in lesson_data:\n            lesson[\"groups\"] = [g for g in lesson[\"groups\"] if g != pk]\n\n        all_node_ids = set()\n        for lesson in lesson_data:\n            all_node_ids |= set(lesson.get(\"node_ids\"))\n        for exam in exam_data:\n            exam_node_ids = [question['exercise_id'] for question in exam.get(\"question_sources\")]\n            all_node_ids |= set(exam_node_ids)\n\n        query_content = ContentNode.objects.filter(id__in=all_node_ids)\n\n        learners_data = data(UserSerializer, query_learners)\n\n        output = {\n            \"id\": pk,\n            \"name\": classroom.name,\n            \"coaches\": data(UserSerializer, classroom.get_coaches()),\n            \"learners\": learners_data,\n            \"groups\": data(GroupSerializer, classroom.get_learner_groups()),\n            \"exams\": exam_data,\n            \"exam_learner_status\": data(ExamStatusSerializer, query_exam_logs),\n            \"content\": data(ContentSerializer, query_content),\n            \"content_learner_status\": content_status_serializer(lesson_data, learners_data, classroom),\n            \"lessons\": lesson_data,\n        }\n\n        return Response(output)\n", "path": "kolibri/plugins/coach/class_summary_api.py"}], "after_files": [{"content": "from django.db.models import Max\nfrom django.db.models import Sum\nfrom django.shortcuts import get_object_or_404\nfrom rest_framework import serializers\nfrom rest_framework import viewsets\nfrom rest_framework.response import Response\n\nfrom kolibri.core.auth import models as auth_models\nfrom kolibri.core.content.models import ContentNode\nfrom kolibri.core.exams.models import Exam\nfrom kolibri.core.lessons.models import Lesson\nfrom kolibri.core.logger import models as logger_models\nfrom kolibri.core.notifications.models import LearnerProgressNotification\nfrom kolibri.core.notifications.models import NotificationEventType\n\n\n# Intended to match  NotificationEventType\nNOT_STARTED = \"NotStarted\"\nSTARTED = \"Started\"\nHELP_NEEDED = \"HelpNeeded\"\nCOMPLETED = \"Completed\"\n\n\ndef content_status_serializer(lesson_data, learners_data, classroom):\n\n    # First generate a unique set of content node ids from all the lessons\n    lesson_node_ids = set()\n    for lesson in lesson_data:\n        lesson_node_ids |= set(lesson.get(\"node_ids\"))\n\n    # Now create a map of content_id to node_id so that we can map between lessons, and notifications\n    # which use the node id, and summary logs, which use content_id\n    content_map = {n[0]: n[1] for n in ContentNode.objects.filter(id__in=lesson_node_ids).values_list(\"content_id\", \"id\")}\n\n    # Get all the values we need from the summary logs to be able to summarize current status on the\n    # relevant content items.\n    content_log_values = logger_models.ContentSummaryLog.objects.filter(\n        content_id__in=set(content_map.keys()), user__in=[learner[\"id\"] for learner in learners_data]\n    ).values(\"user_id\", \"content_id\", \"end_timestamp\", \"time_spent\", \"progress\")\n\n    # In order to make the lookup speedy, generate a unique key for each user/node that we find\n    # listed in the needs help notifications that are relevant. We can then just check\n    # existence of this key in the set in order to see whether this user has been flagged as needing\n    # help.\n    lookup_key = \"{user_id}-{node_id}\"\n    needs_help = {\n        lookup_key.format(user_id=n[0], node_id=n[1]): n[2] for n in LearnerProgressNotification.objects.filter(\n            classroom_id=classroom.id,\n            notification_event=NotificationEventType.Help,\n            lesson_id__in=[lesson[\"id\"] for lesson in lesson_data],\n        ).values_list(\"user_id\", \"contentnode_id\", \"timestamp\")\n    }\n\n    # In case a previously flagged learner has since completed an exercise, check all the completed\n    # notifications also\n    completed = {\n        lookup_key.format(user_id=n[0], node_id=n[1]): n[2] for n in LearnerProgressNotification.objects.filter(\n            classroom_id=classroom.id,\n            notification_event=NotificationEventType.Completed,\n            lesson_id__in=[lesson[\"id\"] for lesson in lesson_data],\n        ).values_list(\"user_id\", \"contentnode_id\", \"timestamp\")\n    }\n\n    def get_status(log):\n        \"\"\"\n        Read the dict from a content summary log values query and return the status\n        In the case that we have found a needs help notification for the user and content node\n        in question, return that they need help, otherwise return status based on their\n        current progress.\n        \"\"\"\n        content_id = log[\"content_id\"]\n        if content_id in content_map:\n            # Don't try to lookup anything if we don't know the content_id\n            # node_id mapping - might happen if a channel has since been deleted\n            key = lookup_key.format(user_id=log[\"user_id\"], node_id=content_map[content_id])\n            if key in needs_help:\n                # Now check if we have not already registered completion of the content node\n                # or if we have and the timestamp is earlier than that on the needs_help event\n                if key not in completed or completed[key] < needs_help[key]:\n                    return HELP_NEEDED\n        if log[\"progress\"] == 1:\n            return COMPLETED\n        elif log[\"progress\"] == 0:\n            return NOT_STARTED\n        return STARTED\n\n    def map_content_logs(log):\n        \"\"\"\n        Parse the content logs to return objects in the expected format.\n        \"\"\"\n        return {\n            \"learner_id\": log[\"user_id\"],\n            \"content_id\": log[\"content_id\"],\n            \"status\": get_status(log),\n            \"last_activity\": log[\"end_timestamp\"],\n            \"time_spent\": log[\"time_spent\"],\n        }\n\n    return map(map_content_logs, content_log_values)\n\n\nclass ExamStatusSerializer(serializers.ModelSerializer):\n    status = serializers.SerializerMethodField()\n    exam_id = serializers.PrimaryKeyRelatedField(source=\"exam\", read_only=True)\n    learner_id = serializers.PrimaryKeyRelatedField(source=\"user\", read_only=True)\n    last_activity = serializers.CharField()\n    num_correct = serializers.SerializerMethodField()\n\n    def get_status(self, exam_log):\n        if exam_log.closed:\n            return COMPLETED\n        else:\n            return STARTED\n\n    def get_num_correct(self, exam_log):\n        return (\n            exam_log.attemptlogs.values_list('item')\n            .order_by('completion_timestamp')\n            .distinct()\n            .aggregate(Sum('correct'))\n            .get('correct__sum')\n        )\n\n    class Meta:\n        model = logger_models.ExamLog\n        fields = (\"exam_id\", \"learner_id\", \"status\", \"last_activity\", \"num_correct\")\n\n\nclass GroupSerializer(serializers.ModelSerializer):\n    member_ids = serializers.SerializerMethodField()\n\n    def get_member_ids(self, group):\n        return group.get_members().values_list(\"id\", flat=True)\n\n    class Meta:\n        model = auth_models.LearnerGroup\n        fields = (\"id\", \"name\", \"member_ids\")\n\n\nclass UserSerializer(serializers.ModelSerializer):\n    name = serializers.CharField(source=\"full_name\")\n\n    class Meta:\n        model = auth_models.FacilityUser\n        fields = (\"id\", \"name\", \"username\")\n\n\nclass LessonNodeIdsField(serializers.Field):\n    def to_representation(self, values):\n        return [value[\"contentnode_id\"] for value in values]\n\n\nclass LessonAssignmentsField(serializers.RelatedField):\n    def to_representation(self, assignment):\n        return assignment.collection.id\n\n\nclass LessonSerializer(serializers.ModelSerializer):\n    active = serializers.BooleanField(source=\"is_active\")\n    node_ids = LessonNodeIdsField(default=[], source=\"resources\")\n\n    # classrooms are in here, and filtered out later\n    groups = LessonAssignmentsField(\n        many=True, read_only=True, source=\"lesson_assignments\"\n    )\n\n    class Meta:\n        model = Lesson\n        fields = (\"id\", \"title\", \"active\", \"node_ids\", \"groups\")\n\n\nclass ExamQuestionSourcesField(serializers.Field):\n    def to_representation(self, values):\n        return values\n\n\nclass ExamAssignmentsField(serializers.RelatedField):\n    def to_representation(self, assignment):\n        return assignment.collection.id\n\n\nclass ExamSerializer(serializers.ModelSerializer):\n\n    question_sources = ExamQuestionSourcesField(default=[])\n\n    # classes are in here, and filtered out later\n    groups = ExamAssignmentsField(many=True, read_only=True, source=\"assignments\")\n\n    class Meta:\n        model = Exam\n        fields = (\"id\", \"title\", \"active\", \"question_sources\", \"groups\", \"data_model_version\", \"question_count\")\n\n\nclass ContentSerializer(serializers.ModelSerializer):\n    node_id = serializers.CharField(source=\"id\")\n\n    class Meta:\n        model = ContentNode\n        fields = (\"node_id\", \"content_id\", \"title\", \"kind\")\n\n\ndef data(Serializer, queryset):\n    return Serializer(queryset, many=True).data\n\n\nclass ClassSummaryViewSet(viewsets.ViewSet):\n    def retrieve(self, request, pk):\n        classroom = get_object_or_404(auth_models.Classroom, id=pk)\n        query_learners = classroom.get_members()\n        query_lesson = Lesson.objects.filter(collection=pk)\n        query_exams = Exam.objects.filter(collection=pk)\n        query_exam_logs = logger_models.ExamLog.objects.filter(\n            exam__in=query_exams\n        ).annotate(last_activity=Max(\"attemptlogs__end_timestamp\"))\n\n        lesson_data = data(LessonSerializer, query_lesson)\n        exam_data = data(ExamSerializer, query_exams)\n\n        # filter classes out of exam assignments\n        for exam in exam_data:\n            exam[\"groups\"] = [g for g in exam[\"groups\"] if g != pk]\n\n        # filter classes out of lesson assignments\n        for lesson in lesson_data:\n            lesson[\"groups\"] = [g for g in lesson[\"groups\"] if g != pk]\n\n        all_node_ids = set()\n        for lesson in lesson_data:\n            all_node_ids |= set(lesson.get(\"node_ids\"))\n        for exam in exam_data:\n            exam_node_ids = [question['exercise_id'] for question in exam.get(\"question_sources\")]\n            all_node_ids |= set(exam_node_ids)\n\n        query_content = ContentNode.objects.filter(id__in=all_node_ids)\n\n        learners_data = data(UserSerializer, query_learners)\n\n        output = {\n            \"id\": pk,\n            \"name\": classroom.name,\n            \"coaches\": data(UserSerializer, classroom.get_coaches()),\n            \"learners\": learners_data,\n            \"groups\": data(GroupSerializer, classroom.get_learner_groups()),\n            \"exams\": exam_data,\n            \"exam_learner_status\": data(ExamStatusSerializer, query_exam_logs),\n            \"content\": data(ContentSerializer, query_content),\n            \"content_learner_status\": content_status_serializer(lesson_data, learners_data, classroom),\n            \"lessons\": lesson_data,\n        }\n\n        return Response(output)\n", "path": "kolibri/plugins/coach/class_summary_api.py"}]}