Repository: learningequality/kolibri

Issue:
coach-content flag does not seem to be updated after re-publishing and updating

### Observed behavior

reported by @radinamatic:

* Imported a channel with coach content (token `nakav-mafak`, channel name "CK 12 Testing"), I can see it as admin or coach, but not as learner.
* Updated the channel on Studio, received the email, correctly updated it on http://kolibribeta.learningequality.org/ and I still cannot see it as the learner

### Expected behavior

Coach content should update as expected

### User-facing consequences

difficult or unable to update state of coach content



### Steps to reproduce

Might have been an issue with that particular channel, as I've just created another with some coach content, and can see it OK as learner. Might be good to check anyways as Studio issue, when updating old channels?

### Context

0.10.0


Discussion:
Replicated similar state on an upgrade scenario from 0.8 to the tagged 0.10 release. `CK 12 testing channel` has been updated on Studio, I can see it as admin, still not as learner, but if I build an exam or a lesson with its resources, they do appear visible to learners.

Still not sure if this is a Studio or Kolibri issue or both... 😕 

![2018-07-02_2-59-43](https://user-images.githubusercontent.com/1457929/42140908-1f8d3aca-7da5-11e8-9207-7b796d5faec4.png)

Another detail from the fresh install of 0.10 on Windows 10, that I don't remember noticing before, content is visible under Recommended for learners, by not under Channels:

![2018-07-02_3-55-57](https://user-images.githubusercontent.com/1457929/42141866-4ec88536-7dac-11e8-93ee-d930fc931824.png)


  

Resolving patch:
diff --git a/kolibri/content/utils/annotation.py b/kolibri/content/utils/annotation.py
--- a/kolibri/content/utils/annotation.py
+++ b/kolibri/content/utils/annotation.py
@@ -3,7 +3,6 @@
 import os
 
 from le_utils.constants import content_kinds
-from sqlalchemy import all_
 from sqlalchemy import and_
 from sqlalchemy import exists
 from sqlalchemy import func
@@ -201,7 +200,7 @@ def recurse_availability_up_tree(channel_id):
         # but otherwise false.
         # Everything after the select statement should be identical to the available_nodes expression above.
         if bridge.engine.name == 'sqlite':
-            coach_content_nodes = select([all_(child.c.coach_content)]).where(
+            coach_content_nodes = select([func.min(child.c.coach_content)]).where(
                 and_(
                     child.c.available == True,  # noqa
                     child.c.level == level,


Test patch:
diff --git a/kolibri/content/test/test_annotation.py b/kolibri/content/test/test_annotation.py
--- a/kolibri/content/test/test_annotation.py
+++ b/kolibri/content/test/test_annotation.py
@@ -104,11 +104,97 @@ def test_all_content_nodes_available_coach_content(self):
         self.assertTrue(ContentNode.objects.get(id="2e8bac07947855369fe2d77642dfc870").coach_content)
 
     def test_no_content_nodes_coach_content(self):
-        ContentNode.objects.filter(kind=content_kinds.TOPIC).update(available=True)
+        ContentNode.objects.all().update(available=True)
         ContentNode.objects.all().update(coach_content=False)
         recurse_availability_up_tree(channel_id="6199dde695db4ee4ab392222d5af1e5c")
         self.assertEqual(ContentNode.objects.filter(coach_content=True).count(), 0)
 
+    def test_all_root_content_nodes_coach_content(self):
+        ContentNode.objects.all().update(available=True, coach_content=False)
+        root_node = ContentNode.objects.get(parent__isnull=True)
+        ContentNode.objects.filter(parent=root_node).exclude(kind=content_kinds.TOPIC).update(coach_content=True)
+        recurse_availability_up_tree(channel_id="6199dde695db4ee4ab392222d5af1e5c")
+        root_node.refresh_from_db()
+        self.assertFalse(root_node.coach_content)
+
+    def test_one_root_content_node_coach_content(self):
+        ContentNode.objects.all().update(available=True, coach_content=False)
+        root_node = ContentNode.objects.get(parent__isnull=True)
+        node = ContentNode.objects.filter(parent=root_node).exclude(kind=content_kinds.TOPIC).first()
+        node.coach_content = True
+        node.save()
+        recurse_availability_up_tree(channel_id="6199dde695db4ee4ab392222d5af1e5c")
+        root_node.refresh_from_db()
+        self.assertFalse(root_node.coach_content)
+
+    def test_one_root_topic_node_coach_content(self):
+        ContentNode.objects.all().update(available=True, coach_content=False)
+        root_node = ContentNode.objects.get(parent__isnull=True)
+        node = ContentNode.objects.filter(parent=root_node, kind=content_kinds.TOPIC).first()
+        node.coach_content = True
+        node.save()
+        recurse_availability_up_tree(channel_id="6199dde695db4ee4ab392222d5af1e5c")
+        root_node.refresh_from_db()
+        self.assertFalse(root_node.coach_content)
+
+    def test_one_child_node_coach_content(self):
+        ContentNode.objects.all().update(available=True, coach_content=False)
+        root_node = ContentNode.objects.get(parent__isnull=True)
+        node = ContentNode.objects.filter(parent=root_node, kind=content_kinds.TOPIC).first()
+        ContentNode.objects.create(
+            title='test1',
+            id=uuid.uuid4().hex,
+            content_id=uuid.uuid4().hex,
+            channel_id=root_node.channel_id,
+            parent=node,
+            kind=content_kinds.VIDEO,
+            available=True,
+            coach_content=True,
+        )
+        recurse_availability_up_tree(channel_id="6199dde695db4ee4ab392222d5af1e5c")
+        root_node.refresh_from_db()
+        node.refresh_from_db()
+        self.assertFalse(root_node.coach_content)
+        self.assertFalse(node.coach_content)
+
+    def test_one_child_coach_content_parent_no_siblings(self):
+        ContentNode.objects.all().update(available=True, coach_content=False)
+        root_node = ContentNode.objects.get(parent__isnull=True)
+        topic_node = ContentNode.objects.filter(parent=root_node, kind=content_kinds.TOPIC).first()
+        parent_node = ContentNode.objects.create(
+            title='test1',
+            id=uuid.uuid4().hex,
+            content_id=uuid.uuid4().hex,
+            channel_id=root_node.channel_id,
+            parent=topic_node,
+            kind=content_kinds.TOPIC,
+            available=True,
+            coach_content=False,
+        )
+        ContentNode.objects.create(
+            title='test2',
+            id=uuid.uuid4().hex,
+            content_id=uuid.uuid4().hex,
+            channel_id=root_node.channel_id,
+            parent=parent_node,
+            kind=content_kinds.VIDEO,
+            available=True,
+            coach_content=True,
+        )
+        ContentNode.objects.create(
+            title='test3',
+            id=uuid.uuid4().hex,
+            content_id=uuid.uuid4().hex,
+            channel_id=root_node.channel_id,
+            parent=parent_node,
+            kind=content_kinds.VIDEO,
+            available=True,
+            coach_content=False,
+        )
+        recurse_availability_up_tree(channel_id="6199dde695db4ee4ab392222d5af1e5c")
+        parent_node.refresh_from_db()
+        self.assertFalse(parent_node.coach_content)
+
     def test_one_content_node_many_siblings_coach_content(self):
         ContentNode.objects.filter(kind=content_kinds.TOPIC).update(available=True)
         ContentNode.objects.filter(id='32a941fb77c2576e8f6b294cde4c3b0c').update(coach_content=True)