We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Detecting if a topic is a channel
## Summary

Currenty, we detect if a topic is a channel by checking that the `channel_id` and `pk` are equal. I ran into a channel where they are not equal. See 2nd channel in image below. Does this just need to be republished? Also, should we be just annotating channels as kind=channel in the models?

![image](https://user-images.githubusercontent.com/7193975/30656070-ab496d34-9de7-11e7-86d1-c27b27a669bb.png)


![image](https://user-images.githubusercontent.com/7193975/30656122-d5060c4a-9de7-11e7-909e-ff67890a4a5a.png)


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `kolibri/content/serializers.py`
Content:
```
1 from django.core.cache import cache
2 from django.db.models import Manager, Sum
3 from django.db.models.query import RawQuerySet
4 from kolibri.content.models import AssessmentMetaData, ChannelMetadata, ContentNode, File, Language
5 from le_utils.constants import content_kinds
6 from rest_framework import serializers
7 
8 
9 class ChannelMetadataSerializer(serializers.ModelSerializer):
10     root = serializers.PrimaryKeyRelatedField(read_only=True)
11 
12     def to_representation(self, instance):
13         value = super(ChannelMetadataSerializer, self).to_representation(instance)
14 
15         # if it has the file_size flag add extra file_size information
16         if 'request' in self.context and self.context['request'].GET.get('file_sizes', False):
17             descendants = instance.root.get_descendants()
18             total_resources = descendants.exclude(kind=content_kinds.TOPIC).count()
19             channel_summary = descendants.prefetch_related('files__local_file').aggregate(
20                 total_file_size=Sum('files__local_file__file_size')
21             )
22             value.update({"total_resources": total_resources})
23             value.update(channel_summary)
24         return value
25 
26     class Meta:
27         model = ChannelMetadata
28         fields = ('root', 'id', 'name', 'description', 'author', 'last_updated', 'version')
29 
30 
31 class LowerCaseField(serializers.CharField):
32 
33     def to_representation(self, obj):
34         return super(LowerCaseField, self).to_representation(obj).lower()
35 
36 
37 class LanguageSerializer(serializers.ModelSerializer):
38     id = LowerCaseField(max_length=14)
39     lang_code = LowerCaseField(max_length=3)
40     lang_subcode = LowerCaseField(max_length=10)
41 
42     class Meta:
43         model = Language
44         fields = ('id', 'lang_code', 'lang_subcode', 'lang_name', 'lang_direction')
45 
46 
47 class FileSerializer(serializers.ModelSerializer):
48     storage_url = serializers.SerializerMethodField()
49     preset = serializers.SerializerMethodField()
50     download_url = serializers.SerializerMethodField()
51     extension = serializers.SerializerMethodField()
52     file_size = serializers.SerializerMethodField()
53     lang = LanguageSerializer()
54 
55     def get_storage_url(self, target_node):
56         return target_node.get_storage_url()
57 
58     def get_preset(self, target_node):
59         return target_node.get_preset()
60 
61     def get_download_url(self, target_node):
62         return target_node.get_download_url()
63 
64     def get_extension(self, target_node):
65         return target_node.get_extension()
66 
67     def get_file_size(self, target_node):
68         return target_node.get_file_size()
69 
70     class Meta:
71         model = File
72         fields = ('storage_url', 'id', 'priority', 'available', 'file_size', 'extension', 'preset', 'lang',
73                   'supplementary', 'thumbnail', 'download_url')
74 
75 
76 class AssessmentMetaDataSerializer(serializers.ModelSerializer):
77 
78     assessment_item_ids = serializers.JSONField(default='[]')
79     mastery_model = serializers.JSONField(default='{}')
80 
81     class Meta:
82         model = AssessmentMetaData
83         fields = ('assessment_item_ids', 'number_of_assessments', 'mastery_model', 'randomize', 'is_manipulable', )
84 
85 
86 def get_summary_logs(content_ids, user):
87     from kolibri.logger.models import ContentSummaryLog
88     if not content_ids:
89         return ContentSummaryLog.objects.none()
90     # get all summary logs for the current user that correspond to the descendant content nodes
91     return ContentSummaryLog.objects.filter(user=user, content_id__in=content_ids)
92 
93 
94 def get_topic_progress_fraction(topic, user):
95     leaf_ids = topic.get_descendants(include_self=False).order_by().exclude(
96         kind=content_kinds.TOPIC).values_list("content_id", flat=True)
97     return round(
98         (get_summary_logs(leaf_ids, user).aggregate(Sum('progress'))['progress__sum'] or 0) / (len(leaf_ids) or 1),
99         4
100     )
101 
102 
103 def get_content_progress_fraction(content, user):
104     from kolibri.logger.models import ContentSummaryLog
105     try:
106         # add up all the progress for the logs, and divide by the total number of content nodes to get overall progress
107         overall_progress = ContentSummaryLog.objects.get(user=user, content_id=content.content_id).progress
108     except ContentSummaryLog.DoesNotExist:
109         return None
110     return round(overall_progress, 4)
111 
112 
113 def get_topic_and_content_progress_fraction(node, user):
114     if node.kind == content_kinds.TOPIC:
115         return get_topic_progress_fraction(node, user)
116     else:
117         return get_content_progress_fraction(node, user)
118 
119 
120 def get_topic_and_content_progress_fractions(nodes, user):
121     leaf_ids = nodes.get_descendants(include_self=True).order_by().exclude(
122         kind=content_kinds.TOPIC).values_list("content_id", flat=True)
123 
124     summary_logs = get_summary_logs(leaf_ids, user)
125 
126     overall_progress = {log['content_id']: round(log['progress'], 4) for log in summary_logs.values('content_id', 'progress')}
127 
128     for node in nodes:
129         if node.kind == content_kinds.TOPIC:
130             leaf_ids = node.get_descendants(include_self=True).order_by().exclude(
131                 kind=content_kinds.TOPIC).values_list("content_id", flat=True)
132             overall_progress[node.content_id] = round(
133                 sum(overall_progress.get(leaf_id, 0) for leaf_id in leaf_ids) / len(leaf_ids),
134                 4
135             )
136 
137     return overall_progress
138 
139 
140 def get_content_progress_fractions(nodes, user):
141     if isinstance(nodes, RawQuerySet) or isinstance(nodes, list):
142         leaf_ids = [datum.content_id for datum in nodes]
143     else:
144         leaf_ids = nodes.exclude(kind=content_kinds.TOPIC).values_list("content_id", flat=True)
145 
146     summary_logs = get_summary_logs(leaf_ids, user)
147 
148     # make a lookup dict for all logs to allow mapping from content_id to current progress
149     overall_progress = {log['content_id']: round(log['progress'], 4) for log in summary_logs.values('content_id', 'progress')}
150     return overall_progress
151 
152 
153 class ContentNodeListSerializer(serializers.ListSerializer):
154 
155     def to_representation(self, data):
156 
157         # Dealing with nested relationships, data can be a Manager,
158         # so, first get a queryset from the Manager if needed
159         data = data.all() if isinstance(data, Manager) else data
160 
161         cache_key = None
162         # Cache parent look ups only
163         if "parent" in self.context['request'].GET:
164             cache_key = 'contentnode_list_{parent}'.format(
165                 parent=self.context['request'].GET.get('parent'))
166 
167             if cache.get(cache_key):
168                 return cache.get(cache_key)
169 
170         if not data:
171             return data
172 
173         if 'request' not in self.context or not self.context['request'].user.is_facility_user:
174             progress_dict = {}
175         else:
176             user = self.context["request"].user
177             # Don't annotate topic progress as too expensive
178             progress_dict = get_content_progress_fractions(data, user)
179 
180         result = []
181         topic_only = True
182 
183         # Allow results to be limited after all queryset filtering has occurred
184         if self.limit:
185             data = data[:self.limit]
186 
187         for item in data:
188             obj = self.child.to_representation(
189                 item,
190                 progress_fraction=progress_dict.get(item.content_id),
191                 annotate_progress_fraction=False
192             )
193             topic_only = topic_only and obj.get('kind') == content_kinds.TOPIC
194             result.append(obj)
195 
196         # Only store if all nodes are topics, because we don't annotate progress on them
197         # This has the happy side effect of not caching our dynamically calculated
198         # recommendation queries, which might change for the same user over time
199         # because they do not return topics
200         if topic_only and cache_key:
201             cache.set(cache_key, result, 60 * 10)
202 
203         return result
204 
205 
206 class ContentNodeSerializer(serializers.ModelSerializer):
207     parent = serializers.PrimaryKeyRelatedField(read_only=True)
208     files = FileSerializer(many=True, read_only=True)
209     assessmentmetadata = AssessmentMetaDataSerializer(read_only=True, allow_null=True, many=True)
210     license = serializers.StringRelatedField(many=False)
211     license_description = serializers.SerializerMethodField()
212     lang = LanguageSerializer()
213 
214     def __new__(cls, *args, **kwargs):
215         # This is overwritten to provide a ListClassSerializer for many=True
216         limit = kwargs.pop('limit', None)
217         new = super(ContentNodeSerializer, cls).__new__(cls, *args, **kwargs)
218         new.limit = limit
219         return new
220 
221     def __init__(self, *args, **kwargs):
222         # Instantiate the superclass normally
223         super(ContentNodeSerializer, self).__init__(*args, **kwargs)
224 
225         # enable dynamic fields specification!
226         if 'request' in self.context and self.context['request'].GET.get('fields', None):
227             fields = self.context['request'].GET['fields'].split(',')
228             # Drop any fields that are not specified in the `fields` argument.
229             allowed = set(fields)
230             existing = set(self.fields.keys())
231             for field_name in existing - allowed:
232                 self.fields.pop(field_name)
233 
234     def to_representation(self, instance, progress_fraction=None, annotate_progress_fraction=True):
235         if progress_fraction is None and annotate_progress_fraction:
236             if 'request' not in self.context or not self.context['request'].user.is_facility_user:
237                 # Don't try to annotate for a non facility user
238                 progress_fraction = 0.0
239             else:
240                 user = self.context["request"].user
241                 if instance.kind != content_kinds.TOPIC:
242                     progress_fraction = get_content_progress_fraction(instance, user)
243         value = super(ContentNodeSerializer, self).to_representation(instance)
244         value['progress_fraction'] = progress_fraction
245         return value
246 
247     def get_license_description(self, target_node):
248         if target_node.license_id:
249             return target_node.license.license_description
250         return ''
251 
252     class Meta:
253         model = ContentNode
254         fields = (
255             'pk', 'content_id', 'title', 'description', 'kind', 'available', 'sort_order', 'license_owner',
256             'license', 'license_description', 'files', 'parent', 'author',
257             'assessmentmetadata', 'lang', 'channel_id',
258         )
259 
260         list_serializer_class = ContentNodeListSerializer
261 
262 
263 class ContentNodeProgressListSerializer(serializers.ListSerializer):
264 
265     def to_representation(self, data):
266 
267         if not data:
268             return data
269 
270         if 'request' not in self.context or not self.context['request'].user.is_facility_user:
271             progress_dict = {}
272         else:
273             user = self.context["request"].user
274             # Don't annotate topic progress as too expensive
275             progress_dict = get_topic_and_content_progress_fractions(data, user)
276 
277         # Dealing with nested relationships, data can be a Manager,
278         # so, first get a queryset from the Manager if needed
279         iterable = data.all() if isinstance(data, Manager) else data
280 
281         return [
282             self.child.to_representation(
283                 item,
284                 progress_fraction=progress_dict.get(item.content_id, 0.0),
285                 annotate_progress_fraction=False
286             ) for item in iterable
287         ]
288 
289 
290 class ContentNodeProgressSerializer(serializers.Serializer):
291 
292     def to_representation(self, instance, progress_fraction=None, annotate_progress_fraction=True):
293         if progress_fraction is None and annotate_progress_fraction:
294             if 'request' not in self.context or not self.context['request'].user.is_facility_user:
295                 # Don't try to annotate for a non facility user
296                 progress_fraction = 0
297             else:
298                 user = self.context["request"].user
299                 progress_fraction = get_topic_and_content_progress_fraction(instance, user) or 0.0
300         return {
301             'pk': instance.pk,
302             'progress_fraction': progress_fraction,
303         }
304 
305     class Meta:
306         list_serializer_class = ContentNodeProgressListSerializer
307
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/kolibri/content/serializers.py b/kolibri/content/serializers.py
--- a/kolibri/content/serializers.py
+++ b/kolibri/content/serializers.py
@@ -25,7 +25,7 @@
 
     class Meta:
         model = ChannelMetadata
-        fields = ('root', 'id', 'name', 'description', 'author', 'last_updated', 'version')
+        fields = ('root', 'id', 'name', 'description', 'author', 'last_updated', 'version', 'thumbnail')
 
 
 class LowerCaseField(serializers.CharField):

{"golden_diff": "diff --git a/kolibri/content/serializers.py b/kolibri/content/serializers.py\n--- a/kolibri/content/serializers.py\n+++ b/kolibri/content/serializers.py\n@@ -25,7 +25,7 @@\n \n     class Meta:\n         model = ChannelMetadata\n-        fields = ('root', 'id', 'name', 'description', 'author', 'last_updated', 'version')\n+        fields = ('root', 'id', 'name', 'description', 'author', 'last_updated', 'version', 'thumbnail')\n \n \n class LowerCaseField(serializers.CharField):\n", "issue": "Detecting if a topic is a channel\n## Summary\r\n\r\nCurrenty, we detect if a topic is a channel by checking that the `channel_id` and `pk` are equal. I ran into a channel where they are not equal. See 2nd channel in image below. Does this just need to be republished? Also, should we be just annotating channels as kind=channel in the models?\r\n\r\n![image](https://user-images.githubusercontent.com/7193975/30656070-ab496d34-9de7-11e7-86d1-c27b27a669bb.png)\r\n\r\n\r\n![image](https://user-images.githubusercontent.com/7193975/30656122-d5060c4a-9de7-11e7-909e-ff67890a4a5a.png)\r\n\n", "before_files": [{"content": "from django.core.cache import cache\nfrom django.db.models import Manager, Sum\nfrom django.db.models.query import RawQuerySet\nfrom kolibri.content.models import AssessmentMetaData, ChannelMetadata, ContentNode, File, Language\nfrom le_utils.constants import content_kinds\nfrom rest_framework import serializers\n\n\nclass ChannelMetadataSerializer(serializers.ModelSerializer):\n    root = serializers.PrimaryKeyRelatedField(read_only=True)\n\n    def to_representation(self, instance):\n        value = super(ChannelMetadataSerializer, self).to_representation(instance)\n\n        # if it has the file_size flag add extra file_size information\n        if 'request' in self.context and self.context['request'].GET.get('file_sizes', False):\n            descendants = instance.root.get_descendants()\n            total_resources = descendants.exclude(kind=content_kinds.TOPIC).count()\n            channel_summary = descendants.prefetch_related('files__local_file').aggregate(\n                total_file_size=Sum('files__local_file__file_size')\n            )\n            value.update({\"total_resources\": total_resources})\n            value.update(channel_summary)\n        return value\n\n    class Meta:\n        model = ChannelMetadata\n        fields = ('root', 'id', 'name', 'description', 'author', 'last_updated', 'version')\n\n\nclass LowerCaseField(serializers.CharField):\n\n    def to_representation(self, obj):\n        return super(LowerCaseField, self).to_representation(obj).lower()\n\n\nclass LanguageSerializer(serializers.ModelSerializer):\n    id = LowerCaseField(max_length=14)\n    lang_code = LowerCaseField(max_length=3)\n    lang_subcode = LowerCaseField(max_length=10)\n\n    class Meta:\n        model = Language\n        fields = ('id', 'lang_code', 'lang_subcode', 'lang_name', 'lang_direction')\n\n\nclass FileSerializer(serializers.ModelSerializer):\n    storage_url = serializers.SerializerMethodField()\n    preset = serializers.SerializerMethodField()\n    download_url = serializers.SerializerMethodField()\n    extension = serializers.SerializerMethodField()\n    file_size = serializers.SerializerMethodField()\n    lang = LanguageSerializer()\n\n    def get_storage_url(self, target_node):\n        return target_node.get_storage_url()\n\n    def get_preset(self, target_node):\n        return target_node.get_preset()\n\n    def get_download_url(self, target_node):\n        return target_node.get_download_url()\n\n    def get_extension(self, target_node):\n        return target_node.get_extension()\n\n    def get_file_size(self, target_node):\n        return target_node.get_file_size()\n\n    class Meta:\n        model = File\n        fields = ('storage_url', 'id', 'priority', 'available', 'file_size', 'extension', 'preset', 'lang',\n                  'supplementary', 'thumbnail', 'download_url')\n\n\nclass AssessmentMetaDataSerializer(serializers.ModelSerializer):\n\n    assessment_item_ids = serializers.JSONField(default='[]')\n    mastery_model = serializers.JSONField(default='{}')\n\n    class Meta:\n        model = AssessmentMetaData\n        fields = ('assessment_item_ids', 'number_of_assessments', 'mastery_model', 'randomize', 'is_manipulable', )\n\n\ndef get_summary_logs(content_ids, user):\n    from kolibri.logger.models import ContentSummaryLog\n    if not content_ids:\n        return ContentSummaryLog.objects.none()\n    # get all summary logs for the current user that correspond to the descendant content nodes\n    return ContentSummaryLog.objects.filter(user=user, content_id__in=content_ids)\n\n\ndef get_topic_progress_fraction(topic, user):\n    leaf_ids = topic.get_descendants(include_self=False).order_by().exclude(\n        kind=content_kinds.TOPIC).values_list(\"content_id\", flat=True)\n    return round(\n        (get_summary_logs(leaf_ids, user).aggregate(Sum('progress'))['progress__sum'] or 0) / (len(leaf_ids) or 1),\n        4\n    )\n\n\ndef get_content_progress_fraction(content, user):\n    from kolibri.logger.models import ContentSummaryLog\n    try:\n        # add up all the progress for the logs, and divide by the total number of content nodes to get overall progress\n        overall_progress = ContentSummaryLog.objects.get(user=user, content_id=content.content_id).progress\n    except ContentSummaryLog.DoesNotExist:\n        return None\n    return round(overall_progress, 4)\n\n\ndef get_topic_and_content_progress_fraction(node, user):\n    if node.kind == content_kinds.TOPIC:\n        return get_topic_progress_fraction(node, user)\n    else:\n        return get_content_progress_fraction(node, user)\n\n\ndef get_topic_and_content_progress_fractions(nodes, user):\n    leaf_ids = nodes.get_descendants(include_self=True).order_by().exclude(\n        kind=content_kinds.TOPIC).values_list(\"content_id\", flat=True)\n\n    summary_logs = get_summary_logs(leaf_ids, user)\n\n    overall_progress = {log['content_id']: round(log['progress'], 4) for log in summary_logs.values('content_id', 'progress')}\n\n    for node in nodes:\n        if node.kind == content_kinds.TOPIC:\n            leaf_ids = node.get_descendants(include_self=True).order_by().exclude(\n                kind=content_kinds.TOPIC).values_list(\"content_id\", flat=True)\n            overall_progress[node.content_id] = round(\n                sum(overall_progress.get(leaf_id, 0) for leaf_id in leaf_ids) / len(leaf_ids),\n                4\n            )\n\n    return overall_progress\n\n\ndef get_content_progress_fractions(nodes, user):\n    if isinstance(nodes, RawQuerySet) or isinstance(nodes, list):\n        leaf_ids = [datum.content_id for datum in nodes]\n    else:\n        leaf_ids = nodes.exclude(kind=content_kinds.TOPIC).values_list(\"content_id\", flat=True)\n\n    summary_logs = get_summary_logs(leaf_ids, user)\n\n    # make a lookup dict for all logs to allow mapping from content_id to current progress\n    overall_progress = {log['content_id']: round(log['progress'], 4) for log in summary_logs.values('content_id', 'progress')}\n    return overall_progress\n\n\nclass ContentNodeListSerializer(serializers.ListSerializer):\n\n    def to_representation(self, data):\n\n        # Dealing with nested relationships, data can be a Manager,\n        # so, first get a queryset from the Manager if needed\n        data = data.all() if isinstance(data, Manager) else data\n\n        cache_key = None\n        # Cache parent look ups only\n        if \"parent\" in self.context['request'].GET:\n            cache_key = 'contentnode_list_{parent}'.format(\n                parent=self.context['request'].GET.get('parent'))\n\n            if cache.get(cache_key):\n                return cache.get(cache_key)\n\n        if not data:\n            return data\n\n        if 'request' not in self.context or not self.context['request'].user.is_facility_user:\n            progress_dict = {}\n        else:\n            user = self.context[\"request\"].user\n            # Don't annotate topic progress as too expensive\n            progress_dict = get_content_progress_fractions(data, user)\n\n        result = []\n        topic_only = True\n\n        # Allow results to be limited after all queryset filtering has occurred\n        if self.limit:\n            data = data[:self.limit]\n\n        for item in data:\n            obj = self.child.to_representation(\n                item,\n                progress_fraction=progress_dict.get(item.content_id),\n                annotate_progress_fraction=False\n            )\n            topic_only = topic_only and obj.get('kind') == content_kinds.TOPIC\n            result.append(obj)\n\n        # Only store if all nodes are topics, because we don't annotate progress on them\n        # This has the happy side effect of not caching our dynamically calculated\n        # recommendation queries, which might change for the same user over time\n        # because they do not return topics\n        if topic_only and cache_key:\n            cache.set(cache_key, result, 60 * 10)\n\n        return result\n\n\nclass ContentNodeSerializer(serializers.ModelSerializer):\n    parent = serializers.PrimaryKeyRelatedField(read_only=True)\n    files = FileSerializer(many=True, read_only=True)\n    assessmentmetadata = AssessmentMetaDataSerializer(read_only=True, allow_null=True, many=True)\n    license = serializers.StringRelatedField(many=False)\n    license_description = serializers.SerializerMethodField()\n    lang = LanguageSerializer()\n\n    def __new__(cls, *args, **kwargs):\n        # This is overwritten to provide a ListClassSerializer for many=True\n        limit = kwargs.pop('limit', None)\n        new = super(ContentNodeSerializer, cls).__new__(cls, *args, **kwargs)\n        new.limit = limit\n        return new\n\n    def __init__(self, *args, **kwargs):\n        # Instantiate the superclass normally\n        super(ContentNodeSerializer, self).__init__(*args, **kwargs)\n\n        # enable dynamic fields specification!\n        if 'request' in self.context and self.context['request'].GET.get('fields', None):\n            fields = self.context['request'].GET['fields'].split(',')\n            # Drop any fields that are not specified in the `fields` argument.\n            allowed = set(fields)\n            existing = set(self.fields.keys())\n            for field_name in existing - allowed:\n                self.fields.pop(field_name)\n\n    def to_representation(self, instance, progress_fraction=None, annotate_progress_fraction=True):\n        if progress_fraction is None and annotate_progress_fraction:\n            if 'request' not in self.context or not self.context['request'].user.is_facility_user:\n                # Don't try to annotate for a non facility user\n                progress_fraction = 0.0\n            else:\n                user = self.context[\"request\"].user\n                if instance.kind != content_kinds.TOPIC:\n                    progress_fraction = get_content_progress_fraction(instance, user)\n        value = super(ContentNodeSerializer, self).to_representation(instance)\n        value['progress_fraction'] = progress_fraction\n        return value\n\n    def get_license_description(self, target_node):\n        if target_node.license_id:\n            return target_node.license.license_description\n        return ''\n\n    class Meta:\n        model = ContentNode\n        fields = (\n            'pk', 'content_id', 'title', 'description', 'kind', 'available', 'sort_order', 'license_owner',\n            'license', 'license_description', 'files', 'parent', 'author',\n            'assessmentmetadata', 'lang', 'channel_id',\n        )\n\n        list_serializer_class = ContentNodeListSerializer\n\n\nclass ContentNodeProgressListSerializer(serializers.ListSerializer):\n\n    def to_representation(self, data):\n\n        if not data:\n            return data\n\n        if 'request' not in self.context or not self.context['request'].user.is_facility_user:\n            progress_dict = {}\n        else:\n            user = self.context[\"request\"].user\n            # Don't annotate topic progress as too expensive\n            progress_dict = get_topic_and_content_progress_fractions(data, user)\n\n        # Dealing with nested relationships, data can be a Manager,\n        # so, first get a queryset from the Manager if needed\n        iterable = data.all() if isinstance(data, Manager) else data\n\n        return [\n            self.child.to_representation(\n                item,\n                progress_fraction=progress_dict.get(item.content_id, 0.0),\n                annotate_progress_fraction=False\n            ) for item in iterable\n        ]\n\n\nclass ContentNodeProgressSerializer(serializers.Serializer):\n\n    def to_representation(self, instance, progress_fraction=None, annotate_progress_fraction=True):\n        if progress_fraction is None and annotate_progress_fraction:\n            if 'request' not in self.context or not self.context['request'].user.is_facility_user:\n                # Don't try to annotate for a non facility user\n                progress_fraction = 0\n            else:\n                user = self.context[\"request\"].user\n                progress_fraction = get_topic_and_content_progress_fraction(instance, user) or 0.0\n        return {\n            'pk': instance.pk,\n            'progress_fraction': progress_fraction,\n        }\n\n    class Meta:\n        list_serializer_class = ContentNodeProgressListSerializer\n", "path": "kolibri/content/serializers.py"}], "after_files": [{"content": "from django.core.cache import cache\nfrom django.db.models import Manager, Sum\nfrom django.db.models.query import RawQuerySet\nfrom kolibri.content.models import AssessmentMetaData, ChannelMetadata, ContentNode, File, Language\nfrom le_utils.constants import content_kinds\nfrom rest_framework import serializers\n\n\nclass ChannelMetadataSerializer(serializers.ModelSerializer):\n    root = serializers.PrimaryKeyRelatedField(read_only=True)\n\n    def to_representation(self, instance):\n        value = super(ChannelMetadataSerializer, self).to_representation(instance)\n\n        # if it has the file_size flag add extra file_size information\n        if 'request' in self.context and self.context['request'].GET.get('file_sizes', False):\n            descendants = instance.root.get_descendants()\n            total_resources = descendants.exclude(kind=content_kinds.TOPIC).count()\n            channel_summary = descendants.prefetch_related('files__local_file').aggregate(\n                total_file_size=Sum('files__local_file__file_size')\n            )\n            value.update({\"total_resources\": total_resources})\n            value.update(channel_summary)\n        return value\n\n    class Meta:\n        model = ChannelMetadata\n        fields = ('root', 'id', 'name', 'description', 'author', 'last_updated', 'version', 'thumbnail')\n\n\nclass LowerCaseField(serializers.CharField):\n\n    def to_representation(self, obj):\n        return super(LowerCaseField, self).to_representation(obj).lower()\n\n\nclass LanguageSerializer(serializers.ModelSerializer):\n    id = LowerCaseField(max_length=14)\n    lang_code = LowerCaseField(max_length=3)\n    lang_subcode = LowerCaseField(max_length=10)\n\n    class Meta:\n        model = Language\n        fields = ('id', 'lang_code', 'lang_subcode', 'lang_name', 'lang_direction')\n\n\nclass FileSerializer(serializers.ModelSerializer):\n    storage_url = serializers.SerializerMethodField()\n    preset = serializers.SerializerMethodField()\n    download_url = serializers.SerializerMethodField()\n    extension = serializers.SerializerMethodField()\n    file_size = serializers.SerializerMethodField()\n    lang = LanguageSerializer()\n\n    def get_storage_url(self, target_node):\n        return target_node.get_storage_url()\n\n    def get_preset(self, target_node):\n        return target_node.get_preset()\n\n    def get_download_url(self, target_node):\n        return target_node.get_download_url()\n\n    def get_extension(self, target_node):\n        return target_node.get_extension()\n\n    def get_file_size(self, target_node):\n        return target_node.get_file_size()\n\n    class Meta:\n        model = File\n        fields = ('storage_url', 'id', 'priority', 'available', 'file_size', 'extension', 'preset', 'lang',\n                  'supplementary', 'thumbnail', 'download_url')\n\n\nclass AssessmentMetaDataSerializer(serializers.ModelSerializer):\n\n    assessment_item_ids = serializers.JSONField(default='[]')\n    mastery_model = serializers.JSONField(default='{}')\n\n    class Meta:\n        model = AssessmentMetaData\n        fields = ('assessment_item_ids', 'number_of_assessments', 'mastery_model', 'randomize', 'is_manipulable', )\n\n\ndef get_summary_logs(content_ids, user):\n    from kolibri.logger.models import ContentSummaryLog\n    if not content_ids:\n        return ContentSummaryLog.objects.none()\n    # get all summary logs for the current user that correspond to the descendant content nodes\n    return ContentSummaryLog.objects.filter(user=user, content_id__in=content_ids)\n\n\ndef get_topic_progress_fraction(topic, user):\n    leaf_ids = topic.get_descendants(include_self=False).order_by().exclude(\n        kind=content_kinds.TOPIC).values_list(\"content_id\", flat=True)\n    return round(\n        (get_summary_logs(leaf_ids, user).aggregate(Sum('progress'))['progress__sum'] or 0) / (len(leaf_ids) or 1),\n        4\n    )\n\n\ndef get_content_progress_fraction(content, user):\n    from kolibri.logger.models import ContentSummaryLog\n    try:\n        # add up all the progress for the logs, and divide by the total number of content nodes to get overall progress\n        overall_progress = ContentSummaryLog.objects.get(user=user, content_id=content.content_id).progress\n    except ContentSummaryLog.DoesNotExist:\n        return None\n    return round(overall_progress, 4)\n\n\ndef get_topic_and_content_progress_fraction(node, user):\n    if node.kind == content_kinds.TOPIC:\n        return get_topic_progress_fraction(node, user)\n    else:\n        return get_content_progress_fraction(node, user)\n\n\ndef get_topic_and_content_progress_fractions(nodes, user):\n    leaf_ids = nodes.get_descendants(include_self=True).order_by().exclude(\n        kind=content_kinds.TOPIC).values_list(\"content_id\", flat=True)\n\n    summary_logs = get_summary_logs(leaf_ids, user)\n\n    overall_progress = {log['content_id']: round(log['progress'], 4) for log in summary_logs.values('content_id', 'progress')}\n\n    for node in nodes:\n        if node.kind == content_kinds.TOPIC:\n            leaf_ids = node.get_descendants(include_self=True).order_by().exclude(\n                kind=content_kinds.TOPIC).values_list(\"content_id\", flat=True)\n            overall_progress[node.content_id] = round(\n                sum(overall_progress.get(leaf_id, 0) for leaf_id in leaf_ids) / len(leaf_ids),\n                4\n            )\n\n    return overall_progress\n\n\ndef get_content_progress_fractions(nodes, user):\n    if isinstance(nodes, RawQuerySet) or isinstance(nodes, list):\n        leaf_ids = [datum.content_id for datum in nodes]\n    else:\n        leaf_ids = nodes.exclude(kind=content_kinds.TOPIC).values_list(\"content_id\", flat=True)\n\n    summary_logs = get_summary_logs(leaf_ids, user)\n\n    # make a lookup dict for all logs to allow mapping from content_id to current progress\n    overall_progress = {log['content_id']: round(log['progress'], 4) for log in summary_logs.values('content_id', 'progress')}\n    return overall_progress\n\n\nclass ContentNodeListSerializer(serializers.ListSerializer):\n\n    def to_representation(self, data):\n\n        # Dealing with nested relationships, data can be a Manager,\n        # so, first get a queryset from the Manager if needed\n        data = data.all() if isinstance(data, Manager) else data\n\n        cache_key = None\n        # Cache parent look ups only\n        if \"parent\" in self.context['request'].GET:\n            cache_key = 'contentnode_list_{parent}'.format(\n                parent=self.context['request'].GET.get('parent'))\n\n            if cache.get(cache_key):\n                return cache.get(cache_key)\n\n        if not data:\n            return data\n\n        if 'request' not in self.context or not self.context['request'].user.is_facility_user:\n            progress_dict = {}\n        else:\n            user = self.context[\"request\"].user\n            # Don't annotate topic progress as too expensive\n            progress_dict = get_content_progress_fractions(data, user)\n\n        result = []\n        topic_only = True\n\n        # Allow results to be limited after all queryset filtering has occurred\n        if self.limit:\n            data = data[:self.limit]\n\n        for item in data:\n            obj = self.child.to_representation(\n                item,\n                progress_fraction=progress_dict.get(item.content_id),\n                annotate_progress_fraction=False\n            )\n            topic_only = topic_only and obj.get('kind') == content_kinds.TOPIC\n            result.append(obj)\n\n        # Only store if all nodes are topics, because we don't annotate progress on them\n        # This has the happy side effect of not caching our dynamically calculated\n        # recommendation queries, which might change for the same user over time\n        # because they do not return topics\n        if topic_only and cache_key:\n            cache.set(cache_key, result, 60 * 10)\n\n        return result\n\n\nclass ContentNodeSerializer(serializers.ModelSerializer):\n    parent = serializers.PrimaryKeyRelatedField(read_only=True)\n    files = FileSerializer(many=True, read_only=True)\n    assessmentmetadata = AssessmentMetaDataSerializer(read_only=True, allow_null=True, many=True)\n    license = serializers.StringRelatedField(many=False)\n    license_description = serializers.SerializerMethodField()\n    lang = LanguageSerializer()\n\n    def __new__(cls, *args, **kwargs):\n        # This is overwritten to provide a ListClassSerializer for many=True\n        limit = kwargs.pop('limit', None)\n        new = super(ContentNodeSerializer, cls).__new__(cls, *args, **kwargs)\n        new.limit = limit\n        return new\n\n    def __init__(self, *args, **kwargs):\n        # Instantiate the superclass normally\n        super(ContentNodeSerializer, self).__init__(*args, **kwargs)\n\n        # enable dynamic fields specification!\n        if 'request' in self.context and self.context['request'].GET.get('fields', None):\n            fields = self.context['request'].GET['fields'].split(',')\n            # Drop any fields that are not specified in the `fields` argument.\n            allowed = set(fields)\n            existing = set(self.fields.keys())\n            for field_name in existing - allowed:\n                self.fields.pop(field_name)\n\n    def to_representation(self, instance, progress_fraction=None, annotate_progress_fraction=True):\n        if progress_fraction is None and annotate_progress_fraction:\n            if 'request' not in self.context or not self.context['request'].user.is_facility_user:\n                # Don't try to annotate for a non facility user\n                progress_fraction = 0.0\n            else:\n                user = self.context[\"request\"].user\n                if instance.kind != content_kinds.TOPIC:\n                    progress_fraction = get_content_progress_fraction(instance, user)\n        value = super(ContentNodeSerializer, self).to_representation(instance)\n        value['progress_fraction'] = progress_fraction\n        return value\n\n    def get_license_description(self, target_node):\n        if target_node.license_id:\n            return target_node.license.license_description\n        return ''\n\n    class Meta:\n        model = ContentNode\n        fields = (\n            'pk', 'content_id', 'title', 'description', 'kind', 'available', 'sort_order', 'license_owner',\n            'license', 'license_description', 'files', 'parent', 'author',\n            'assessmentmetadata', 'lang', 'channel_id',\n        )\n\n        list_serializer_class = ContentNodeListSerializer\n\n\nclass ContentNodeProgressListSerializer(serializers.ListSerializer):\n\n    def to_representation(self, data):\n\n        if not data:\n            return data\n\n        if 'request' not in self.context or not self.context['request'].user.is_facility_user:\n            progress_dict = {}\n        else:\n            user = self.context[\"request\"].user\n            # Don't annotate topic progress as too expensive\n            progress_dict = get_topic_and_content_progress_fractions(data, user)\n\n        # Dealing with nested relationships, data can be a Manager,\n        # so, first get a queryset from the Manager if needed\n        iterable = data.all() if isinstance(data, Manager) else data\n\n        return [\n            self.child.to_representation(\n                item,\n                progress_fraction=progress_dict.get(item.content_id, 0.0),\n                annotate_progress_fraction=False\n            ) for item in iterable\n        ]\n\n\nclass ContentNodeProgressSerializer(serializers.Serializer):\n\n    def to_representation(self, instance, progress_fraction=None, annotate_progress_fraction=True):\n        if progress_fraction is None and annotate_progress_fraction:\n            if 'request' not in self.context or not self.context['request'].user.is_facility_user:\n                # Don't try to annotate for a non facility user\n                progress_fraction = 0\n            else:\n                user = self.context[\"request\"].user\n                progress_fraction = get_topic_and_content_progress_fraction(instance, user) or 0.0\n        return {\n            'pk': instance.pk,\n            'progress_fraction': progress_fraction,\n        }\n\n    class Meta:\n        list_serializer_class = ContentNodeProgressListSerializer\n", "path": "kolibri/content/serializers.py"}]}