Repository: learningequality/kolibri

Issue:
Coach-facing content should not show up under "Recommended" for non-coaches
<!--
Instructions:
 * Fill out the sections below, replace …'s with information about your issue
 * Use the 'preview' function above this text box to verify formatting before submitting
-->

### Observed behavior
<!--
Description of the behavior that was observed, including screenshots or other references when applicable
-->

![image](https://user-images.githubusercontent.com/618416/51058559-0c7c4e80-159e-11e9-9b52-27cc07dd750f.png)

![peek 2019-01-11 12-41](https://user-images.githubusercontent.com/618416/51058655-5107ea00-159e-11e9-8381-780c28f0c2e2.gif)

### Expected behavior
<!--
Description of what behavior was expected but did not occur
-->

The session above wasn't logged in, so "Recommended" shouldn't have included this coach-only content.

### User-facing consequences
<!--
Implications and real-world consequences for learners, coaches, admins, and other users of the application
-->

Exposing content to learners unintentionally.

### Context
<!--
Tell us about your environment, including:
 * Kolibri version
 * Operating system
 * Browser
-->

Kolibri 0.11.0



Discussion:
interesting that the 'coach' icon doesn't appear on that card
We don't show coach content icons to non-coach users.

Resolving patch:
diff --git a/kolibri/core/content/api.py b/kolibri/core/content/api.py
--- a/kolibri/core/content/api.py
+++ b/kolibri/core/content/api.py
@@ -449,6 +449,8 @@ def next_steps(self, request, **kwargs):
                     Q(has_prerequisite__in=completed_content_nodes)
                     | Q(lft__in=[rght + 1 for rght in completed_content_nodes.values_list('rght', flat=True)])
                 ).order_by()
+                if not (user.roles.exists() or user.is_superuser):  # must have coach role or higher
+                    queryset = queryset.exclude(coach_content=True)
 
         serializer = self.get_serializer(queryset, many=True)
         return Response(serializer.data)
@@ -461,9 +463,16 @@ def popular(self, request, **kwargs):
         :param request: request object
         :return: 10 most popular content nodes
         """
-        queryset = self.get_queryset(prefetch=True)
-
         cache_key = 'popular_content'
+        coach_content = False
+
+        user = request.user
+        if user.is_facility_user:  # exclude anon users
+            if user.roles.exists() or user.is_superuser:  # must have coach role or higher
+                cache_key = 'popular_content_coach'
+                coach_content = True
+
+        queryset = self.get_queryset(prefetch=True)
 
         if not cache.get(cache_key):
             if ContentSessionLog.objects.count() < 50:
@@ -473,11 +482,16 @@ def popular(self, request, **kwargs):
                 count_cache_key = 'content_count_for_popular'
                 count = cache.get(count_cache_key) or min(pks.count(), 25)
                 queryset = queryset.filter(pk__in=sample(list(pks), count))
+                if not coach_content:
+                    queryset = queryset.exclude(coach_content=True)
             else:
                 # get the most accessed content nodes
                 # search for content nodes that currently exist in the database
+                content_nodes = models.ContentNode.objects.filter(available=True)
+                if not coach_content:
+                    content_nodes = content_nodes.exclude(coach_content=True)
                 content_counts_sorted = ContentSessionLog.objects \
-                    .filter(content_id__in=models.ContentNode.objects.values_list('content_id', flat=True).distinct()) \
+                    .filter(content_id__in=content_nodes.values_list('content_id', flat=True).distinct()) \
                     .values_list('content_id', flat=True) \
                     .annotate(Count('content_id')) \
                     .order_by('-content_id__count')


Test patch:
diff --git a/kolibri/core/content/test/test_content_app.py b/kolibri/core/content/test/test_content_app.py
--- a/kolibri/core/content/test/test_content_app.py
+++ b/kolibri/core/content/test/test_content_app.py
@@ -833,6 +833,30 @@ def test_popular(self):
         response_content_ids = set(node['content_id'] for node in response.json())
         self.assertSetEqual(set(expected_content_ids), response_content_ids)
 
+    def test_popular_no_coach_content(self):
+        expected_content_ids = self._create_session_logs()
+        node = content.ContentNode.objects.get(content_id=expected_content_ids[0])
+        node.coach_content = True
+        node.save()
+        expected_content_ids = expected_content_ids[1:]
+        response = self.client.get(reverse('kolibri:core:contentnode_slim-popular'))
+        response_content_ids = set(node['content_id'] for node in response.json())
+        self.assertSetEqual(set(expected_content_ids), response_content_ids)
+
+    def test_popular_coach_has_coach_content(self):
+        coach = FacilityUser.objects.create(username="coach", facility=self.facility)
+        coach.set_password(DUMMY_PASSWORD)
+        coach.save()
+        self.facility.add_coach(coach)
+        expected_content_ids = self._create_session_logs()
+        node = content.ContentNode.objects.get(content_id=expected_content_ids[0])
+        node.coach_content = True
+        node.save()
+        self.client.login(username="coach", password=DUMMY_PASSWORD)
+        response = self.client.get(reverse('kolibri:core:contentnode_slim-popular'))
+        response_content_ids = set(node['content_id'] for node in response.json())
+        self.assertSetEqual(set(expected_content_ids), response_content_ids)
+
     def test_popular_ten_minute_cache(self):
         self._create_session_logs()
         response = self.client.get(reverse('kolibri:core:contentnode_slim-popular'))
@@ -965,6 +989,48 @@ def test_next_steps_prereq_in_progress(self):
         response_content_ids = set(node['content_id'] for node in response.json())
         self.assertSetEqual(set(expected_content_ids), response_content_ids)
 
+    def test_next_steps_prereq_coach_content_not_coach(self):
+        facility = Facility.objects.create(name="MyFac")
+        user = FacilityUser.objects.create(username="user", facility=facility)
+        root = content.ContentNode.objects.get(title='root')
+        ContentSummaryLog.objects.create(channel_id=root.channel_id,
+                                         content_id=root.content_id,
+                                         user_id=user.id,
+                                         progress=1,
+                                         start_timestamp=timezone.now(),
+                                         kind='audio')
+        user.set_password(DUMMY_PASSWORD)
+        user.save()
+        self.client.login(username=user.username, password=DUMMY_PASSWORD)
+        post_req = root.prerequisite_for.first()
+        post_req.coach_content = True
+        post_req.save()
+        response = self.client.get(reverse('kolibri:core:contentnode_slim-next-steps', kwargs={"pk": user.id}))
+        response_content_ids = set(node['content_id'] for node in response.json())
+        self.assertSetEqual(set(), response_content_ids)
+
+    def test_next_steps_prereq_coach_content_coach(self):
+        facility = Facility.objects.create(name="MyFac")
+        user = FacilityUser.objects.create(username="user", facility=facility)
+        facility.add_coach(user)
+        root = content.ContentNode.objects.get(title='root')
+        ContentSummaryLog.objects.create(channel_id=root.channel_id,
+                                         content_id=root.content_id,
+                                         user_id=user.id,
+                                         progress=1,
+                                         start_timestamp=timezone.now(),
+                                         kind='audio')
+        user.set_password(DUMMY_PASSWORD)
+        user.save()
+        self.client.login(username=user.username, password=DUMMY_PASSWORD)
+        post_req = root.prerequisite_for.first()
+        post_req.coach_content = True
+        post_req.save()
+        expected_content_ids = (post_req.content_id,)
+        response = self.client.get(reverse('kolibri:core:contentnode_slim-next-steps', kwargs={"pk": user.id}))
+        response_content_ids = set(node['content_id'] for node in response.json())
+        self.assertSetEqual(set(expected_content_ids), response_content_ids)
+
     def test_next_steps_sibling(self):
         facility = Facility.objects.create(name="MyFac")
         user = FacilityUser.objects.create(username="user", facility=facility)
@@ -1009,6 +1075,48 @@ def test_next_steps_sibling_in_progress(self):
         response_content_ids = set(node['content_id'] for node in response.json())
         self.assertSetEqual(set(expected_content_ids), response_content_ids)
 
+    def test_next_steps_sibling_coach_content_not_coach(self):
+        facility = Facility.objects.create(name="MyFac")
+        user = FacilityUser.objects.create(username="user", facility=facility)
+        node = content.ContentNode.objects.get(content_id='ce603df7c46b424b934348995e1b05fb')
+        ContentSummaryLog.objects.create(channel_id=node.channel_id,
+                                         content_id=node.content_id,
+                                         user_id=user.id,
+                                         progress=1,
+                                         start_timestamp=timezone.now(),
+                                         kind='audio')
+        user.set_password(DUMMY_PASSWORD)
+        user.save()
+        self.client.login(username=user.username, password=DUMMY_PASSWORD)
+        sibling = node.get_next_sibling()
+        sibling.coach_content = True
+        sibling.save()
+        response = self.client.get(reverse('kolibri:core:contentnode_slim-next-steps', kwargs={"pk": user.id}))
+        response_content_ids = set(node['content_id'] for node in response.json())
+        self.assertSetEqual(set(), response_content_ids)
+
+    def test_next_steps_sibling_coach_content_coach(self):
+        facility = Facility.objects.create(name="MyFac")
+        user = FacilityUser.objects.create(username="user", facility=facility)
+        facility.add_coach(user)
+        node = content.ContentNode.objects.get(content_id='ce603df7c46b424b934348995e1b05fb')
+        ContentSummaryLog.objects.create(channel_id=node.channel_id,
+                                         content_id=node.content_id,
+                                         user_id=user.id,
+                                         progress=1,
+                                         start_timestamp=timezone.now(),
+                                         kind='audio')
+        user.set_password(DUMMY_PASSWORD)
+        user.save()
+        self.client.login(username=user.username, password=DUMMY_PASSWORD)
+        sibling = node.get_next_sibling()
+        sibling.coach_content = True
+        sibling.save()
+        expected_content_ids = (sibling.content_id,)
+        response = self.client.get(reverse('kolibri:core:contentnode_slim-next-steps', kwargs={"pk": user.id}))
+        response_content_ids = set(node['content_id'] for node in response.json())
+        self.assertSetEqual(set(expected_content_ids), response_content_ids)
+
     def tearDown(self):
         """
         clean up files/folders created during the test