We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
<Enhancement> A pop up on the screen when a learner masters an exercise would be great.
## Summary

<Enhancement> A pop up on the screen when a learner masters an exercise would be great.

Presently the learner is not aware if he/she has mastered the topic, since the point addition happens very subtly.
Hence a Pop up on screen that would read "You have Mastered the Exercise and earned 500 points, Keep Going"  would be great. Anything even shorter would be great. 

Its just an on screen pop up would be great.

## System information

 - Version: 0.4.0Beta1
 - Operating system: Linus LTS 14.04
 - Browser: Chrome

## How to reproduce

1. Master an exercise on a tablet.
2. You not know if you have mastered the topic clearly .

## Screenshots

![img-20170511-wa0002](https://cloud.githubusercontent.com/assets/13453618/25945726/a11480a4-3665-11e7-9621-52a26a8be9c2.jpg)

![img-20170511-wa0001](https://cloud.githubusercontent.com/assets/13453618/25945727/a2af45b6-3665-11e7-87db-d9d6003a41f0.jpg)


--- 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/api.py`
Content:
```
1 from functools import reduce
2 from random import sample
3 
4 from django.core.cache import cache
5 from django.db.models import Q
6 from django.db.models.aggregates import Count
7 from kolibri.content import models, serializers
8 from kolibri.content.content_db_router import get_active_content_database
9 from kolibri.logger.models import ContentSessionLog, ContentSummaryLog
10 from le_utils.constants import content_kinds
11 from rest_framework import filters, pagination, viewsets
12 from rest_framework.decorators import detail_route
13 from rest_framework.response import Response
14 
15 from .utils.search import fuzz
16 
17 def _join_with_logical_operator(lst, operator):
18     op = ") {operator} (".format(operator=operator)
19     return "(({items}))".format(items=op.join(lst))
20 
21 class ChannelMetadataCacheViewSet(viewsets.ModelViewSet):
22     serializer_class = serializers.ChannelMetadataCacheSerializer
23 
24     def get_queryset(self):
25         return models.ChannelMetadataCache.objects.all()
26 
27 
28 class ContentNodeFilter(filters.FilterSet):
29     search = filters.django_filters.MethodFilter(action='title_description_filter')
30     recommendations_for = filters.django_filters.MethodFilter()
31     next_steps = filters.django_filters.MethodFilter()
32     popular = filters.django_filters.MethodFilter()
33     resume = filters.django_filters.MethodFilter()
34     kind = filters.django_filters.MethodFilter()
35     ids = filters.django_filters.MethodFilter()
36 
37     class Meta:
38         model = models.ContentNode
39         fields = ['parent', 'search', 'prerequisite_for', 'has_prerequisite', 'related', 'recommendations_for', 'ids', 'content_id']
40 
41     def title_description_filter(self, queryset, value):
42         """
43         search for title or description that contains the keywords that are not necessary in adjacent
44         """
45         exact_match = queryset.filter(Q(parent__isnull=False), Q(title__icontains=value) | Q(description__icontains=value))
46         if exact_match:
47             return exact_match
48         # if no exact match, fuzzy search using the stemmed_metaphone field in ContentNode that covers the title and description
49         fuzzed_tokens = [fuzz(word) for word in value.split()]
50         if not fuzzed_tokens[0]:
51             return []
52         token_queries = [reduce(lambda x, y: x | y, [Q(stemmed_metaphone__contains=token) for token in tokens]) for tokens in fuzzed_tokens]
53         return queryset.filter(
54             Q(parent__isnull=False),
55             reduce(lambda x, y: x & y, token_queries))
56 
57     def filter_recommendations_for(self, queryset, value):
58         """
59         Recommend items that are similar to this piece of content.
60         """
61         recc_node = queryset.get(pk=value)
62         descendants = recc_node.get_descendants(include_self=False).exclude(kind__in=['topic', ''])
63         siblings = recc_node.get_siblings(include_self=False).exclude(kind__in=['topic', ''])
64         data = descendants | siblings  # concatenates different querysets
65         return data
66 
67     def filter_next_steps(self, queryset, value):
68         """
69         Recommend uncompleted content, content that has user completed content as a prerequisite.
70 
71         :param queryset: all content nodes for this channel
72         :param value: id of currently logged in user, or none if user is anonymous
73         :return: uncompleted content nodes, or empty queryset if user is anonymous
74         """
75 
76         # if user is anonymous, don't return any nodes
77         if not value:
78             return queryset.none()
79 
80         tables = [
81             '"{summarylog_table}" AS "complete_log"',
82             '"{summarylog_table}" AS "incomplete_log"',
83             '"{content_table}" AS "complete_node"',
84             '"{content_table}" AS "incomplete_node"',
85         ]
86         table_names = {
87             "summarylog_table": ContentSummaryLog._meta.db_table,
88             "content_table": models.ContentNode._meta.db_table,
89         }
90         # aliases for sql table names
91         sql_tables_and_aliases = [table.format(**table_names) for table in tables]
92         # where conditions joined by ANDs
93         where_statements = ["NOT (incomplete_log.progress < 1 AND incomplete_log.content_id = incomplete_node.content_id)",
94                             "complete_log.user_id = '{user_id}'".format(user_id=value),
95                             "incomplete_log.user_id = '{user_id}'".format(user_id=value),
96                             "complete_log.progress = 1",
97                             "complete_node.rght = incomplete_node.lft - 1",
98                             "complete_log.content_id = complete_node.content_id"]
99         # custom SQL query to get uncompleted content based on mptt algorithm
100         next_steps_recommendations = "SELECT incomplete_node.* FROM {tables} WHERE {where}".format(
101             tables=", ".join(sql_tables_and_aliases),
102             where=_join_with_logical_operator(where_statements, "AND")
103         )
104         return models.ContentNode.objects.raw(next_steps_recommendations)
105 
106     def filter_popular(self, queryset, value):
107         """
108         Recommend content that is popular with all users.
109 
110         :param queryset: all content nodes for this channel
111         :param value: id of currently logged in user, or none if user is anonymous
112         :return: 10 most popular content nodes
113         """
114         if ContentSessionLog.objects.count() < 50:
115             # return 25 random content nodes if not enough session logs
116             pks = queryset.values_list('pk', flat=True).exclude(kind__in=['topic', ''])
117             count = min(pks.count(), 25)
118             return queryset.filter(pk__in=sample(list(pks), count))
119 
120         cache_key = 'popular_for_{}'.format(get_active_content_database())
121         if cache.get(cache_key):
122             return cache.get(cache_key)
123 
124         # get the most accessed content nodes
125         content_counts_sorted = ContentSessionLog.objects \
126             .filter(channel_id=get_active_content_database()) \
127             .values_list('content_id', flat=True) \
128             .annotate(Count('content_id')) \
129             .order_by('-content_id__count')
130 
131         most_popular = queryset.filter(content_id__in=list(content_counts_sorted[:10]))
132 
133         # cache the popular results queryset for 10 minutes, for efficiency
134         cache.set(cache_key, most_popular, 60 * 10)
135         return most_popular
136 
137     def filter_resume(self, queryset, value):
138         """
139         Recommend content that the user has recently engaged with, but not finished.
140 
141         :param queryset: all content nodes for this channel
142         :param value: id of currently logged in user, or none if user is anonymous
143         :return: 10 most recently viewed content nodes
144         """
145 
146         # if user is anonymous, return no nodes
147         if not value:
148             return queryset.none()
149 
150         # get the most recently viewed, but not finished, content nodes
151         content_ids = ContentSummaryLog.objects \
152             .filter(user=value, channel_id=get_active_content_database()) \
153             .exclude(progress=1) \
154             .order_by('end_timestamp') \
155             .values_list('content_id', flat=True) \
156             .distinct()
157 
158         resume = queryset.filter(content_id__in=list(content_ids[:10]))
159 
160         return resume
161 
162     def filter_kind(self, queryset, value):
163         """
164         Show only content of a given kind.
165 
166         :param queryset: all content nodes for this channel
167         :param value: 'content' for everything except topics, or one of the content kind constants
168         :return: content nodes of the given kind
169         """
170         if value == 'content':
171             return queryset.exclude(kind=content_kinds.TOPIC).order_by("lft")
172         return queryset.filter(kind=value).order_by("lft")
173 
174     def filter_ids(self, queryset, value):
175         return queryset.filter(pk__in=value.split(','))
176 
177 
178 class OptionalPageNumberPagination(pagination.PageNumberPagination):
179     """
180     Pagination class that allows for page number-style pagination, when requested.
181     To activate, the `page_size` argument must be set. For example, to request the first 20 records:
182     `?page_size=20&page=1`
183     """
184     page_size = None
185     page_size_query_param = "page_size"
186 
187 
188 class ContentNodeViewset(viewsets.ModelViewSet):
189     serializer_class = serializers.ContentNodeSerializer
190     filter_backends = (filters.DjangoFilterBackend,)
191     filter_class = ContentNodeFilter
192     pagination_class = OptionalPageNumberPagination
193 
194     def get_queryset(self):
195         return models.ContentNode.objects.all().select_related(
196             'parent',
197             'license',
198         ).prefetch_related(
199             'assessmentmetadata',
200             'files',
201         )
202 
203     @detail_route(methods=['get'])
204     def descendants(self, request, **kwargs):
205         node = self.get_object()
206         kind = self.request.query_params.get('descendant_kind', None)
207         descendants = node.get_descendants()
208         if kind:
209             descendants = descendants.filter(kind=kind)
210 
211         serializer = self.get_serializer(descendants, many=True)
212         return Response(serializer.data)
213 
214     @detail_route(methods=['get'])
215     def ancestors(self, request, **kwargs):
216         return Response(self.get_object().get_ancestors().values('pk', 'title'))
217 
218     @detail_route(methods=['get'])
219     def next_content(self, request, **kwargs):
220         # retrieve the "next" content node, according to depth-first tree traversal
221         this_item = self.get_object()
222         next_item = models.ContentNode.objects.filter(tree_id=this_item.tree_id, lft__gt=this_item.rght).order_by("lft").first()
223         if not next_item:
224             next_item = this_item.get_root()
225         return Response({'kind': next_item.kind, 'id': next_item.id})
226 
227 
228 class FileViewset(viewsets.ModelViewSet):
229     serializer_class = serializers.FileSerializer
230     pagination_class = OptionalPageNumberPagination
231 
232     def get_queryset(self):
233         return models.File.objects.all()
234
```
--- 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/api.py b/kolibri/content/api.py
--- a/kolibri/content/api.py
+++ b/kolibri/content/api.py
@@ -222,7 +222,7 @@
         next_item = models.ContentNode.objects.filter(tree_id=this_item.tree_id, lft__gt=this_item.rght).order_by("lft").first()
         if not next_item:
             next_item = this_item.get_root()
-        return Response({'kind': next_item.kind, 'id': next_item.id})
+        return Response({'kind': next_item.kind, 'id': next_item.id, 'title': next_item.title})
 
 
 class FileViewset(viewsets.ModelViewSet):

{"golden_diff": "diff --git a/kolibri/content/api.py b/kolibri/content/api.py\n--- a/kolibri/content/api.py\n+++ b/kolibri/content/api.py\n@@ -222,7 +222,7 @@\n         next_item = models.ContentNode.objects.filter(tree_id=this_item.tree_id, lft__gt=this_item.rght).order_by(\"lft\").first()\n         if not next_item:\n             next_item = this_item.get_root()\n-        return Response({'kind': next_item.kind, 'id': next_item.id})\n+        return Response({'kind': next_item.kind, 'id': next_item.id, 'title': next_item.title})\n \n \n class FileViewset(viewsets.ModelViewSet):\n", "issue": "<Enhancement> A pop up on the screen when a learner masters an exercise would be great.\n## Summary\r\n\r\n<Enhancement> A pop up on the screen when a learner masters an exercise would be great.\r\n\r\nPresently the learner is not aware if he/she has mastered the topic, since the point addition happens very subtly.\r\nHence a Pop up on screen that would read \"You have Mastered the Exercise and earned 500 points, Keep Going\"  would be great. Anything even shorter would be great. \r\n\r\nIts just an on screen pop up would be great.\r\n\r\n## System information\r\n\r\n - Version: 0.4.0Beta1\r\n - Operating system: Linus LTS 14.04\r\n - Browser: Chrome\r\n\r\n## How to reproduce\r\n\r\n1. Master an exercise on a tablet.\r\n2. You not know if you have mastered the topic clearly .\r\n\r\n## Screenshots\r\n\r\n![img-20170511-wa0002](https://cloud.githubusercontent.com/assets/13453618/25945726/a11480a4-3665-11e7-9621-52a26a8be9c2.jpg)\r\n\r\n![img-20170511-wa0001](https://cloud.githubusercontent.com/assets/13453618/25945727/a2af45b6-3665-11e7-87db-d9d6003a41f0.jpg)\r\n\n", "before_files": [{"content": "from functools import reduce\nfrom random import sample\n\nfrom django.core.cache import cache\nfrom django.db.models import Q\nfrom django.db.models.aggregates import Count\nfrom kolibri.content import models, serializers\nfrom kolibri.content.content_db_router import get_active_content_database\nfrom kolibri.logger.models import ContentSessionLog, ContentSummaryLog\nfrom le_utils.constants import content_kinds\nfrom rest_framework import filters, pagination, viewsets\nfrom rest_framework.decorators import detail_route\nfrom rest_framework.response import Response\n\nfrom .utils.search import fuzz\n\ndef _join_with_logical_operator(lst, operator):\n    op = \") {operator} (\".format(operator=operator)\n    return \"(({items}))\".format(items=op.join(lst))\n\nclass ChannelMetadataCacheViewSet(viewsets.ModelViewSet):\n    serializer_class = serializers.ChannelMetadataCacheSerializer\n\n    def get_queryset(self):\n        return models.ChannelMetadataCache.objects.all()\n\n\nclass ContentNodeFilter(filters.FilterSet):\n    search = filters.django_filters.MethodFilter(action='title_description_filter')\n    recommendations_for = filters.django_filters.MethodFilter()\n    next_steps = filters.django_filters.MethodFilter()\n    popular = filters.django_filters.MethodFilter()\n    resume = filters.django_filters.MethodFilter()\n    kind = filters.django_filters.MethodFilter()\n    ids = filters.django_filters.MethodFilter()\n\n    class Meta:\n        model = models.ContentNode\n        fields = ['parent', 'search', 'prerequisite_for', 'has_prerequisite', 'related', 'recommendations_for', 'ids', 'content_id']\n\n    def title_description_filter(self, queryset, value):\n        \"\"\"\n        search for title or description that contains the keywords that are not necessary in adjacent\n        \"\"\"\n        exact_match = queryset.filter(Q(parent__isnull=False), Q(title__icontains=value) | Q(description__icontains=value))\n        if exact_match:\n            return exact_match\n        # if no exact match, fuzzy search using the stemmed_metaphone field in ContentNode that covers the title and description\n        fuzzed_tokens = [fuzz(word) for word in value.split()]\n        if not fuzzed_tokens[0]:\n            return []\n        token_queries = [reduce(lambda x, y: x | y, [Q(stemmed_metaphone__contains=token) for token in tokens]) for tokens in fuzzed_tokens]\n        return queryset.filter(\n            Q(parent__isnull=False),\n            reduce(lambda x, y: x & y, token_queries))\n\n    def filter_recommendations_for(self, queryset, value):\n        \"\"\"\n        Recommend items that are similar to this piece of content.\n        \"\"\"\n        recc_node = queryset.get(pk=value)\n        descendants = recc_node.get_descendants(include_self=False).exclude(kind__in=['topic', ''])\n        siblings = recc_node.get_siblings(include_self=False).exclude(kind__in=['topic', ''])\n        data = descendants | siblings  # concatenates different querysets\n        return data\n\n    def filter_next_steps(self, queryset, value):\n        \"\"\"\n        Recommend uncompleted content, content that has user completed content as a prerequisite.\n\n        :param queryset: all content nodes for this channel\n        :param value: id of currently logged in user, or none if user is anonymous\n        :return: uncompleted content nodes, or empty queryset if user is anonymous\n        \"\"\"\n\n        # if user is anonymous, don't return any nodes\n        if not value:\n            return queryset.none()\n\n        tables = [\n            '\"{summarylog_table}\" AS \"complete_log\"',\n            '\"{summarylog_table}\" AS \"incomplete_log\"',\n            '\"{content_table}\" AS \"complete_node\"',\n            '\"{content_table}\" AS \"incomplete_node\"',\n        ]\n        table_names = {\n            \"summarylog_table\": ContentSummaryLog._meta.db_table,\n            \"content_table\": models.ContentNode._meta.db_table,\n        }\n        # aliases for sql table names\n        sql_tables_and_aliases = [table.format(**table_names) for table in tables]\n        # where conditions joined by ANDs\n        where_statements = [\"NOT (incomplete_log.progress < 1 AND incomplete_log.content_id = incomplete_node.content_id)\",\n                            \"complete_log.user_id = '{user_id}'\".format(user_id=value),\n                            \"incomplete_log.user_id = '{user_id}'\".format(user_id=value),\n                            \"complete_log.progress = 1\",\n                            \"complete_node.rght = incomplete_node.lft - 1\",\n                            \"complete_log.content_id = complete_node.content_id\"]\n        # custom SQL query to get uncompleted content based on mptt algorithm\n        next_steps_recommendations = \"SELECT incomplete_node.* FROM {tables} WHERE {where}\".format(\n            tables=\", \".join(sql_tables_and_aliases),\n            where=_join_with_logical_operator(where_statements, \"AND\")\n        )\n        return models.ContentNode.objects.raw(next_steps_recommendations)\n\n    def filter_popular(self, queryset, value):\n        \"\"\"\n        Recommend content that is popular with all users.\n\n        :param queryset: all content nodes for this channel\n        :param value: id of currently logged in user, or none if user is anonymous\n        :return: 10 most popular content nodes\n        \"\"\"\n        if ContentSessionLog.objects.count() < 50:\n            # return 25 random content nodes if not enough session logs\n            pks = queryset.values_list('pk', flat=True).exclude(kind__in=['topic', ''])\n            count = min(pks.count(), 25)\n            return queryset.filter(pk__in=sample(list(pks), count))\n\n        cache_key = 'popular_for_{}'.format(get_active_content_database())\n        if cache.get(cache_key):\n            return cache.get(cache_key)\n\n        # get the most accessed content nodes\n        content_counts_sorted = ContentSessionLog.objects \\\n            .filter(channel_id=get_active_content_database()) \\\n            .values_list('content_id', flat=True) \\\n            .annotate(Count('content_id')) \\\n            .order_by('-content_id__count')\n\n        most_popular = queryset.filter(content_id__in=list(content_counts_sorted[:10]))\n\n        # cache the popular results queryset for 10 minutes, for efficiency\n        cache.set(cache_key, most_popular, 60 * 10)\n        return most_popular\n\n    def filter_resume(self, queryset, value):\n        \"\"\"\n        Recommend content that the user has recently engaged with, but not finished.\n\n        :param queryset: all content nodes for this channel\n        :param value: id of currently logged in user, or none if user is anonymous\n        :return: 10 most recently viewed content nodes\n        \"\"\"\n\n        # if user is anonymous, return no nodes\n        if not value:\n            return queryset.none()\n\n        # get the most recently viewed, but not finished, content nodes\n        content_ids = ContentSummaryLog.objects \\\n            .filter(user=value, channel_id=get_active_content_database()) \\\n            .exclude(progress=1) \\\n            .order_by('end_timestamp') \\\n            .values_list('content_id', flat=True) \\\n            .distinct()\n\n        resume = queryset.filter(content_id__in=list(content_ids[:10]))\n\n        return resume\n\n    def filter_kind(self, queryset, value):\n        \"\"\"\n        Show only content of a given kind.\n\n        :param queryset: all content nodes for this channel\n        :param value: 'content' for everything except topics, or one of the content kind constants\n        :return: content nodes of the given kind\n        \"\"\"\n        if value == 'content':\n            return queryset.exclude(kind=content_kinds.TOPIC).order_by(\"lft\")\n        return queryset.filter(kind=value).order_by(\"lft\")\n\n    def filter_ids(self, queryset, value):\n        return queryset.filter(pk__in=value.split(','))\n\n\nclass OptionalPageNumberPagination(pagination.PageNumberPagination):\n    \"\"\"\n    Pagination class that allows for page number-style pagination, when requested.\n    To activate, the `page_size` argument must be set. For example, to request the first 20 records:\n    `?page_size=20&page=1`\n    \"\"\"\n    page_size = None\n    page_size_query_param = \"page_size\"\n\n\nclass ContentNodeViewset(viewsets.ModelViewSet):\n    serializer_class = serializers.ContentNodeSerializer\n    filter_backends = (filters.DjangoFilterBackend,)\n    filter_class = ContentNodeFilter\n    pagination_class = OptionalPageNumberPagination\n\n    def get_queryset(self):\n        return models.ContentNode.objects.all().select_related(\n            'parent',\n            'license',\n        ).prefetch_related(\n            'assessmentmetadata',\n            'files',\n        )\n\n    @detail_route(methods=['get'])\n    def descendants(self, request, **kwargs):\n        node = self.get_object()\n        kind = self.request.query_params.get('descendant_kind', None)\n        descendants = node.get_descendants()\n        if kind:\n            descendants = descendants.filter(kind=kind)\n\n        serializer = self.get_serializer(descendants, many=True)\n        return Response(serializer.data)\n\n    @detail_route(methods=['get'])\n    def ancestors(self, request, **kwargs):\n        return Response(self.get_object().get_ancestors().values('pk', 'title'))\n\n    @detail_route(methods=['get'])\n    def next_content(self, request, **kwargs):\n        # retrieve the \"next\" content node, according to depth-first tree traversal\n        this_item = self.get_object()\n        next_item = models.ContentNode.objects.filter(tree_id=this_item.tree_id, lft__gt=this_item.rght).order_by(\"lft\").first()\n        if not next_item:\n            next_item = this_item.get_root()\n        return Response({'kind': next_item.kind, 'id': next_item.id})\n\n\nclass FileViewset(viewsets.ModelViewSet):\n    serializer_class = serializers.FileSerializer\n    pagination_class = OptionalPageNumberPagination\n\n    def get_queryset(self):\n        return models.File.objects.all()\n", "path": "kolibri/content/api.py"}], "after_files": [{"content": "from functools import reduce\nfrom random import sample\n\nfrom django.core.cache import cache\nfrom django.db.models import Q\nfrom django.db.models.aggregates import Count\nfrom kolibri.content import models, serializers\nfrom kolibri.content.content_db_router import get_active_content_database\nfrom kolibri.logger.models import ContentSessionLog, ContentSummaryLog\nfrom le_utils.constants import content_kinds\nfrom rest_framework import filters, pagination, viewsets\nfrom rest_framework.decorators import detail_route\nfrom rest_framework.response import Response\n\nfrom .utils.search import fuzz\n\ndef _join_with_logical_operator(lst, operator):\n    op = \") {operator} (\".format(operator=operator)\n    return \"(({items}))\".format(items=op.join(lst))\n\nclass ChannelMetadataCacheViewSet(viewsets.ModelViewSet):\n    serializer_class = serializers.ChannelMetadataCacheSerializer\n\n    def get_queryset(self):\n        return models.ChannelMetadataCache.objects.all()\n\n\nclass ContentNodeFilter(filters.FilterSet):\n    search = filters.django_filters.MethodFilter(action='title_description_filter')\n    recommendations_for = filters.django_filters.MethodFilter()\n    next_steps = filters.django_filters.MethodFilter()\n    popular = filters.django_filters.MethodFilter()\n    resume = filters.django_filters.MethodFilter()\n    kind = filters.django_filters.MethodFilter()\n    ids = filters.django_filters.MethodFilter()\n\n    class Meta:\n        model = models.ContentNode\n        fields = ['parent', 'search', 'prerequisite_for', 'has_prerequisite', 'related', 'recommendations_for', 'ids', 'content_id']\n\n    def title_description_filter(self, queryset, value):\n        \"\"\"\n        search for title or description that contains the keywords that are not necessary in adjacent\n        \"\"\"\n        exact_match = queryset.filter(Q(parent__isnull=False), Q(title__icontains=value) | Q(description__icontains=value))\n        if exact_match:\n            return exact_match\n        # if no exact match, fuzzy search using the stemmed_metaphone field in ContentNode that covers the title and description\n        fuzzed_tokens = [fuzz(word) for word in value.split()]\n        if not fuzzed_tokens[0]:\n            return []\n        token_queries = [reduce(lambda x, y: x | y, [Q(stemmed_metaphone__contains=token) for token in tokens]) for tokens in fuzzed_tokens]\n        return queryset.filter(\n            Q(parent__isnull=False),\n            reduce(lambda x, y: x & y, token_queries))\n\n    def filter_recommendations_for(self, queryset, value):\n        \"\"\"\n        Recommend items that are similar to this piece of content.\n        \"\"\"\n        recc_node = queryset.get(pk=value)\n        descendants = recc_node.get_descendants(include_self=False).exclude(kind__in=['topic', ''])\n        siblings = recc_node.get_siblings(include_self=False).exclude(kind__in=['topic', ''])\n        data = descendants | siblings  # concatenates different querysets\n        return data\n\n    def filter_next_steps(self, queryset, value):\n        \"\"\"\n        Recommend uncompleted content, content that has user completed content as a prerequisite.\n\n        :param queryset: all content nodes for this channel\n        :param value: id of currently logged in user, or none if user is anonymous\n        :return: uncompleted content nodes, or empty queryset if user is anonymous\n        \"\"\"\n\n        # if user is anonymous, don't return any nodes\n        if not value:\n            return queryset.none()\n\n        tables = [\n            '\"{summarylog_table}\" AS \"complete_log\"',\n            '\"{summarylog_table}\" AS \"incomplete_log\"',\n            '\"{content_table}\" AS \"complete_node\"',\n            '\"{content_table}\" AS \"incomplete_node\"',\n        ]\n        table_names = {\n            \"summarylog_table\": ContentSummaryLog._meta.db_table,\n            \"content_table\": models.ContentNode._meta.db_table,\n        }\n        # aliases for sql table names\n        sql_tables_and_aliases = [table.format(**table_names) for table in tables]\n        # where conditions joined by ANDs\n        where_statements = [\"NOT (incomplete_log.progress < 1 AND incomplete_log.content_id = incomplete_node.content_id)\",\n                            \"complete_log.user_id = '{user_id}'\".format(user_id=value),\n                            \"incomplete_log.user_id = '{user_id}'\".format(user_id=value),\n                            \"complete_log.progress = 1\",\n                            \"complete_node.rght = incomplete_node.lft - 1\",\n                            \"complete_log.content_id = complete_node.content_id\"]\n        # custom SQL query to get uncompleted content based on mptt algorithm\n        next_steps_recommendations = \"SELECT incomplete_node.* FROM {tables} WHERE {where}\".format(\n            tables=\", \".join(sql_tables_and_aliases),\n            where=_join_with_logical_operator(where_statements, \"AND\")\n        )\n        return models.ContentNode.objects.raw(next_steps_recommendations)\n\n    def filter_popular(self, queryset, value):\n        \"\"\"\n        Recommend content that is popular with all users.\n\n        :param queryset: all content nodes for this channel\n        :param value: id of currently logged in user, or none if user is anonymous\n        :return: 10 most popular content nodes\n        \"\"\"\n        if ContentSessionLog.objects.count() < 50:\n            # return 25 random content nodes if not enough session logs\n            pks = queryset.values_list('pk', flat=True).exclude(kind__in=['topic', ''])\n            count = min(pks.count(), 25)\n            return queryset.filter(pk__in=sample(list(pks), count))\n\n        cache_key = 'popular_for_{}'.format(get_active_content_database())\n        if cache.get(cache_key):\n            return cache.get(cache_key)\n\n        # get the most accessed content nodes\n        content_counts_sorted = ContentSessionLog.objects \\\n            .filter(channel_id=get_active_content_database()) \\\n            .values_list('content_id', flat=True) \\\n            .annotate(Count('content_id')) \\\n            .order_by('-content_id__count')\n\n        most_popular = queryset.filter(content_id__in=list(content_counts_sorted[:10]))\n\n        # cache the popular results queryset for 10 minutes, for efficiency\n        cache.set(cache_key, most_popular, 60 * 10)\n        return most_popular\n\n    def filter_resume(self, queryset, value):\n        \"\"\"\n        Recommend content that the user has recently engaged with, but not finished.\n\n        :param queryset: all content nodes for this channel\n        :param value: id of currently logged in user, or none if user is anonymous\n        :return: 10 most recently viewed content nodes\n        \"\"\"\n\n        # if user is anonymous, return no nodes\n        if not value:\n            return queryset.none()\n\n        # get the most recently viewed, but not finished, content nodes\n        content_ids = ContentSummaryLog.objects \\\n            .filter(user=value, channel_id=get_active_content_database()) \\\n            .exclude(progress=1) \\\n            .order_by('end_timestamp') \\\n            .values_list('content_id', flat=True) \\\n            .distinct()\n\n        resume = queryset.filter(content_id__in=list(content_ids[:10]))\n\n        return resume\n\n    def filter_kind(self, queryset, value):\n        \"\"\"\n        Show only content of a given kind.\n\n        :param queryset: all content nodes for this channel\n        :param value: 'content' for everything except topics, or one of the content kind constants\n        :return: content nodes of the given kind\n        \"\"\"\n        if value == 'content':\n            return queryset.exclude(kind=content_kinds.TOPIC).order_by(\"lft\")\n        return queryset.filter(kind=value).order_by(\"lft\")\n\n    def filter_ids(self, queryset, value):\n        return queryset.filter(pk__in=value.split(','))\n\n\nclass OptionalPageNumberPagination(pagination.PageNumberPagination):\n    \"\"\"\n    Pagination class that allows for page number-style pagination, when requested.\n    To activate, the `page_size` argument must be set. For example, to request the first 20 records:\n    `?page_size=20&page=1`\n    \"\"\"\n    page_size = None\n    page_size_query_param = \"page_size\"\n\n\nclass ContentNodeViewset(viewsets.ModelViewSet):\n    serializer_class = serializers.ContentNodeSerializer\n    filter_backends = (filters.DjangoFilterBackend,)\n    filter_class = ContentNodeFilter\n    pagination_class = OptionalPageNumberPagination\n\n    def get_queryset(self):\n        return models.ContentNode.objects.all().select_related(\n            'parent',\n            'license',\n        ).prefetch_related(\n            'assessmentmetadata',\n            'files',\n        )\n\n    @detail_route(methods=['get'])\n    def descendants(self, request, **kwargs):\n        node = self.get_object()\n        kind = self.request.query_params.get('descendant_kind', None)\n        descendants = node.get_descendants()\n        if kind:\n            descendants = descendants.filter(kind=kind)\n\n        serializer = self.get_serializer(descendants, many=True)\n        return Response(serializer.data)\n\n    @detail_route(methods=['get'])\n    def ancestors(self, request, **kwargs):\n        return Response(self.get_object().get_ancestors().values('pk', 'title'))\n\n    @detail_route(methods=['get'])\n    def next_content(self, request, **kwargs):\n        # retrieve the \"next\" content node, according to depth-first tree traversal\n        this_item = self.get_object()\n        next_item = models.ContentNode.objects.filter(tree_id=this_item.tree_id, lft__gt=this_item.rght).order_by(\"lft\").first()\n        if not next_item:\n            next_item = this_item.get_root()\n        return Response({'kind': next_item.kind, 'id': next_item.id, 'title': next_item.title})\n\n\nclass FileViewset(viewsets.ModelViewSet):\n    serializer_class = serializers.FileSerializer\n    pagination_class = OptionalPageNumberPagination\n\n    def get_queryset(self):\n        return models.File.objects.all()\n", "path": "kolibri/content/api.py"}]}