Repository: learningequality/studio
Issue: Node permission checks fail for clipboard nodes

## Summary

#1456 added permission checks for accessing and editing nodes. It checks for permissions against the node's channel, but in the case of the clipboard, it doesn't belong to any channels. 

We should add a check or filter to make sure that any nodes that belong to the clipboard tree are not part of the channel permissions check.

## Category

BUG

## How to reproduce

1. Load the channel edit page, and watch for console errors caused by attempts to load clipboard nodes. 


Changed files:
contentcuration/contentcuration/tests/test_node_views.py, contentcuration/contentcuration/models.py

diff --git a/contentcuration/contentcuration/models.py b/contentcuration/contentcuration/models.py
index 8516fcbb3..9cb672a6f 100644
--- a/contentcuration/contentcuration/models.py
+++ b/contentcuration/contentcuration/models.py
@@ -158,6 +158,8 @@ class User(AbstractBaseUser, PermissionsMixin):
         if self.is_admin:
             return True
         root = node.get_root()
+        if root == self.clipboard_tree:
+            return True
         channel_id = Channel.objects.filter(Q(main_tree=root)
                                             | Q(chef_tree=root)
                                             | Q(trash_tree=root)
@@ -171,7 +173,11 @@ class User(AbstractBaseUser, PermissionsMixin):
     def can_view_nodes(self, nodes):
         if self.is_admin:
             return True
-        root_nodes = ContentNode.objects.filter(parent=None, tree_id__in=nodes.values_list("tree_id", flat=True).distinct()).distinct()
+        root_nodes_all = ContentNode.objects.filter(parent=None, tree_id__in=nodes.values_list("tree_id", flat=True).distinct()).distinct()
+        # If all the nodes belong to the clipboard, skip the channel check.
+        root_nodes = root_nodes_all.exclude(tree_id=self.clipboard_tree.tree_id)
+        if root_nodes.count() == 0 and root_nodes_all.count() > 0:
+            return True
         channels = Channel.objects.filter(Q(main_tree__in=root_nodes)
                                           | Q(chef_tree__in=root_nodes)
                                           | Q(trash_tree__in=root_nodes)
@@ -192,6 +198,9 @@ class User(AbstractBaseUser, PermissionsMixin):
         if self.is_admin:
             return True
         root = node.get_root()
+        if root == self.clipboard_tree:
+            return True
+
         channel_id = Channel.objects.filter(Q(main_tree=root)
                                             | Q(chef_tree=root)
                                             | Q(trash_tree=root)
@@ -205,7 +214,11 @@ class User(AbstractBaseUser, PermissionsMixin):
     def can_edit_nodes(self, nodes):
         if self.is_admin:
             return True
-        root_nodes = ContentNode.objects.filter(parent=None, tree_id__in=nodes.values_list("tree_id", flat=True).distinct()).distinct()
+        root_nodes_all = ContentNode.objects.filter(parent=None, tree_id__in=nodes.values_list("tree_id", flat=True).distinct()).distinct()
+        # If all the nodes belong to the clipboard, skip the channel check.
+        root_nodes = root_nodes_all.exclude(tree_id=self.clipboard_tree.tree_id)
+        if root_nodes.count() == 0 and root_nodes_all.count() > 0:
+            return True
         channels = Channel.objects.filter(Q(main_tree__in=root_nodes)
                                           | Q(chef_tree__in=root_nodes)
                                           | Q(trash_tree__in=root_nodes)
diff --git a/contentcuration/contentcuration/tests/test_node_views.py b/contentcuration/contentcuration/tests/test_node_views.py
index cb3f91274..0325b4ca3 100644
--- a/contentcuration/contentcuration/tests/test_node_views.py
+++ b/contentcuration/contentcuration/tests/test_node_views.py
@@ -127,13 +127,47 @@ class GetTotalSizeEndpointTestCase(BaseAPITestCase):
         self.assertEqual(response.status_code, 404)
 
 
-class GetNodesByIdsEndpointTestCase(BaseAPITestCase):
+class GetNodePathEndpointTestCase(BaseAPITestCase):
     def test_200_post(self):
+        response = self.get(
+            reverse("get_node_path", args=[
+                self.channel.main_tree.node_id,
+                self.channel.main_tree.tree_id,
+                self.channel.main_tree.children.first().node_id,
+            ])
+        )
+        self.assertEqual(response.status_code, 200)
+        self.assertTrue('node' in response.data)
+
+    def test_404_no_permission(self):
+        new_channel = Channel.objects.create()
+        new_channel.main_tree = tree()
+        new_channel.save()
+        response = self.get(
+            reverse("get_node_path", args=[
+                new_channel.main_tree.node_id,
+                new_channel.main_tree.tree_id,
+                new_channel.main_tree.children.first().node_id
+            ]),
+        )
+        self.assertEqual(response.status_code, 404)
+
+
+class GetNodesByIdsEndpointTestCase(BaseAPITestCase):
+    def test_200_get(self):
         response = self.get(
             reverse("get_nodes_by_ids", kwargs={"ids": self.channel.main_tree.id})
         )
         self.assertEqual(response.status_code, 200)
 
+    def test_200_clipboard(self):
+        self.user.clipboard_tree = tree()
+        self.user.clipboard_tree.save()
+        response = self.get(
+            reverse("get_nodes_by_ids", kwargs={"ids": self.user.clipboard_tree.id}),
+        )
+        self.assertEqual(response.status_code, 200)
+
     def test_404_no_permission(self):
         new_channel = Channel.objects.create()
         new_channel.main_tree = tree()