diff --git a/kolibri/core/content/serializers.py b/kolibri/core/content/serializers.py index ee7e67b7a9b..d4c2ea9fb6e 100644 --- a/kolibri/core/content/serializers.py +++ b/kolibri/core/content/serializers.py @@ -5,6 +5,7 @@ from kolibri.core.content.models import ContentNode from kolibri.core.content.models import File from kolibri.core.content.models import Language +from kolibri.core.content.utils.tree import get_channel_content_selection from kolibri.core.fields import create_timezonestamp @@ -248,3 +249,36 @@ def get_updated_resource(self, instance): def get_is_leaf(self, instance): return instance.kind != content_kinds.TOPIC + + +class ContentManifestChannelSerializer(serializers.ModelSerializer): + include_node_ids = serializers.SerializerMethodField() + exclude_node_ids = serializers.SerializerMethodField() + + _content_selection = None + + class Meta: + model = ChannelMetadata + fields = ( + "id", + "version", + "include_node_ids", + "exclude_node_ids", + ) + + def get_include_node_ids(self, instance): + include_nodes, _ = self._get_content_selection(instance) + return [node.id for node in include_nodes] + + def get_exclude_node_ids(self, instance): + _, exclude_nodes = self._get_content_selection(instance) + return [node.id for node in exclude_nodes] + + def _get_content_selection(self, instance): + if not self._content_selection: + self._content_selection = get_channel_content_selection(instance) + return self._content_selection + + +class ContentManifestSerializer(serializers.Serializer): + channels = serializers.ListField(child=ContentManifestChannelSerializer()) diff --git a/kolibri/core/content/utils/tree.py b/kolibri/core/content/utils/tree.py index a6105f5c16e..c825ee21f3d 100644 --- a/kolibri/core/content/utils/tree.py +++ b/kolibri/core/content/utils/tree.py @@ -18,3 +18,42 @@ def get_channel_node_depth(bridge, channel_id): return node_depth[0] return 0 + + +def get_channel_content_selection(channel_metadata): + include_nodes = list() + exclude_nodes = list() + + available_nodes = ContentNode.objects.filter( + channel_id=channel_metadata.id, available=True + ).exclude(kind="topic") + + available_nodes_queue = [channel_metadata.root] + + while len(available_nodes_queue) > 0: + node = available_nodes_queue.pop(0) + + # We could add nodes to exclude_nodes when less than half of the + # sibling nodes are missing. However, it is unclear if this would + # be useful. + + if node.kind == "topic": + leaf_nodes = _get_leaf_nodes(node) + matching_leaf_nodes = set(leaf_nodes).intersection(available_nodes) + missing_leaf_nodes = set(leaf_nodes).difference(available_nodes) + if len(missing_leaf_nodes) == 0: + assert node not in include_nodes + include_nodes.append(node) + elif len(matching_leaf_nodes) > 0: + available_nodes_queue.extend(node.children.all()) + elif node in available_nodes: + assert node not in include_nodes + include_nodes.append(node) + + return include_nodes, exclude_nodes + + +def _get_leaf_nodes(node): + return ContentNode.objects.filter( + lft__gte=node.lft, lft__lte=node.rght, channel_id=node.channel_id + ).exclude(kind="topic")