Repository: learningequality/kolibri

Issue:
Incorrect frontend state management for collection-based content import flows
## Observed behavior

Two related issues when importing content through Kolibri Studio Collection tokens:
1. When you add a collection token through "IMPORT WITH TOKEN", it filters the list down to just the channels from that collection. But if you then toggle the view with "Select entire channels instead", the token seems to be getting cleared and it shows the full list of channels again.
2. When you're viewing the "entire channels" import interface, and select some channels, and then add a collection token, it filters the list down to the collection channels, but still thinks the other channels are selected, and includes them in what's going to be imported (and they can't be unselected):

![image](https://user-images.githubusercontent.com/618416/138760885-087353ad-adfa-407c-a95f-888d73718944.png)

(note the "117 channels selected" -- this blocks me from importing, too, as I don't have space for that)

## Expected behavior

1. Switching views should maintain the current token context.
2. Putting in a token should clear the currently channel selections.

## Context

Chrome in latest 0.15.x.


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
@@ -18,6 +18,7 @@
 from django.utils.cache import patch_response_headers
 from django.utils.decorators import method_decorator
 from django.utils.translation import ugettext as _
+from django.views.decorators.cache import cache_page
 from django.views.decorators.http import etag
 from django_filters.rest_framework import BaseInFilter
 from django_filters.rest_framework import BooleanFilter
@@ -1482,6 +1483,7 @@ def get_queryset(self):
         return models.File.objects.all()
 
 
+@method_decorator(cache_page(60 * 5), name="dispatch")
 class RemoteChannelViewSet(viewsets.ViewSet):
     permission_classes = (CanManageContent,)
 
@@ -1564,9 +1566,10 @@ def list(self, request, *args, **kwargs):
         baseurl = request.GET.get("baseurl", None)
         keyword = request.GET.get("keyword", None)
         language = request.GET.get("language", None)
+        token = request.GET.get("token", None)
         try:
             channels = self._make_channel_endpoint_request(
-                baseurl=baseurl, keyword=keyword, language=language
+                identifier=token, baseurl=baseurl, keyword=keyword, language=language
             )
         except requests.exceptions.ConnectionError:
             return Response(
@@ -1603,19 +1606,3 @@ def kolibri_studio_status(self, request, **kwargs):
                 return Response({"status": "online"})
         except requests.ConnectionError:
             return Response({"status": "offline"})
-
-    @detail_route(methods=["get"])
-    def retrieve_list(self, request, pk=None):
-        baseurl = request.GET.get("baseurl", None)
-        keyword = request.GET.get("keyword", None)
-        language = request.GET.get("language", None)
-        try:
-            return Response(
-                self._make_channel_endpoint_request(
-                    identifier=pk, baseurl=baseurl, keyword=keyword, language=language
-                )
-            )
-        except requests.exceptions.ConnectionError:
-            return Response(
-                {"status": "offline"}, status=status.HTTP_503_SERVICE_UNAVAILABLE
-            )


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
@@ -1718,14 +1718,6 @@ def test_channel_list(self):
         )
         self.assertEqual(response.data[0]["id"], 1)
 
-    @mock_patch_decorator
-    def test_channel_retrieve_list(self):
-        response = self.client.get(
-            reverse("kolibri:core:remotechannel-retrieve-list", kwargs={"pk": 1}),
-            format="json",
-        )
-        self.assertEqual(response.data[0]["id"], 1)
-
     @mock_patch_decorator
     def test_no_permission_non_superuser_channel_list(self):
         user = FacilityUser.objects.create(username="user", facility=self.facility)
diff --git a/kolibri/plugins/device/assets/test/views/ChannelTokenModal.spec.js b/kolibri/plugins/device/assets/test/views/ChannelTokenModal.spec.js
--- a/kolibri/plugins/device/assets/test/views/ChannelTokenModal.spec.js
+++ b/kolibri/plugins/device/assets/test/views/ChannelTokenModal.spec.js
@@ -76,10 +76,10 @@ describe('channelTokenModal component', () => {
     });
 
     it('emits a "submit" event if token lookup is successful', () => {
-      const tokenPayload = { id: 'toka-toka-token' };
+      const tokenPayload = { token: 'toka-toka-token', channels: [{ id: 'toka-toka-token' }] };
       const { lookupTokenStub } = getElements(wrapper);
       const lookupStub = lookupTokenStub();
-      lookupStub.mockResolvedValue([tokenPayload]);
+      lookupStub.mockResolvedValue(tokenPayload.channels);
       return inputToken(wrapper, 'toka-toka-token')
         .then(() => {
           wrapper.vm.submitForm();