We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Enhance Quiz- Side panel doesn't show on mobile

## Overview

When I was explaining a certain piece of behavior in the  select resource component  to  @akolson .  I discovered that  when you  want to add resource while on mobile the side panel does not appear at all.  or  when it's loaded and the page change to mobile view everything get's distorted.



Uploading Screen Recording 2024-02-01 at 16.37.15.mov…



improve exam preview layout when there are many questions in a quiz

### Observed behavior

see comments in https://docs.google.com/document/d/1QeRghWCFdJgWV627DaIWRM80pJ3IkJr_TRQjI4uJXEw/edit#heading=h.kj33sjr8m9b5

### Expected behavior

don't scroll exercise preview offscreen

### User-facing consequences

extra scrolling when quizzes have many questions


### Steps to reproduce

create a quiz with 50 questions and then try to rearrange them 

### Context

0.12

--- 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/learn/viewsets.py`
Content:
```
1 from django.db.models import Count
2 from django.db.models import OuterRef
3 from django.db.models import Q
4 from django.db.models import Subquery
5 from django.db.models import Sum
6 from django.db.models.fields import IntegerField
7 from rest_framework.permissions import IsAuthenticated
8 from rest_framework.response import Response
9 from rest_framework.views import APIView
10 
11 from kolibri.core.api import ReadOnlyValuesViewset
12 from kolibri.core.auth.models import Classroom
13 from kolibri.core.auth.models import Facility
14 from kolibri.core.content.api import ContentNodeProgressViewset
15 from kolibri.core.content.api import ContentNodeViewset
16 from kolibri.core.content.api import UserContentNodeViewset
17 from kolibri.core.content.models import ContentNode
18 from kolibri.core.exams.models import Exam
19 from kolibri.core.lessons.models import Lesson
20 from kolibri.core.logger.models import AttemptLog
21 from kolibri.core.logger.models import MasteryLog
22 
23 
24 contentnode_progress_viewset = ContentNodeProgressViewset()
25 contentnode_viewset = ContentNodeViewset()
26 user_contentnode_viewset = UserContentNodeViewset()
27 
28 
29 class LearnStateView(APIView):
30     def get(self, request, format=None):
31         """
32         Returns some configuration variables applicable to users navigating learn.
33         - in_classes: Whether the user is in any classes
34         - can_download_externally: Whether the user can download content externally from Kolibri
35         """
36         if request.user.is_anonymous:
37             default_facility = Facility.get_default_facility()
38             can_download_externally = (
39                 default_facility.dataset.show_download_button_in_learn
40                 if default_facility
41                 else True
42             )
43             return Response(
44                 {
45                     "in_classes": False,
46                     "can_download_externally": can_download_externally,
47                 }
48             )
49         return Response(
50             {
51                 "in_classes": request.user.memberships.exists(),
52                 "can_download_externally": request.user.dataset.show_download_button_in_learn,
53             }
54         )
55 
56 
57 def _consolidate_lessons_data(request, lessons):
58     lesson_contentnode_ids = set()
59     for lesson in lessons:
60         lesson_contentnode_ids |= {
61             resource["contentnode_id"] for resource in lesson["resources"]
62         }
63 
64     contentnode_progress = (
65         contentnode_progress_viewset.serialize_list(
66             request, {"ids": lesson_contentnode_ids}
67         )
68         if lesson_contentnode_ids
69         else []
70     )
71 
72     contentnodes = (
73         contentnode_viewset.serialize_list(request, {"ids": lesson_contentnode_ids})
74         if lesson_contentnode_ids
75         else []
76     )
77 
78     progress_map = {l["content_id"]: l["progress"] for l in contentnode_progress}
79 
80     contentnode_map = {c["id"]: c for c in contentnodes}
81 
82     for lesson in lessons:
83         lesson["progress"] = {
84             "resource_progress": sum(
85                 (
86                     progress_map[resource["content_id"]]
87                     for resource in lesson["resources"]
88                     if resource["content_id"] in progress_map
89                 )
90             ),
91             "total_resources": len(lesson["resources"]),
92         }
93         missing_resource = False
94         for resource in lesson["resources"]:
95             resource["progress"] = progress_map.get(resource["content_id"], 0)
96             resource["contentnode"] = contentnode_map.get(
97                 resource["contentnode_id"], None
98             )
99             missing_resource = missing_resource or not resource["contentnode"]
100         lesson["missing_resource"] = missing_resource
101 
102 
103 class LearnerClassroomViewset(ReadOnlyValuesViewset):
104     """
105     Returns all Classrooms for which the requesting User is a member,
106     along with all associated assignments.
107     """
108 
109     permission_classes = (IsAuthenticated,)
110 
111     values = ("id", "name")
112 
113     def get_queryset(self):
114         if self.request.user.is_anonymous:
115             return Classroom.objects.none()
116         return Classroom.objects.filter(membership__user=self.request.user)
117 
118     def consolidate(self, items, queryset):
119         if not items:
120             return items
121         lessons = (
122             Lesson.objects.filter(
123                 lesson_assignments__collection__membership__user=self.request.user,
124                 is_active=True,
125                 collection__in=(c["id"] for c in items),
126             )
127             .distinct()
128             .values(
129                 "description", "id", "is_active", "title", "resources", "collection"
130             )
131         )
132         _consolidate_lessons_data(self.request, lessons)
133 
134         user_masterylog_content_ids = MasteryLog.objects.filter(
135             user=self.request.user
136         ).values("summarylog__content_id")
137 
138         exams = (
139             Exam.objects.filter(
140                 assignments__collection__membership__user=self.request.user,
141                 collection__in=(c["id"] for c in items),
142             )
143             .filter(Q(active=True) | Q(id__in=user_masterylog_content_ids))
144             .annotate(
145                 closed=Subquery(
146                     MasteryLog.objects.filter(
147                         summarylog__content_id=OuterRef("id"), user=self.request.user
148                     ).values("complete")[:1]
149                 ),
150                 score=Subquery(
151                     AttemptLog.objects.filter(
152                         sessionlog__content_id=OuterRef("id"), user=self.request.user
153                     )
154                     .order_by()
155                     .values_list("item")
156                     .distinct()
157                     .values("masterylog")
158                     .annotate(total_correct=Sum("correct"))
159                     .values("total_correct"),
160                     output_field=IntegerField(),
161                 ),
162                 answer_count=Subquery(
163                     AttemptLog.objects.filter(
164                         sessionlog__content_id=OuterRef("id"), user=self.request.user
165                     )
166                     .order_by()
167                     .values_list("item")
168                     .distinct()
169                     .values("masterylog")
170                     .annotate(total_complete=Count("id"))
171                     .values("total_complete"),
172                     output_field=IntegerField(),
173                 ),
174             )
175             .distinct()
176             .values(
177                 "collection",
178                 "active",
179                 "archive",
180                 "id",
181                 "question_count",
182                 "title",
183                 "closed",
184                 "answer_count",
185                 "score",
186                 "question_sources",
187             )
188         )
189         exam_node_ids = set()
190         for exam in exams:
191             exam_node_ids |= {
192                 question["exercise_id"] for question in exam.get("question_sources")
193             }
194 
195         available_exam_ids = set(
196             ContentNode.objects.filter_by_uuids(exam_node_ids).values_list(
197                 "id", flat=True
198             )
199         )
200 
201         for exam in exams:
202             closed = exam.pop("closed")
203             score = exam.pop("score")
204             answer_count = exam.pop("answer_count")
205             if closed is not None:
206                 exam["progress"] = {
207                     "closed": closed,
208                     "score": score,
209                     "answer_count": answer_count,
210                     "started": True,
211                 }
212             else:
213                 exam["progress"] = {
214                     "score": None,
215                     "answer_count": None,
216                     "closed": None,
217                     "started": False,
218                 }
219             exam["missing_resource"] = any(
220                 question["exercise_id"] not in available_exam_ids
221                 for question in exam.get("question_sources")
222             )
223         out_items = []
224         for item in items:
225             item["assignments"] = {
226                 "exams": [exam for exam in exams if exam["collection"] == item["id"]],
227                 "lessons": [
228                     lesson for lesson in lessons if lesson["collection"] == item["id"]
229                 ],
230             }
231             out_items.append(item)
232         return out_items
233 
234 
235 learner_classroom_viewset = LearnerClassroomViewset()
236 
237 
238 def _resumable_resources(classrooms):
239     for classroom in classrooms:
240         for lesson in classroom["assignments"]["lessons"]:
241             for resource in lesson["resources"]:
242                 yield 0 < resource["progress"] < 1
243 
244 
245 class LearnHomePageHydrationView(APIView):
246     def get(self, request, format=None):
247         classrooms = []
248         resumable_resources = []
249         resumable_resources_progress = []
250         if not request.user.is_anonymous:
251             classrooms = learner_classroom_viewset.serialize_list(request)
252             if not classrooms or not any(_resumable_resources(classrooms)):
253                 resumable_resources = user_contentnode_viewset.serialize_list(
254                     request,
255                     {"resume": True, "max_results": 12, "ordering": "-last_interacted"},
256                 )
257                 resumable_resources_progress = (
258                     contentnode_progress_viewset.serialize_list(
259                         request,
260                         {
261                             "resume": True,
262                             "max_results": 12,
263                             "ordering": "-last_interacted",
264                         },
265                     )
266                 )
267 
268         return Response(
269             {
270                 "classrooms": classrooms,
271                 "resumable_resources": resumable_resources,
272                 "resumable_resources_progress": resumable_resources_progress,
273             }
274         )
275 
276 
277 def _map_lesson_classroom(item):
278     return {
279         "id": item.pop("collection__id"),
280         "name": item.pop("collection__name"),
281         "parent": item.pop("collection__parent_id"),
282     }
283 
284 
285 class LearnerLessonViewset(ReadOnlyValuesViewset):
286     """
287     Special Viewset for Learners to view Lessons to which they are assigned.
288     The core Lesson Viewset is locked down to Admin users only.
289     """
290 
291     permission_classes = (IsAuthenticated,)
292 
293     values = (
294         "id",
295         "title",
296         "description",
297         "resources",
298         "is_active",
299         "collection",
300         "collection__id",
301         "collection__name",
302         "collection__parent_id",
303     )
304 
305     field_map = {"classroom": _map_lesson_classroom}
306 
307     def get_queryset(self):
308         if self.request.user.is_anonymous:
309             return Lesson.objects.none()
310         return Lesson.objects.filter(
311             lesson_assignments__collection__membership__user=self.request.user,
312             is_active=True,
313         )
314 
315     def consolidate(self, items, queryset):
316         if not items:
317             return items
318 
319         _consolidate_lessons_data(self.request, items)
320 
321         return items
322
```
--- 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/learn/viewsets.py b/kolibri/plugins/learn/viewsets.py
--- a/kolibri/plugins/learn/viewsets.py
+++ b/kolibri/plugins/learn/viewsets.py
@@ -16,6 +16,7 @@
 from kolibri.core.content.api import UserContentNodeViewset
 from kolibri.core.content.models import ContentNode
 from kolibri.core.exams.models import Exam
+from kolibri.core.exams.models import exam_assignment_lookup
 from kolibri.core.lessons.models import Lesson
 from kolibri.core.logger.models import AttemptLog
 from kolibri.core.logger.models import MasteryLog
@@ -182,14 +183,19 @@
                 "title",
                 "closed",
                 "answer_count",
+                "data_model_version",
                 "score",
                 "question_sources",
             )
         )
         exam_node_ids = set()
+
         for exam in exams:
             exam_node_ids |= {
-                question["exercise_id"] for question in exam.get("question_sources")
+                exercise_id
+                for exercise_id, _ in exam_assignment_lookup(
+                    exam.get("question_sources", [])
+                )
             }
 
         available_exam_ids = set(
@@ -217,8 +223,10 @@
                     "started": False,
                 }
             exam["missing_resource"] = any(
-                question["exercise_id"] not in available_exam_ids
-                for question in exam.get("question_sources")
+                exercise_id not in available_exam_ids
+                for exercise_id, _ in exam_assignment_lookup(
+                    exam.get("question_sources", [])
+                )
             )
         out_items = []
         for item in items:

{"golden_diff": "diff --git a/kolibri/plugins/learn/viewsets.py b/kolibri/plugins/learn/viewsets.py\n--- a/kolibri/plugins/learn/viewsets.py\n+++ b/kolibri/plugins/learn/viewsets.py\n@@ -16,6 +16,7 @@\n from kolibri.core.content.api import UserContentNodeViewset\n from kolibri.core.content.models import ContentNode\n from kolibri.core.exams.models import Exam\n+from kolibri.core.exams.models import exam_assignment_lookup\n from kolibri.core.lessons.models import Lesson\n from kolibri.core.logger.models import AttemptLog\n from kolibri.core.logger.models import MasteryLog\n@@ -182,14 +183,19 @@\n                 \"title\",\n                 \"closed\",\n                 \"answer_count\",\n+                \"data_model_version\",\n                 \"score\",\n                 \"question_sources\",\n             )\n         )\n         exam_node_ids = set()\n+\n         for exam in exams:\n             exam_node_ids |= {\n-                question[\"exercise_id\"] for question in exam.get(\"question_sources\")\n+                exercise_id\n+                for exercise_id, _ in exam_assignment_lookup(\n+                    exam.get(\"question_sources\", [])\n+                )\n             }\n \n         available_exam_ids = set(\n@@ -217,8 +223,10 @@\n                     \"started\": False,\n                 }\n             exam[\"missing_resource\"] = any(\n-                question[\"exercise_id\"] not in available_exam_ids\n-                for question in exam.get(\"question_sources\")\n+                exercise_id not in available_exam_ids\n+                for exercise_id, _ in exam_assignment_lookup(\n+                    exam.get(\"question_sources\", [])\n+                )\n             )\n         out_items = []\n         for item in items:\n", "issue": "Enhance Quiz- Side panel doesn't show on mobile\n\r\n## Overview\r\n\r\nWhen I was explaining a certain piece of behavior in the  select resource component  to  @akolson .  I discovered that  when you  want to add resource while on mobile the side panel does not appear at all.  or  when it's loaded and the page change to mobile view everything get's distorted.\r\n\r\n\r\n\r\nUploading Screen Recording 2024-02-01 at 16.37.15.mov\u2026\r\n\r\n\r\n\nimprove exam preview layout when there are many questions in a quiz\n\r\n### Observed behavior\r\n\r\nsee comments in https://docs.google.com/document/d/1QeRghWCFdJgWV627DaIWRM80pJ3IkJr_TRQjI4uJXEw/edit#heading=h.kj33sjr8m9b5\r\n\r\n### Expected behavior\r\n\r\ndon't scroll exercise preview offscreen\r\n\r\n### User-facing consequences\r\n\r\nextra scrolling when quizzes have many questions\r\n\r\n\r\n### Steps to reproduce\r\n\r\ncreate a quiz with 50 questions and then try to rearrange them \r\n\r\n### Context\r\n\r\n0.12\n", "before_files": [{"content": "from django.db.models import Count\nfrom django.db.models import OuterRef\nfrom django.db.models import Q\nfrom django.db.models import Subquery\nfrom django.db.models import Sum\nfrom django.db.models.fields import IntegerField\nfrom rest_framework.permissions import IsAuthenticated\nfrom rest_framework.response import Response\nfrom rest_framework.views import APIView\n\nfrom kolibri.core.api import ReadOnlyValuesViewset\nfrom kolibri.core.auth.models import Classroom\nfrom kolibri.core.auth.models import Facility\nfrom kolibri.core.content.api import ContentNodeProgressViewset\nfrom kolibri.core.content.api import ContentNodeViewset\nfrom kolibri.core.content.api import UserContentNodeViewset\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.models import AttemptLog\nfrom kolibri.core.logger.models import MasteryLog\n\n\ncontentnode_progress_viewset = ContentNodeProgressViewset()\ncontentnode_viewset = ContentNodeViewset()\nuser_contentnode_viewset = UserContentNodeViewset()\n\n\nclass LearnStateView(APIView):\n    def get(self, request, format=None):\n        \"\"\"\n        Returns some configuration variables applicable to users navigating learn.\n        - in_classes: Whether the user is in any classes\n        - can_download_externally: Whether the user can download content externally from Kolibri\n        \"\"\"\n        if request.user.is_anonymous:\n            default_facility = Facility.get_default_facility()\n            can_download_externally = (\n                default_facility.dataset.show_download_button_in_learn\n                if default_facility\n                else True\n            )\n            return Response(\n                {\n                    \"in_classes\": False,\n                    \"can_download_externally\": can_download_externally,\n                }\n            )\n        return Response(\n            {\n                \"in_classes\": request.user.memberships.exists(),\n                \"can_download_externally\": request.user.dataset.show_download_button_in_learn,\n            }\n        )\n\n\ndef _consolidate_lessons_data(request, lessons):\n    lesson_contentnode_ids = set()\n    for lesson in lessons:\n        lesson_contentnode_ids |= {\n            resource[\"contentnode_id\"] for resource in lesson[\"resources\"]\n        }\n\n    contentnode_progress = (\n        contentnode_progress_viewset.serialize_list(\n            request, {\"ids\": lesson_contentnode_ids}\n        )\n        if lesson_contentnode_ids\n        else []\n    )\n\n    contentnodes = (\n        contentnode_viewset.serialize_list(request, {\"ids\": lesson_contentnode_ids})\n        if lesson_contentnode_ids\n        else []\n    )\n\n    progress_map = {l[\"content_id\"]: l[\"progress\"] for l in contentnode_progress}\n\n    contentnode_map = {c[\"id\"]: c for c in contentnodes}\n\n    for lesson in lessons:\n        lesson[\"progress\"] = {\n            \"resource_progress\": sum(\n                (\n                    progress_map[resource[\"content_id\"]]\n                    for resource in lesson[\"resources\"]\n                    if resource[\"content_id\"] in progress_map\n                )\n            ),\n            \"total_resources\": len(lesson[\"resources\"]),\n        }\n        missing_resource = False\n        for resource in lesson[\"resources\"]:\n            resource[\"progress\"] = progress_map.get(resource[\"content_id\"], 0)\n            resource[\"contentnode\"] = contentnode_map.get(\n                resource[\"contentnode_id\"], None\n            )\n            missing_resource = missing_resource or not resource[\"contentnode\"]\n        lesson[\"missing_resource\"] = missing_resource\n\n\nclass LearnerClassroomViewset(ReadOnlyValuesViewset):\n    \"\"\"\n    Returns all Classrooms for which the requesting User is a member,\n    along with all associated assignments.\n    \"\"\"\n\n    permission_classes = (IsAuthenticated,)\n\n    values = (\"id\", \"name\")\n\n    def get_queryset(self):\n        if self.request.user.is_anonymous:\n            return Classroom.objects.none()\n        return Classroom.objects.filter(membership__user=self.request.user)\n\n    def consolidate(self, items, queryset):\n        if not items:\n            return items\n        lessons = (\n            Lesson.objects.filter(\n                lesson_assignments__collection__membership__user=self.request.user,\n                is_active=True,\n                collection__in=(c[\"id\"] for c in items),\n            )\n            .distinct()\n            .values(\n                \"description\", \"id\", \"is_active\", \"title\", \"resources\", \"collection\"\n            )\n        )\n        _consolidate_lessons_data(self.request, lessons)\n\n        user_masterylog_content_ids = MasteryLog.objects.filter(\n            user=self.request.user\n        ).values(\"summarylog__content_id\")\n\n        exams = (\n            Exam.objects.filter(\n                assignments__collection__membership__user=self.request.user,\n                collection__in=(c[\"id\"] for c in items),\n            )\n            .filter(Q(active=True) | Q(id__in=user_masterylog_content_ids))\n            .annotate(\n                closed=Subquery(\n                    MasteryLog.objects.filter(\n                        summarylog__content_id=OuterRef(\"id\"), user=self.request.user\n                    ).values(\"complete\")[:1]\n                ),\n                score=Subquery(\n                    AttemptLog.objects.filter(\n                        sessionlog__content_id=OuterRef(\"id\"), user=self.request.user\n                    )\n                    .order_by()\n                    .values_list(\"item\")\n                    .distinct()\n                    .values(\"masterylog\")\n                    .annotate(total_correct=Sum(\"correct\"))\n                    .values(\"total_correct\"),\n                    output_field=IntegerField(),\n                ),\n                answer_count=Subquery(\n                    AttemptLog.objects.filter(\n                        sessionlog__content_id=OuterRef(\"id\"), user=self.request.user\n                    )\n                    .order_by()\n                    .values_list(\"item\")\n                    .distinct()\n                    .values(\"masterylog\")\n                    .annotate(total_complete=Count(\"id\"))\n                    .values(\"total_complete\"),\n                    output_field=IntegerField(),\n                ),\n            )\n            .distinct()\n            .values(\n                \"collection\",\n                \"active\",\n                \"archive\",\n                \"id\",\n                \"question_count\",\n                \"title\",\n                \"closed\",\n                \"answer_count\",\n                \"score\",\n                \"question_sources\",\n            )\n        )\n        exam_node_ids = set()\n        for exam in exams:\n            exam_node_ids |= {\n                question[\"exercise_id\"] for question in exam.get(\"question_sources\")\n            }\n\n        available_exam_ids = set(\n            ContentNode.objects.filter_by_uuids(exam_node_ids).values_list(\n                \"id\", flat=True\n            )\n        )\n\n        for exam in exams:\n            closed = exam.pop(\"closed\")\n            score = exam.pop(\"score\")\n            answer_count = exam.pop(\"answer_count\")\n            if closed is not None:\n                exam[\"progress\"] = {\n                    \"closed\": closed,\n                    \"score\": score,\n                    \"answer_count\": answer_count,\n                    \"started\": True,\n                }\n            else:\n                exam[\"progress\"] = {\n                    \"score\": None,\n                    \"answer_count\": None,\n                    \"closed\": None,\n                    \"started\": False,\n                }\n            exam[\"missing_resource\"] = any(\n                question[\"exercise_id\"] not in available_exam_ids\n                for question in exam.get(\"question_sources\")\n            )\n        out_items = []\n        for item in items:\n            item[\"assignments\"] = {\n                \"exams\": [exam for exam in exams if exam[\"collection\"] == item[\"id\"]],\n                \"lessons\": [\n                    lesson for lesson in lessons if lesson[\"collection\"] == item[\"id\"]\n                ],\n            }\n            out_items.append(item)\n        return out_items\n\n\nlearner_classroom_viewset = LearnerClassroomViewset()\n\n\ndef _resumable_resources(classrooms):\n    for classroom in classrooms:\n        for lesson in classroom[\"assignments\"][\"lessons\"]:\n            for resource in lesson[\"resources\"]:\n                yield 0 < resource[\"progress\"] < 1\n\n\nclass LearnHomePageHydrationView(APIView):\n    def get(self, request, format=None):\n        classrooms = []\n        resumable_resources = []\n        resumable_resources_progress = []\n        if not request.user.is_anonymous:\n            classrooms = learner_classroom_viewset.serialize_list(request)\n            if not classrooms or not any(_resumable_resources(classrooms)):\n                resumable_resources = user_contentnode_viewset.serialize_list(\n                    request,\n                    {\"resume\": True, \"max_results\": 12, \"ordering\": \"-last_interacted\"},\n                )\n                resumable_resources_progress = (\n                    contentnode_progress_viewset.serialize_list(\n                        request,\n                        {\n                            \"resume\": True,\n                            \"max_results\": 12,\n                            \"ordering\": \"-last_interacted\",\n                        },\n                    )\n                )\n\n        return Response(\n            {\n                \"classrooms\": classrooms,\n                \"resumable_resources\": resumable_resources,\n                \"resumable_resources_progress\": resumable_resources_progress,\n            }\n        )\n\n\ndef _map_lesson_classroom(item):\n    return {\n        \"id\": item.pop(\"collection__id\"),\n        \"name\": item.pop(\"collection__name\"),\n        \"parent\": item.pop(\"collection__parent_id\"),\n    }\n\n\nclass LearnerLessonViewset(ReadOnlyValuesViewset):\n    \"\"\"\n    Special Viewset for Learners to view Lessons to which they are assigned.\n    The core Lesson Viewset is locked down to Admin users only.\n    \"\"\"\n\n    permission_classes = (IsAuthenticated,)\n\n    values = (\n        \"id\",\n        \"title\",\n        \"description\",\n        \"resources\",\n        \"is_active\",\n        \"collection\",\n        \"collection__id\",\n        \"collection__name\",\n        \"collection__parent_id\",\n    )\n\n    field_map = {\"classroom\": _map_lesson_classroom}\n\n    def get_queryset(self):\n        if self.request.user.is_anonymous:\n            return Lesson.objects.none()\n        return Lesson.objects.filter(\n            lesson_assignments__collection__membership__user=self.request.user,\n            is_active=True,\n        )\n\n    def consolidate(self, items, queryset):\n        if not items:\n            return items\n\n        _consolidate_lessons_data(self.request, items)\n\n        return items\n", "path": "kolibri/plugins/learn/viewsets.py"}], "after_files": [{"content": "from django.db.models import Count\nfrom django.db.models import OuterRef\nfrom django.db.models import Q\nfrom django.db.models import Subquery\nfrom django.db.models import Sum\nfrom django.db.models.fields import IntegerField\nfrom rest_framework.permissions import IsAuthenticated\nfrom rest_framework.response import Response\nfrom rest_framework.views import APIView\n\nfrom kolibri.core.api import ReadOnlyValuesViewset\nfrom kolibri.core.auth.models import Classroom\nfrom kolibri.core.auth.models import Facility\nfrom kolibri.core.content.api import ContentNodeProgressViewset\nfrom kolibri.core.content.api import ContentNodeViewset\nfrom kolibri.core.content.api import UserContentNodeViewset\nfrom kolibri.core.content.models import ContentNode\nfrom kolibri.core.exams.models import Exam\nfrom kolibri.core.exams.models import exam_assignment_lookup\nfrom kolibri.core.lessons.models import Lesson\nfrom kolibri.core.logger.models import AttemptLog\nfrom kolibri.core.logger.models import MasteryLog\n\n\ncontentnode_progress_viewset = ContentNodeProgressViewset()\ncontentnode_viewset = ContentNodeViewset()\nuser_contentnode_viewset = UserContentNodeViewset()\n\n\nclass LearnStateView(APIView):\n    def get(self, request, format=None):\n        \"\"\"\n        Returns some configuration variables applicable to users navigating learn.\n        - in_classes: Whether the user is in any classes\n        - can_download_externally: Whether the user can download content externally from Kolibri\n        \"\"\"\n        if request.user.is_anonymous:\n            default_facility = Facility.get_default_facility()\n            can_download_externally = (\n                default_facility.dataset.show_download_button_in_learn\n                if default_facility\n                else True\n            )\n            return Response(\n                {\n                    \"in_classes\": False,\n                    \"can_download_externally\": can_download_externally,\n                }\n            )\n        return Response(\n            {\n                \"in_classes\": request.user.memberships.exists(),\n                \"can_download_externally\": request.user.dataset.show_download_button_in_learn,\n            }\n        )\n\n\ndef _consolidate_lessons_data(request, lessons):\n    lesson_contentnode_ids = set()\n    for lesson in lessons:\n        lesson_contentnode_ids |= {\n            resource[\"contentnode_id\"] for resource in lesson[\"resources\"]\n        }\n\n    contentnode_progress = (\n        contentnode_progress_viewset.serialize_list(\n            request, {\"ids\": lesson_contentnode_ids}\n        )\n        if lesson_contentnode_ids\n        else []\n    )\n\n    contentnodes = (\n        contentnode_viewset.serialize_list(request, {\"ids\": lesson_contentnode_ids})\n        if lesson_contentnode_ids\n        else []\n    )\n\n    progress_map = {l[\"content_id\"]: l[\"progress\"] for l in contentnode_progress}\n\n    contentnode_map = {c[\"id\"]: c for c in contentnodes}\n\n    for lesson in lessons:\n        lesson[\"progress\"] = {\n            \"resource_progress\": sum(\n                (\n                    progress_map[resource[\"content_id\"]]\n                    for resource in lesson[\"resources\"]\n                    if resource[\"content_id\"] in progress_map\n                )\n            ),\n            \"total_resources\": len(lesson[\"resources\"]),\n        }\n        missing_resource = False\n        for resource in lesson[\"resources\"]:\n            resource[\"progress\"] = progress_map.get(resource[\"content_id\"], 0)\n            resource[\"contentnode\"] = contentnode_map.get(\n                resource[\"contentnode_id\"], None\n            )\n            missing_resource = missing_resource or not resource[\"contentnode\"]\n        lesson[\"missing_resource\"] = missing_resource\n\n\nclass LearnerClassroomViewset(ReadOnlyValuesViewset):\n    \"\"\"\n    Returns all Classrooms for which the requesting User is a member,\n    along with all associated assignments.\n    \"\"\"\n\n    permission_classes = (IsAuthenticated,)\n\n    values = (\"id\", \"name\")\n\n    def get_queryset(self):\n        if self.request.user.is_anonymous:\n            return Classroom.objects.none()\n        return Classroom.objects.filter(membership__user=self.request.user)\n\n    def consolidate(self, items, queryset):\n        if not items:\n            return items\n        lessons = (\n            Lesson.objects.filter(\n                lesson_assignments__collection__membership__user=self.request.user,\n                is_active=True,\n                collection__in=(c[\"id\"] for c in items),\n            )\n            .distinct()\n            .values(\n                \"description\", \"id\", \"is_active\", \"title\", \"resources\", \"collection\"\n            )\n        )\n        _consolidate_lessons_data(self.request, lessons)\n\n        user_masterylog_content_ids = MasteryLog.objects.filter(\n            user=self.request.user\n        ).values(\"summarylog__content_id\")\n\n        exams = (\n            Exam.objects.filter(\n                assignments__collection__membership__user=self.request.user,\n                collection__in=(c[\"id\"] for c in items),\n            )\n            .filter(Q(active=True) | Q(id__in=user_masterylog_content_ids))\n            .annotate(\n                closed=Subquery(\n                    MasteryLog.objects.filter(\n                        summarylog__content_id=OuterRef(\"id\"), user=self.request.user\n                    ).values(\"complete\")[:1]\n                ),\n                score=Subquery(\n                    AttemptLog.objects.filter(\n                        sessionlog__content_id=OuterRef(\"id\"), user=self.request.user\n                    )\n                    .order_by()\n                    .values_list(\"item\")\n                    .distinct()\n                    .values(\"masterylog\")\n                    .annotate(total_correct=Sum(\"correct\"))\n                    .values(\"total_correct\"),\n                    output_field=IntegerField(),\n                ),\n                answer_count=Subquery(\n                    AttemptLog.objects.filter(\n                        sessionlog__content_id=OuterRef(\"id\"), user=self.request.user\n                    )\n                    .order_by()\n                    .values_list(\"item\")\n                    .distinct()\n                    .values(\"masterylog\")\n                    .annotate(total_complete=Count(\"id\"))\n                    .values(\"total_complete\"),\n                    output_field=IntegerField(),\n                ),\n            )\n            .distinct()\n            .values(\n                \"collection\",\n                \"active\",\n                \"archive\",\n                \"id\",\n                \"question_count\",\n                \"title\",\n                \"closed\",\n                \"answer_count\",\n                \"data_model_version\",\n                \"score\",\n                \"question_sources\",\n            )\n        )\n        exam_node_ids = set()\n\n        for exam in exams:\n            exam_node_ids |= {\n                exercise_id\n                for exercise_id, _ in exam_assignment_lookup(\n                    exam.get(\"question_sources\", [])\n                )\n            }\n\n        available_exam_ids = set(\n            ContentNode.objects.filter_by_uuids(exam_node_ids).values_list(\n                \"id\", flat=True\n            )\n        )\n\n        for exam in exams:\n            closed = exam.pop(\"closed\")\n            score = exam.pop(\"score\")\n            answer_count = exam.pop(\"answer_count\")\n            if closed is not None:\n                exam[\"progress\"] = {\n                    \"closed\": closed,\n                    \"score\": score,\n                    \"answer_count\": answer_count,\n                    \"started\": True,\n                }\n            else:\n                exam[\"progress\"] = {\n                    \"score\": None,\n                    \"answer_count\": None,\n                    \"closed\": None,\n                    \"started\": False,\n                }\n            exam[\"missing_resource\"] = any(\n                exercise_id not in available_exam_ids\n                for exercise_id, _ in exam_assignment_lookup(\n                    exam.get(\"question_sources\", [])\n                )\n            )\n        out_items = []\n        for item in items:\n            item[\"assignments\"] = {\n                \"exams\": [exam for exam in exams if exam[\"collection\"] == item[\"id\"]],\n                \"lessons\": [\n                    lesson for lesson in lessons if lesson[\"collection\"] == item[\"id\"]\n                ],\n            }\n            out_items.append(item)\n        return out_items\n\n\nlearner_classroom_viewset = LearnerClassroomViewset()\n\n\ndef _resumable_resources(classrooms):\n    for classroom in classrooms:\n        for lesson in classroom[\"assignments\"][\"lessons\"]:\n            for resource in lesson[\"resources\"]:\n                yield 0 < resource[\"progress\"] < 1\n\n\nclass LearnHomePageHydrationView(APIView):\n    def get(self, request, format=None):\n        classrooms = []\n        resumable_resources = []\n        resumable_resources_progress = []\n        if not request.user.is_anonymous:\n            classrooms = learner_classroom_viewset.serialize_list(request)\n            if not classrooms or not any(_resumable_resources(classrooms)):\n                resumable_resources = user_contentnode_viewset.serialize_list(\n                    request,\n                    {\"resume\": True, \"max_results\": 12, \"ordering\": \"-last_interacted\"},\n                )\n                resumable_resources_progress = (\n                    contentnode_progress_viewset.serialize_list(\n                        request,\n                        {\n                            \"resume\": True,\n                            \"max_results\": 12,\n                            \"ordering\": \"-last_interacted\",\n                        },\n                    )\n                )\n\n        return Response(\n            {\n                \"classrooms\": classrooms,\n                \"resumable_resources\": resumable_resources,\n                \"resumable_resources_progress\": resumable_resources_progress,\n            }\n        )\n\n\ndef _map_lesson_classroom(item):\n    return {\n        \"id\": item.pop(\"collection__id\"),\n        \"name\": item.pop(\"collection__name\"),\n        \"parent\": item.pop(\"collection__parent_id\"),\n    }\n\n\nclass LearnerLessonViewset(ReadOnlyValuesViewset):\n    \"\"\"\n    Special Viewset for Learners to view Lessons to which they are assigned.\n    The core Lesson Viewset is locked down to Admin users only.\n    \"\"\"\n\n    permission_classes = (IsAuthenticated,)\n\n    values = (\n        \"id\",\n        \"title\",\n        \"description\",\n        \"resources\",\n        \"is_active\",\n        \"collection\",\n        \"collection__id\",\n        \"collection__name\",\n        \"collection__parent_id\",\n    )\n\n    field_map = {\"classroom\": _map_lesson_classroom}\n\n    def get_queryset(self):\n        if self.request.user.is_anonymous:\n            return Lesson.objects.none()\n        return Lesson.objects.filter(\n            lesson_assignments__collection__membership__user=self.request.user,\n            is_active=True,\n        )\n\n    def consolidate(self, items, queryset):\n        if not items:\n            return items\n\n        _consolidate_lessons_data(self.request, items)\n\n        return items\n", "path": "kolibri/plugins/learn/viewsets.py"}]}