Repository: learningequality/kolibri

Issue:
MultipleObjectsReturned: get() returned more than one Lesson -- it returned 2!
Sentry Issue: [KOLIBRI-BACKEND-7](https://sentry.io/learningequality/kolibri-backend/issues/877453921/?referrer=github_integration)

```
MultipleObjectsReturned: get() returned more than one Lesson -- it returned 2!
(8 additional frame(s) were not displayed)
...
  File "rest_framework/mixins.py", line 56, in retrieve
    instance = self.get_object()
  File "rest_framework/generics.py", line 98, in get_object
    obj = get_object_or_404(queryset, **filter_kwargs)
  File "rest_framework/generics.py", line 21, in get_object_or_404
    return _get_object_or_404(queryset, *filter_args, **filter_kwargs)
  File "django/shortcuts.py", line 85, in get_object_or_404
    return queryset.get(*args, **kwargs)
  File "django/db/models/query.py", line 384, in get
    (self.model._meta.object_name, num)
```


Discussion:

Resolving patch:
diff --git a/kolibri/plugins/learn/serializers.py b/kolibri/plugins/learn/serializers.py
--- a/kolibri/plugins/learn/serializers.py
+++ b/kolibri/plugins/learn/serializers.py
@@ -4,9 +4,12 @@
 from rest_framework.serializers import ModelSerializer
 from rest_framework.serializers import SerializerMethodField
 
+from kolibri.core.auth.filters import HierarchyRelationsFilter
 from kolibri.core.auth.models import Classroom
 from kolibri.core.exams.models import Exam
+from kolibri.core.exams.models import ExamAssignment
 from kolibri.core.lessons.models import Lesson
+from kolibri.core.lessons.models import LessonAssignment
 from kolibri.core.logger.models import ContentSummaryLog
 from kolibri.core.logger.models import ExamLog
 
@@ -97,19 +100,27 @@ def get_assignments(self, instance):
         Returns all Exams and Lessons (and progress) assigned to the requesting User
         """
         current_user = self.context['request'].user
-        memberships = current_user.memberships.all()
-        learner_groups = [m.collection for m in memberships]
 
         # Return only active Lessons that are assigned to the requesting user's groups
         # TODO move this to a permission_class on Lesson
+        lesson_assignments = HierarchyRelationsFilter(LessonAssignment.objects.all()) \
+            .filter_by_hierarchy(
+                target_user=current_user,
+                ancestor_collection=instance
+        )
         filtered_lessons = Lesson.objects.filter(
-            lesson_assignments__collection__in=learner_groups,
-            collection=instance,
-            is_active=True,
+            lesson_assignments__in=lesson_assignments,
+            is_active=True
         ).distinct()
 
+        exam_assignments = HierarchyRelationsFilter(ExamAssignment.objects.all()) \
+            .filter_by_hierarchy(
+                target_user=current_user,
+                ancestor_collection=instance
+        )
+
         filtered_exams = Exam.objects.filter(
-            assignments__collection__in=learner_groups,
+            assignments__in=exam_assignments,
             collection=instance,
         ).filter(Q(active=True) | Q(examlogs__user=current_user)).distinct()
 
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
@@ -21,11 +21,10 @@ class LearnerClassroomViewset(ReadOnlyModelViewSet):
     serializer_class = LearnerClassroomSerializer
 
     def get_queryset(self):
-        current_user = self.request.user
-        memberships = current_user.memberships.filter(
-            collection__kind='classroom',
-        ).values('collection_id')
-        return Classroom.objects.filter(id__in=memberships)
+        return HierarchyRelationsFilter(Classroom.objects.all()).filter_by_hierarchy(
+            target_user=self.request.user,
+            ancestor_collection=F('id')
+        )
 
 
 class LearnerLessonViewset(ReadOnlyModelViewSet):
@@ -45,4 +44,4 @@ def get_queryset(self):
         return Lesson.objects.filter(
             lesson_assignments__in=assignments,
             is_active=True
-        )
+        ).distinct()


Test patch:
diff --git a/kolibri/plugins/learn/test/test_learner_lesson.py b/kolibri/plugins/learn/test/test_learner_lesson.py
--- a/kolibri/plugins/learn/test/test_learner_lesson.py
+++ b/kolibri/plugins/learn/test/test_learner_lesson.py
@@ -8,6 +8,7 @@
 from kolibri.core.auth.models import Classroom
 from kolibri.core.auth.models import Facility
 from kolibri.core.auth.models import FacilityUser
+from kolibri.core.auth.models import LearnerGroup
 from kolibri.core.auth.test.helpers import provision_device
 from kolibri.core.lessons.models import Lesson
 from kolibri.core.lessons.models import LessonAssignment
@@ -77,3 +78,29 @@ def test_learner_cannot_access_own_inactive_lesson(self):
         self.client.login(username='learner', password='password')
         get_request = self.client.get(reverse(self.basename + '-detail', kwargs={'pk': own_lesson.id}))
         self.assertEqual(get_request.status_code, 404)
+
+    def test_learner_assigned_same_lesson_multiple_times_only_return_one(self):
+        own_lesson = Lesson.objects.create(
+            title='Lesson',
+            collection=self.classroom,
+            created_by=self.learner_user,
+            is_active=True,
+        )
+        LessonAssignment.objects.create(
+            lesson=own_lesson,
+            assigned_by=self.learner_user,
+            collection=self.classroom,
+        )
+        group = LearnerGroup.objects.create(
+            name='Own Group',
+            parent=self.classroom,
+        )
+        group.add_member(self.learner_user)
+        LessonAssignment.objects.create(
+            lesson=own_lesson,
+            assigned_by=self.learner_user,
+            collection=group,
+        )
+        self.client.login(username='learner', password='password')
+        get_request = self.client.get(reverse(self.basename + '-detail', kwargs={'pk': own_lesson.id}))
+        self.assertEqual(get_request.status_code, 200)