Skip to content

Commit 8e0666b

Browse files
committed
Merge branch 'release-v0.5.x' into develop
# Conflicts: # kolibri/plugins/learn/assets/src/views/content-card-grid/index.vue # kolibri/plugins/management/assets/src/views/manage-content-page/channels-grid.vue # requirements/base.txt
2 parents 5bb5ffc + 970e016 commit 8e0666b

File tree

97 files changed

+1155
-628
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

97 files changed

+1155
-628
lines changed

kolibri/content/api.py

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from le_utils.constants import content_kinds
1313
from rest_framework import filters, pagination, viewsets
1414
from rest_framework.decorators import detail_route, list_route
15+
from rest_framework.generics import get_object_or_404
1516
from rest_framework.response import Response
1617
from six.moves.urllib.parse import parse_qs, urlparse
1718

@@ -250,15 +251,48 @@ class ContentNodeViewset(viewsets.ModelViewSet):
250251
filter_class = ContentNodeFilter
251252
pagination_class = OptionalPageNumberPagination
252253

253-
def get_queryset(self):
254-
return models.ContentNode.objects.all().prefetch_related(
254+
def prefetch_related(self, queryset):
255+
return queryset.prefetch_related(
255256
'assessmentmetadata',
256257
'files',
257258
).select_related('license')
258259

260+
def get_queryset(self, prefetch=True):
261+
queryset = models.ContentNode.objects.all()
262+
if prefetch:
263+
return self.prefetch_related(queryset)
264+
return queryset
265+
266+
def get_object(self, prefetch=True):
267+
"""
268+
Returns the object the view is displaying.
269+
You may want to override this if you need to provide non-standard
270+
queryset lookups. Eg if objects are referenced using multiple
271+
keyword arguments in the url conf.
272+
"""
273+
queryset = self.filter_queryset(self.get_queryset(prefetch=prefetch))
274+
275+
# Perform the lookup filtering.
276+
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
277+
278+
assert lookup_url_kwarg in self.kwargs, (
279+
'Expected view %s to be called with a URL keyword argument '
280+
'named "%s". Fix your URL conf, or set the `.lookup_field` '
281+
'attribute on the view correctly.' %
282+
(self.__class__.__name__, lookup_url_kwarg)
283+
)
284+
285+
filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
286+
obj = get_object_or_404(queryset, **filter_kwargs)
287+
288+
# May raise a permission denied
289+
self.check_object_permissions(self.request, obj)
290+
291+
return obj
292+
259293
@detail_route(methods=['get'])
260294
def descendants(self, request, **kwargs):
261-
node = self.get_object()
295+
node = self.get_object(prefetch=False)
262296
kind = self.request.query_params.get('descendant_kind', None)
263297
descendants = node.get_descendants()
264298
if kind:
@@ -269,7 +303,16 @@ def descendants(self, request, **kwargs):
269303

270304
@detail_route(methods=['get'])
271305
def ancestors(self, request, **kwargs):
272-
return Response(self.get_object().get_ancestors().values('pk', 'title'))
306+
cache_key = 'contentnode_ancestors_{db}_{pk}'.format(db=get_active_content_database(), pk=kwargs.get('pk'))
307+
308+
if cache.get(cache_key) is not None:
309+
return Response(cache.get(cache_key))
310+
311+
ancestors = list(self.get_object(prefetch=False).get_ancestors().values('pk', 'title'))
312+
313+
cache.set(cache_key, ancestors, 60 * 10)
314+
315+
return Response(ancestors)
273316

274317
@detail_route(methods=['get'])
275318
def next_content(self, request, **kwargs):

kolibri/content/serializers.py

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from django.core.cache import cache
12
from django.db.models import Manager, Sum
23
from django.db.models.query import RawQuerySet
34
from kolibri.content.models import AssessmentMetaData, ChannelMetadataCache, ContentNode, File
@@ -115,6 +116,20 @@ class ContentNodeListSerializer(serializers.ListSerializer):
115116

116117
def to_representation(self, data):
117118

119+
# Dealing with nested relationships, data can be a Manager,
120+
# so, first get a queryset from the Manager if needed
121+
data = data.all() if isinstance(data, Manager) else data
122+
123+
cache_key = None
124+
# Cache parent look ups only
125+
if "parent" in self.context['request'].GET:
126+
cache_key = 'contentnode_list_{db}_{parent}'.format(
127+
db=get_active_content_database(),
128+
parent=self.context['request'].GET.get('parent'))
129+
130+
if cache.get(cache_key):
131+
return cache.get(cache_key)
132+
118133
if not data:
119134
return data
120135

@@ -125,17 +140,25 @@ def to_representation(self, data):
125140
# Don't annotate topic progress as too expensive
126141
progress_dict = get_content_progress_fractions(data, user)
127142

128-
# Dealing with nested relationships, data can be a Manager,
129-
# so, first get a queryset from the Manager if needed
130-
iterable = data.all() if isinstance(data, Manager) else data
131-
132-
return [
133-
self.child.to_representation(
143+
result = []
144+
topic_only = True
145+
for item in data:
146+
obj = self.child.to_representation(
134147
item,
135148
progress_fraction=progress_dict.get(item.content_id),
136149
annotate_progress_fraction=False
137-
) for item in iterable
138-
]
150+
)
151+
topic_only = topic_only and obj.get('kind') == content_kinds.TOPIC
152+
result.append(obj)
153+
154+
# Only store if all nodes are topics, because we don't annotate progress on them
155+
# This has the happy side effect of not caching our dynamically calculated
156+
# recommendation queries, which might change for the same user over time
157+
# because they do not return topics
158+
if topic_only and cache_key:
159+
cache.set(cache_key, result, 60 * 10)
160+
161+
return result
139162

140163

141164
class ContentNodeSerializer(serializers.ModelSerializer):
@@ -165,7 +188,8 @@ def to_representation(self, instance, progress_fraction=None, annotate_progress_
165188
progress_fraction = 0.0
166189
else:
167190
user = self.context["request"].user
168-
progress_fraction = get_content_progress_fraction(instance, user)
191+
if instance.kind != content_kinds.TOPIC:
192+
progress_fraction = get_content_progress_fraction(instance, user)
169193
value = super(ContentNodeSerializer, self).to_representation(instance)
170194
value['progress_fraction'] = progress_fraction
171195
return value

kolibri/core/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@
33
44
"""
55
from __future__ import print_function, unicode_literals, absolute_import
6+
7+
default_app_config = "kolibri.core.apps.KolibriCoreConfig"

kolibri/core/apps.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from __future__ import unicode_literals
2+
3+
from django.apps import AppConfig
4+
from django.db.backends.signals import connection_created
5+
6+
7+
class KolibriCoreConfig(AppConfig):
8+
name = 'kolibri.core'
9+
10+
def ready(self):
11+
"""
12+
Sets up PRAGMAs.
13+
"""
14+
connection_created.connect(self.activate_pragmas_per_connection)
15+
self.activate_pragmas_on_start()
16+
17+
@staticmethod
18+
def activate_pragmas_per_connection(sender, connection, **kwargs):
19+
"""
20+
Activate SQLite3 PRAGMAs that apply on a per-connection basis. A no-op
21+
right now, but kept around as infrastructure if we ever want to add
22+
PRAGMAs in the future.
23+
"""
24+
25+
if connection.vendor == "sqlite":
26+
cursor = connection.cursor()
27+
28+
# Shorten the default WAL autocheckpoint from 1000 pages to 500
29+
cursor.execute("PRAGMA wal_autocheckpoint=500;")
30+
31+
# We don't turn on the following pragmas, because they have negligible
32+
# performance impact. For reference, here's what we've tested:
33+
34+
# Don't ensure that the OS has fully flushed
35+
# our data to disk.
36+
# cursor.execute("PRAGMA synchronous=OFF;")
37+
38+
# Store cross-database JOINs in memory.
39+
# cursor.execute("PRAGMA temp_store=MEMORY;")
40+
41+
@staticmethod
42+
def activate_pragmas_on_start():
43+
"""
44+
Activate a set of PRAGMAs that apply to the database itself,
45+
and not on a per connection basis.
46+
:return:
47+
"""
48+
from django.db import connection
49+
50+
if connection.vendor == "sqlite":
51+
cursor = connection.cursor()
52+
53+
# http://www.sqlite.org/wal.html
54+
# WAL's main advantage allows simultaneous reads
55+
# and writes (vs. the default exclusive write lock)
56+
# at the cost of a slight penalty to all reads.
57+
cursor.execute("PRAGMA journal_mode=WAL;")

kolibri/core/assets/src/heartbeat.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import logger from 'kolibri.lib.logging';
33
const logging = logger.getLogger(__filename);
44

55
export default class HeartBeat {
6-
constructor(kolibri, delay = 60000) {
6+
constructor(kolibri, delay = 150000) {
77
if (!kolibri) {
88
throw new ReferenceError('A kolibri instance must be passed into the constructor');
99
}

kolibri/core/assets/src/state/actions.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ import { redirectBrowser } from '../utils/browser';
2121

2222
const logging = logger.getLogger(__filename);
2323
const intervalTime = 5000; // Frequency at which time logging is updated
24-
const progressThreshold = 0.1; // Update logs if user has reached 20% more progress
25-
const timeThreshold = 30; // Update logs if 30 seconds have passed since last update
24+
const progressThreshold = 0.25; // Update logs if user has reached 25% more progress
25+
const timeThreshold = 60; // Update logs if 60 seconds have passed since last update
2626

2727
/**
2828
* Vuex State Mappers

kolibri/core/assets/src/utils/import-intl-locale.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ module.exports = (locale, callback) => {
2424
resolve(() => require('intl/locale-data/jsonp/es-ES.js'));
2525
});
2626
});
27+
case 'es-MX':
28+
return new Promise(resolve => {
29+
require.ensure([], require => {
30+
resolve(() => require('intl/locale-data/jsonp/es-MX.js'));
31+
});
32+
});
2733
default:
2834
return new Promise(resolve => {
2935
require.ensure([], require => {

kolibri/core/webpack/hooks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ def bundle(self):
138138
"""
139139
for f in self._stats_file_content["files"]:
140140
filename = f['name']
141-
if any(regex.match(filename) for regex in settings.IGNORE_PATTERNS):
141+
if any(list(regex.match(filename) for regex in settings.IGNORE_PATTERNS)):
142142
continue
143143
relpath = '{0}/{1}'.format(self.unique_slug, filename)
144144
if django_settings.DEBUG:

kolibri/locale/docs/es_ES/LC_MESSAGES/authors.po

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ msgid ""
22
msgstr ""
33
"Project-Id-Version: kolibri\n"
44
"Report-Msgid-Bugs-To: \n"
5-
"POT-Creation-Date: 2017-02-02 00:07+0000\n"
6-
"PO-Revision-Date: 2017-03-31 03:03-0400\n"
5+
"POT-Creation-Date: 2017-03-02 23:01+0000\n"
6+
"PO-Revision-Date: 2017-07-28 17:27-0400\n"
77
"Last-Translator: learningequality <[email protected]>\n"
88
"Language-Team: Spanish\n"
99
"MIME-Version: 1.0\n"
@@ -14,7 +14,7 @@ msgstr ""
1414
"X-Generator: crowdin.com\n"
1515
"X-Crowdin-Project: kolibri\n"
1616
"X-Crowdin-Language: es-ES\n"
17-
"X-Crowdin-File: /release/docs/en/LC_MESSAGES/authors.po\n"
17+
"X-Crowdin-File: /release-v0.5.x/docs/en/LC_MESSAGES/authors.po\n"
1818
"Language: es_ES\n"
1919

2020
#: ../../../AUTHORS.rst:3

kolibri/locale/docs/es_ES/LC_MESSAGES/changelog.po

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ msgstr ""
33
"Project-Id-Version: kolibri\n"
44
"Report-Msgid-Bugs-To: \n"
55
"POT-Creation-Date: 2017-03-02 23:01+0000\n"
6-
"PO-Revision-Date: 2017-05-19 01:26-0400\n"
6+
"PO-Revision-Date: 2017-07-28 17:27-0400\n"
77
"Last-Translator: learningequality <[email protected]>\n"
88
"Language-Team: Spanish\n"
99
"MIME-Version: 1.0\n"
@@ -14,7 +14,7 @@ msgstr ""
1414
"X-Generator: crowdin.com\n"
1515
"X-Crowdin-Project: kolibri\n"
1616
"X-Crowdin-Language: es-ES\n"
17-
"X-Crowdin-File: /release-v0.4.x/docs/en/LC_MESSAGES/changelog.po\n"
17+
"X-Crowdin-File: /release-v0.5.x/docs/en/LC_MESSAGES/changelog.po\n"
1818
"Language: es_ES\n"
1919

2020
#: ../../../CHANGELOG.rst:4

0 commit comments

Comments
 (0)