Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
4aa4700
new django app for housing Kolibri-specific public APIs
AllanOXDi Jan 31, 2023
fc21261
registered the kolibri_public app
AllanOXDi Feb 1, 2023
9e854f5
Refactor kolibri_content models to more closely match the kolibri.cor…
rtibbles Feb 8, 2023
3a17822
Match base implementation of values viewset to Kolibri
rtibbles Feb 8, 2023
0c305dd
Add cursor based pagination class for values viewset.
rtibbles Feb 8, 2023
bb45fef
Add kolibri_public models as a mostly direct copy from kolibri.core.c…
rtibbles Feb 8, 2023
5a8d9a8
Create custom middlewares that allow exemption for sessions and locale.
rtibbles Feb 8, 2023
6715053
Add content node viewsets and tests.
rtibbles Feb 8, 2023
5529b1f
Move v1 public API into kolibri_public.
rtibbles Mar 10, 2023
aaf8384
Set Kolibri version in device info.
rtibbles Mar 10, 2023
a591335
Add v2 channel endpoint.
rtibbles Mar 14, 2023
ef7fa8a
Add import metadata public endpoint.
rtibbles Mar 14, 2023
837afe3
Update Studio device info endpoint to return versioned info.
rtibbles Mar 14, 2023
f656d2b
Add cache headers and test to import_metadata
rtibbles Mar 14, 2023
a631a6f
Add MPTTTreeIdManager model for kolibri_public.
rtibbles Mar 14, 2023
ae70c3d
Clean up no longer used helper function.
rtibbles Mar 14, 2023
917f345
Move channel builder test utility into base and make it flexible for …
rtibbles Mar 14, 2023
79382f9
Add mapper utility to map published content DB to kolibri_public models.
rtibbles Mar 14, 2023
1b375be
Map public channels into kolibri_public when published.
rtibbles Mar 14, 2023
7d3e0bc
Update channel last_updated on update
rtibbles Mar 14, 2023
4af0583
Add management command to backfill kolibri_public channels.
rtibbles Mar 14, 2023
19e2abc
Better handle different models being used in ChannelBuilder test helper.
rtibbles Mar 14, 2023
9c06698
Ensure the currently annotated channel is always included in the orde…
rtibbles Mar 16, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions contentcuration/contentcuration/db/models/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,6 @@ def log_lock_time_spent(timespent):
logging.debug("Spent {} seconds inside an mptt lock".format(timespent))


def execute_queryset_without_results(queryset):
query = queryset.query
compiler = query.get_compiler(queryset.db)
sql, params = compiler.as_sql()
if not sql:
return
cursor = compiler.connection.cursor()
cursor.execute(sql, params)


class CustomContentNodeTreeManager(TreeManager.from_queryset(CustomTreeQuerySet)):
# Added 7-31-2018. We can remove this once we are certain we have eliminated all cases
# where root nodes are getting prepended rather than appended to the tree list.
Expand Down
23 changes: 23 additions & 0 deletions contentcuration/contentcuration/middleware/locale.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from django.middleware.locale import LocaleMiddleware

LOCALE_EXEMPT = "_locale_exempt"


def locale_exempt(view):
setattr(view, LOCALE_EXEMPT, True)
return view


class KolibriStudioLocaleMiddleware(LocaleMiddleware):
def _is_exempt(self, obj):
return hasattr(obj, LOCALE_EXEMPT)

def process_view(self, request, callback, callback_args, callback_kwargs):
if self._is_exempt(callback):
setattr(request, LOCALE_EXEMPT, True)
return None

def process_response(self, request, response):
if self._is_exempt(request):
return response
return super(KolibriStudioLocaleMiddleware, self).process_response(request, response)
23 changes: 23 additions & 0 deletions contentcuration/contentcuration/middleware/session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from django.contrib.sessions.middleware import SessionMiddleware

SESSION_EXEMPT = "_session_exempt"


def session_exempt(view):
setattr(view, SESSION_EXEMPT, True)
return view


class KolibriStudioSessionMiddleware(SessionMiddleware):
def _is_exempt(self, obj):
return hasattr(obj, SESSION_EXEMPT)

def process_view(self, request, callback, callback_args, callback_kwargs):
if self._is_exempt(callback):
setattr(request, SESSION_EXEMPT, True)
return None

def process_response(self, request, response):
if self._is_exempt(request):
return response
return super(KolibriStudioSessionMiddleware, self).process_response(request, response)
5 changes: 3 additions & 2 deletions contentcuration/contentcuration/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
'mathfilters',
'django.contrib.postgres',
'django_celery_results',
'kolibri_public',
)

SESSION_ENGINE = "django.contrib.sessions.backends.cached_db"
Expand Down Expand Up @@ -123,8 +124,8 @@

MIDDLEWARE = (
# 'django.middleware.cache.UpdateCacheMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'contentcuration.middleware.session.KolibriStudioSessionMiddleware',
'contentcuration.middleware.locale.KolibriStudioLocaleMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.common.BrokenLinkEmailsMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
Expand Down
13 changes: 2 additions & 11 deletions contentcuration/contentcuration/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@
from django.urls import path
from django.urls import re_path
from django.views.generic.base import RedirectView
from kolibri_public.urls import urlpatterns as kolibri_public_urls
from rest_framework import routers

import contentcuration.views.admin as admin_views
import contentcuration.views.base as views
import contentcuration.views.internal as internal_views
import contentcuration.views.nodes as node_views
import contentcuration.views.public as public_views
import contentcuration.views.settings as settings_views
import contentcuration.views.users as registration_views
import contentcuration.views.zip as zip_views
Expand Down Expand Up @@ -91,12 +91,7 @@ def get_redirect_url(self, *args, **kwargs):


# Add public api endpoints
urlpatterns += [
re_path(r'^api/public/channel/(?P<channel_id>[^/]+)', public_views.get_channel_name_by_id, name='get_channel_name_by_id'),
re_path(r'^api/public/(?P<version>[^/]+)/channels$', public_views.get_public_channel_list, name='get_public_channel_list'),
re_path(r'^api/public/(?P<version>[^/]+)/channels/lookup/(?P<identifier>[^/]+)', public_views.get_public_channel_lookup, name='get_public_channel_lookup'),
re_path(r'^api/public/info', public_views.InfoViewSet.as_view({'get': 'list'}), name='info'),
]
urlpatterns += kolibri_public_urls

# Add node api enpoints
urlpatterns += [
Expand Down Expand Up @@ -147,10 +142,6 @@ def get_redirect_url(self, *args, **kwargs):
urlpatterns += [re_path(r'^jsreverse/$', django_js_reverse_views.urls_js, name='js_reverse')]

# I18N Endpoints
js_info_dict = {
'packages': ('your.app.package',),
}

urlpatterns += [
re_path(r'^i18n/', include('django.conf.urls.i18n')),
]
Expand Down
116 changes: 116 additions & 0 deletions contentcuration/contentcuration/utils/pagination.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import hashlib
from base64 import b64encode
from collections import OrderedDict
from urllib.parse import urlencode

from django.core.cache import cache
from django.core.exceptions import EmptyResultSet
Expand All @@ -7,6 +10,8 @@
from django.core.paginator import Paginator
from django.db.models import QuerySet
from django.utils.functional import cached_property
from rest_framework.pagination import _reverse_ordering
from rest_framework.pagination import CursorPagination
from rest_framework.pagination import NotFound
from rest_framework.pagination import PageNumberPagination
from rest_framework.response import Response
Expand Down Expand Up @@ -129,3 +134,114 @@ def get_paginated_response_schema(self, schema):

class CachedListPagination(ValuesViewsetPageNumberPagination):
django_paginator_class = CachedValuesViewsetPaginator


class ValuesViewsetCursorPagination(CursorPagination):
def paginate_queryset(self, queryset, request, view=None):
pks_queryset = super(ValuesViewsetCursorPagination, self).paginate_queryset(
queryset, request, view=view
)
if pks_queryset is None:
return None
self.request = request
if self.cursor is None:
reverse = False
else:
_, reverse, _ = self.cursor
ordering = _reverse_ordering(self.ordering) if reverse else self.ordering
return queryset.filter(pk__in=[obj.pk for obj in pks_queryset]).order_by(
*ordering
)

def get_more(self): # noqa C901
"""
Vendored and modified from
https://github.com/encode/django-rest-framework/blob/6ea95b6ad1bc0d4a4234a267b1ba32701878c6bb/rest_framework/pagination.py#L694
"""
if not self.has_next:
return None

if (
self.page
and self.cursor
and self.cursor.reverse
and self.cursor.offset != 0
):
# If we're reversing direction and we have an offset cursor
# then we cannot use the first position we find as a marker.
compare = self._get_position_from_instance(self.page[-1], self.ordering)
else:
compare = self.next_position
offset = 0

has_item_with_unique_position = False
for item in reversed(self.page):
position = self._get_position_from_instance(item, self.ordering)
if position != compare:
# The item in this position and the item following it
# have different positions. We can use this position as
# our marker.
has_item_with_unique_position = True
break

# The item in this position has the same position as the item
# following it, we can't use it as a marker position, so increment
# the offset and keep seeking to the previous item.
compare = position
offset += 1

if self.page and not has_item_with_unique_position:
# There were no unique positions in the page.
if not self.has_previous:
# We are on the first page.
# Our cursor will have an offset equal to the page size,
# but no position to filter against yet.
offset = self.page_size
position = None
elif self.cursor.reverse:
# The change in direction will introduce a paging artifact,
# where we end up skipping forward a few extra items.
offset = 0
position = self.previous_position
else:
# Use the position from the existing cursor and increment
# it's offset by the page size.
offset = self.cursor.offset + self.page_size
position = self.previous_position

if not self.page:
position = self.next_position

tokens = {}
if offset != 0:
tokens["o"] = str(offset)
if position is not None:
tokens["p"] = position

querystring = urlencode(tokens, doseq=True)
encoded = b64encode(querystring.encode("ascii")).decode("ascii")
params = self.request.query_params.copy()
params.update(
{
self.cursor_query_param: encoded,
}
)
return params

def get_paginated_response(self, data):
return Response(OrderedDict([("more", self.get_more()), ("results", data)]))

def get_paginated_response_schema(self, schema):
return {
"type": "object",
"properties": {
"more": {
"type": "object",
"nullable": True,
"example": {
"cursor": "asdadshjashjadh",
},
},
"results": schema,
},
}
6 changes: 5 additions & 1 deletion contentcuration/contentcuration/utils/publish.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from kolibri_content import models as kolibrimodels
from kolibri_content.router import get_active_content_database
from kolibri_content.router import using_content_database
from kolibri_public.utils.mapper import ChannelMapper
from le_utils.constants import completion_criteria
from le_utils.constants import content_kinds
from le_utils.constants import exercises
Expand Down Expand Up @@ -139,12 +140,15 @@ def create_content_database(channel, force, user_id, force_exercises, progress_t
progress_tracker=progress_tracker,
)
tree_mapper.map_nodes()
map_channel_to_kolibri_channel(channel)
kolibri_channel = map_channel_to_kolibri_channel(channel)
# It should be at this percent already, but just in case.
if progress_tracker:
progress_tracker.track(90)
map_prerequisites(channel.main_tree)
save_export_database(channel.pk)
if channel.public:
mapper = ChannelMapper(kolibri_channel)
mapper.run()

return tempdb

Expand Down
Loading