Repository: learningequality/kolibri

Issue:
Don't cache recommended endpoints forever
### Observed behavior
Currently we are caching the ContentNodeSlimViewset endpoints for a year.

### Expected behavior
We should fix up the special case filtering in the 'cache forever' decorator to reflect the change from using filters (checking against GET params) to specific list endpoints (check against URL path).

### User-facing consequences
The same recommendations forever and ever.


Discussion:

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
@@ -66,7 +66,7 @@ def wrapper_func(*args, **kwargs):
         except IndexError:
             request = kwargs.get('request', None)
         if isinstance(request, HttpRequest):
-            if any(map(lambda x: x in request.GET, ['popular', 'next_steps', 'resume'])):
+            if any(map(lambda x: x in request.path, ['popular', 'next_steps', 'resume'])):
                 timeout = 600
         patch_response_headers(response, cache_timeout=timeout)
         return response
@@ -454,18 +454,24 @@ def recommendations_for(self, request, **kwargs):
         serializer = self.get_serializer(queryset, many=True)
         return Response(serializer.data)
 
-    @list_route(methods=['get'])
+    @detail_route(methods=['get'])
     def next_steps(self, request, **kwargs):
         """
         Recommend content that has user completed content as a prerequisite, or leftward sibling.
+        Note that this is a slightly smelly use of a detail route, as the id in question is not for
+        a contentnode, but rather for a user. Recommend we move recommendation endpoints to their own
+        endpoints in future.
 
         :param request: request object
+        :param pk: id of the user whose recommendations they are
         :return: uncompleted content nodes, or empty queryset if user is anonymous
         """
         user = request.user
+        user_id = kwargs.get('pk', None)
         queryset = self.get_queryset(prefetch=True)
         # if user is anonymous, don't return any nodes
-        if not user.is_facility_user:
+        # if person requesting is not the data they are requesting for, also return no nodes
+        if not user.is_facility_user or user.id != user_id:
             queryset = queryset.none()
         else:
             completed_content_ids = ContentSummaryLog.objects.filter(
@@ -526,18 +532,24 @@ def popular(self, request, **kwargs):
 
         return Response(cache.get(cache_key))
 
-    @list_route(methods=['get'])
+    @detail_route(methods=['get'])
     def resume(self, request, **kwargs):
         """
         Recommend content that the user has recently engaged with, but not finished.
+        Note that this is a slightly smelly use of a detail route, as the id in question is not for
+        a contentnode, but rather for a user. Recommend we move recommendation endpoints to their own
+        endpoints in future.
 
         :param request: request object
+        :param pk: id of the user whose recommendations they are
         :return: 10 most recently viewed content nodes
         """
         user = request.user
+        user_id = kwargs.get('pk', None)
         queryset = self.get_queryset(prefetch=True)
-        # if user is anonymous, return no nodes
-        if not user.is_facility_user:
+        # if user is anonymous, don't return any nodes
+        # if person requesting is not the data they are requesting for, also return no nodes
+        if not user.is_facility_user or user.id != user_id:
             queryset = queryset.none()
         else:
             # get the most recently viewed, but not finished, content nodes


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,11 @@ 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_ten_minute_cache(self):
+        self._create_session_logs()
+        response = self.client.get(reverse('kolibri:core:contentnode_slim-popular'))
+        self.assertEqual(response['Cache-Control'], 'max-age=600')
+
     def _create_summary_logs(self):
         facility = Facility.objects.create(name="MyFac")
         user = FacilityUser.objects.create(username="user", facility=facility)
@@ -866,10 +871,23 @@ def _create_summary_logs(self):
     def test_resume(self):
         user, expected_content_ids = self._create_summary_logs()
         self.client.login(username=user.username, password=DUMMY_PASSWORD)
-        response = self.client.get(reverse('kolibri:core:contentnode_slim-resume'))
+        response = self.client.get(reverse('kolibri:core:contentnode_slim-resume', 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_resume_wrong_id(self):
+        user, expected_content_ids = self._create_summary_logs()
+        self.client.login(username=user.username, password=DUMMY_PASSWORD)
+        response = self.client.get(reverse('kolibri:core:contentnode_slim-resume', kwargs={"pk": "wrong"}))
+        response_content_ids = [node['content_id'] for node in response.json()]
+        self.assertEqual([], response_content_ids)
+
+    def test_resume_ten_minute_cache(self):
+        user, expected_content_ids = self._create_summary_logs()
+        self.client.login(username=user.username, password=DUMMY_PASSWORD)
+        response = self.client.get(reverse('kolibri:core:contentnode_slim-resume', kwargs={"pk": user.id}))
+        self.assertEqual(response['Cache-Control'], 'max-age=600')
+
     def test_next_steps_prereq(self):
         facility = Facility.objects.create(name="MyFac")
         user = FacilityUser.objects.create(username="user", facility=facility)
@@ -885,10 +903,43 @@ def test_next_steps_prereq(self):
         self.client.login(username=user.username, password=DUMMY_PASSWORD)
         post_req = root.prerequisite_for.first()
         expected_content_ids = (post_req.content_id,)
-        response = self.client.get(reverse('kolibri:core:contentnode_slim-next-steps'))
+        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_prereq_ten_minute_cache(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)
+        response = self.client.get(reverse('kolibri:core:contentnode_slim-next-steps', kwargs={"pk": user.id}))
+        self.assertEqual(response['Cache-Control'], 'max-age=600')
+
+    def test_next_steps_prereq_wrong_id(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)
+        response = self.client.get(reverse('kolibri:core:contentnode_slim-next-steps', kwargs={"pk": "wrong"}))
+        response_content_ids = [node['content_id'] for node in response.json()]
+        self.assertEqual([], response_content_ids)
+
     def test_next_steps_prereq_in_progress(self):
         facility = Facility.objects.create(name="MyFac")
         user = FacilityUser.objects.create(username="user", facility=facility)
@@ -910,7 +961,7 @@ def test_next_steps_prereq_in_progress(self):
                                          start_timestamp=timezone.now(),
                                          kind='audio')
         expected_content_ids = []
-        response = self.client.get(reverse('kolibri:core:contentnode_slim-next-steps'))
+        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)
 
@@ -929,7 +980,7 @@ def test_next_steps_sibling(self):
         self.client.login(username=user.username, password=DUMMY_PASSWORD)
         sibling = node.get_next_sibling()
         expected_content_ids = (sibling.content_id,)
-        response = self.client.get(reverse('kolibri:core:contentnode_slim-next-steps'))
+        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)
 
@@ -954,7 +1005,7 @@ def test_next_steps_sibling_in_progress(self):
                                          start_timestamp=timezone.now(),
                                          kind='audio')
         expected_content_ids = []
-        response = self.client.get(reverse('kolibri:core:contentnode_slim-next-steps'))
+        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)