Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release v0.17.x into develop #12591

Merged
merged 59 commits into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
f7ea1d4
Allow the FileDownload class to follow redirects.
rtibbles Jun 18, 2024
7d91c02
Fix last record query
AlexVelezLl Jul 1, 2024
3dcb132
Fix id related sorting with timestamp
AlexVelezLl Jul 1, 2024
beb1a1e
Avoid update contentSummaryLog completion_timestamp if already set
AlexVelezLl Jul 2, 2024
8c39b1a
Save completion_timestamp as lesson/resource ccompleted notification …
AlexVelezLl Jul 2, 2024
cdb559e
Fix sorting key in frontend and check empty summarized notifications
AlexVelezLl Jul 2, 2024
f244813
Calculate resource completion notification tiemstamp
AlexVelezLl Jul 3, 2024
a04d18f
Update notifications timestamp of the first and last created/complete…
AlexVelezLl Jul 3, 2024
e35acf7
Add check to completion_timestamp to determine if a resource have bee…
AlexVelezLl Jul 3, 2024
0cd4a36
Update help needed timestamp
AlexVelezLl Jul 3, 2024
9ff55fa
Refactor duplicated code and improve comments
AlexVelezLl Jul 4, 2024
a1eccbe
improve before/after filters with timestamps
AlexVelezLl Jul 4, 2024
92d6ccc
Refactor filter logic
AlexVelezLl Jul 4, 2024
29afe74
Add timestamp index in learner progress notification
AlexVelezLl Jul 4, 2024
f74a047
Fix frontend ordering and edge cases
AlexVelezLl Jul 8, 2024
3080ed6
Fix excercise resource notifications logged on first attempt
AlexVelezLl Jul 8, 2024
b1c77cd
Fix tests
AlexVelezLl Jul 8, 2024
dc29843
Refactor notifications tests and improve attemptlog testing
AlexVelezLl Jul 17, 2024
6b95d68
Add parse_summarylog tests with multiple resources in a lesson
AlexVelezLl Jul 17, 2024
2224dfb
Add create_summarylog tests with multiple resources in a lesson
AlexVelezLl Jul 17, 2024
b4755c2
test batch process summary log
AlexVelezLl Jul 17, 2024
39c6120
test batch attemptlogs
AlexVelezLl Jul 17, 2024
17ab6f7
Refactor
AlexVelezLl Jul 18, 2024
ce499c2
Cleanup Django and DRF related deprecation warnings.
rtibbles Jul 17, 2024
045d0f0
Cleanup sqlalchemy deprecation warnings.
rtibbles Jul 17, 2024
3c23bb8
Remove asserts that aren't clearly dev intended.
rtibbles Jul 18, 2024
92660ef
Cleanup miscellaneous Django 4 deprecations.
rtibbles Jul 18, 2024
585694e
use borrowed string in recoverable error state when device has no fac…
nucleogenesis Jul 2, 2024
5578fe3
initialize JoinOrNewLOD component w/ value from machine context (save…
nucleogenesis Jul 2, 2024
6beef53
update typedef
nucleogenesis Jul 22, 2024
8058b00
use device filtering to preemptively remove devices from list if they…
nucleogenesis Jul 22, 2024
ce91f4a
use i18n string for error message
nucleogenesis Jul 22, 2024
66d9a9d
deviceHasMatchingFacility treats empty filter as wildcard
nucleogenesis Jul 26, 2024
1b7a759
use filterByHasFacilities prop
nucleogenesis Jul 29, 2024
332c49d
Add new api for fetching channel_thumnail and return the uri of thumb…
thesujai Aug 2, 2024
213665c
method to fetch thumbnail
thesujai Aug 2, 2024
c73e29f
handle case when thumbnail is an url and when thumnail is a base64 st…
thesujai Aug 2, 2024
537242d
use field_maps and decode the thumnail and serve it as aimg file
thesujai Aug 5, 2024
3fd704b
Upgrade whitenoise dependency.
rtibbles Aug 5, 2024
db3da4b
Upgrade colorlog dependency.
rtibbles May 13, 2024
af7b8e7
Upgrade all Python dependencies to latest versions that are compatibl…
rtibbles Aug 5, 2024
fea4e9a
Don't error if we can't detect the timezone with new tzlocal error.
rtibbles Jun 26, 2024
e73ce95
test for ChannelThumbnailView
thesujai Aug 5, 2024
c6ddc76
handle malformed thumbnails and return no url when thumbnail not exis…
thesujai Aug 5, 2024
519d50e
jut get he mimetype from the header of thumbnail
thesujai Aug 7, 2024
81893fd
Merge pull request #12469 from rtibbles/deprecation_warnings
bjester Aug 8, 2024
0b24c60
Merge pull request #12530 from thesujai/no-more-base64-thumbnail
rtibbles Aug 8, 2024
365ed24
Merge pull request #12386 from AlexVelezLl/fix-activity-list
rtibbles Aug 8, 2024
e0498f0
appease the linter
nucleogenesis Jul 17, 2024
2a70bff
auto-save changes when user goes to add questions from section editor
nucleogenesis Aug 9, 2024
6992f95
Merge pull request #12165 from rtibbles/python_deps
marcellamaki Aug 12, 2024
ac56b66
Merge pull request #12462 from nucleogenesis/linting-coach-post-eqm
rtibbles Aug 13, 2024
cbb7353
Merge pull request #12309 from rtibbles/allow_redirects
marcellamaki Aug 19, 2024
113e247
Ensure linting errors trigger a non-zero exit code.
rtibbles Aug 20, 2024
1353e99
Merge pull request #12397 from nucleogenesis/fix--noisy-unprovisioned…
rtibbles Aug 22, 2024
53e03ac
Merge pull request #12572 from rtibbles/lint_fix
rtibbles Aug 23, 2024
59eb7cc
Fix uncaught linting error
rtibbles Aug 23, 2024
649223d
Ignore false alarm for undefined string rule
rtibbles Aug 23, 2024
3413aa1
Merge pull request #12592 from learningequality/rtibbles-patch-1
rtibbles Aug 23, 2024
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
5 changes: 0 additions & 5 deletions kolibri/core/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +0,0 @@
"""TODO: Write something about this module (everything in the docstring
enters the docs)

"""
default_app_config = "kolibri.core.apps.KolibriCoreConfig"
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@
import UiAlert from 'kolibri-design-system/lib/keen/UiAlert';
import commonCoreStrings from 'kolibri.coreVue.mixins.commonCoreStrings';
import commonSyncElements from 'kolibri.coreVue.mixins.commonSyncElements';
import pickBy from 'lodash/pickBy';
import { UnreachableConnectionStatuses } from './constants';
import useDeviceDeletion from './useDeviceDeletion.js';
import {
Expand All @@ -163,19 +164,22 @@
deviceFilters.push(useDeviceChannelFilter({ id: props.filterByChannelId }));
}

if (
props.filterByFacilityId !== null ||
props.filterByFacilityCanSignUp !== null ||
props.filterByOnMyOwnFacility !== null
) {
const pickNotNull = v => v !== null;
// Either we build a facility filter or an empty object.
// Passing the empty object to useDeviceFacilityFilter is asking "are there ANY facilities?"
const facilityFilter = pickBy(
{
id: props.filterByFacilityId,
learner_can_sign_up: props.filterByFacilityCanSignUp,
on_my_own_setup: props.filterByOnMyOwnFacility,
},
pickNotNull,
);

// If we're filtering a particular facility
if (Object.keys(facilityFilter).length > 0 || props.filterByHasFacilities) {
apiParams.subset_of_users_device = false;
deviceFilters.push(
useDeviceFacilityFilter({
id: props.filterByFacilityId,
learner_can_sign_up: props.filterByFacilityCanSignUp,
on_my_own_setup: props.filterByOnMyOwnFacility,
}),
);
deviceFilters.push(useDeviceFacilityFilter(facilityFilter));
}

if (props.filterLODAvailable) {
Expand Down Expand Up @@ -254,6 +258,12 @@
type: Boolean,
default: null,
},
// In the setup wizard, to exclude devices that do not have a facility
// eslint-disable-next-line kolibri/vue-no-unused-properties
filterByHasFacilities: {
type: Boolean,
default: null,
},
// If an ID is provided, that device's radio button will be automatically selected
selectedId: {
type: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export function fetchDevices(params = {}) {
* @typedef {Object} FacilityFilter
* @property {string} [id]
* @property {boolean} [learner_can_sign_up]
* @property {boolean} [on_my_own_setup]
*/

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
:filterLODAvailable="filterLODAvailable"
:filterByFacilityCanSignUp="filterByFacilityCanSignUp"
:filterByOnMyOwnFacility="filterByOnMyOwnFacility"
:filterByHasFacilities="filterByHasFacilities"
:selectedId="addedAddressId"
:formDisabled="$attrs.selectAddressDisabled"
@click_add_address="goToAddAddress"
Expand Down Expand Up @@ -56,6 +57,10 @@
type: Boolean,
default: null,
},
filterByHasFacilities: {
type: Boolean,
default: null,
},
// When looking for facilities to import in the setup wizard
filterByOnMyOwnFacility: {
type: Boolean,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,10 +195,6 @@ export function useDeviceFacilityFilter({
filters.on_my_own_setup = on_my_own_setup;
}

if (Object.keys(filters).length === 0) {
return () => Promise.resolve(true);
}

return useAsyncDeviceFilter(function deviceFacilityFilter(device) {
return deviceHasMatchingFacility(device, filters);
});
Expand Down
8 changes: 4 additions & 4 deletions kolibri/core/auth/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,7 @@ def post(self, request):

class FacilityUsernameViewSet(ReadOnlyValuesViewset):
filter_backends = (DjangoFilterBackend, filters.SearchFilter)
filter_fields = ("facility",)
filterset_fields = ("facility",)
search_fields = ("^username",)

values = ("username",)
Expand Down Expand Up @@ -527,7 +527,7 @@ class MembershipViewSet(BulkDeleteMixin, BulkCreateMixin, viewsets.ModelViewSet)
queryset = Membership.objects.all()
serializer_class = MembershipSerializer
filterset_class = MembershipFilter
filter_fields = ["user", "collection", "user_ids"]
filterset_fields = ["user", "collection", "user_ids"]


class RoleFilter(FilterSet):
Expand All @@ -547,7 +547,7 @@ class RoleViewSet(BulkDeleteMixin, BulkCreateMixin, viewsets.ModelViewSet):
queryset = Role.objects.all()
serializer_class = RoleSerializer
filterset_class = RoleFilter
filter_fields = ["user", "collection", "kind", "user_ids"]
filterset_fields = ["user", "collection", "kind", "user_ids"]


dataset_keys = [
Expand Down Expand Up @@ -771,7 +771,7 @@ class LearnerGroupViewSet(ValuesViewset):
queryset = LearnerGroup.objects.all()
serializer_class = LearnerGroupSerializer

filter_fields = ("parent",)
filterset_fields = ("parent",)

values = ("id", "name", "parent", "user_ids")

Expand Down
14 changes: 12 additions & 2 deletions kolibri/core/auth/test/test_bulk_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,21 @@
@override_settings(LANGUAGE_CODE="en")
class UserExportTestCase(TestCase):
@classmethod
def setUpTestData(cls):
def setUpClass(cls):
# Import inside the settings decorator to ensure that the locale is controlled
# Do this inside setUpClass rather than setUpTestData as setUpTestData requires
# assigned objects to be compatible with copy.deepcopy - we do not need to copy
# the management commands as they will not be modified by the tests, unlike the
# models.
from ..management.commands import bulkexportusers as b

cls.b = b
# Run it before setUpClass super call, because this will then execute setUpTestData
# which requires cls.b to be defined.
super().setUpClass()

@classmethod
def setUpTestData(cls):
cls.data = create_dummy_facility_data(
classroom_count=CLASSROOMS, learnergroup_count=1
)
Expand All @@ -28,7 +38,7 @@ def setUpTestData(cls):
_, cls.filepath = tempfile.mkstemp(suffix=".csv")

cls.csv_rows = []
for row in b.csv_file_generator(cls.facility, cls.filepath, True):
for row in cls.b.csv_file_generator(cls.facility, cls.filepath, True):
cls.csv_rows.append(row)

def test_not_specified(self):
Expand Down
2 changes: 1 addition & 1 deletion kolibri/core/bookmarks/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,4 @@ class BookmarksViewSet(ValuesViewset):
KolibriAuthPermissionsFilter,
DjangoFilterBackend,
)
filter_fields = ("contentnode_id",)
filterset_fields = ("contentnode_id",)
3 changes: 0 additions & 3 deletions kolibri/core/content/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@
import os


default_app_config = "kolibri.core.content.apps.KolibriContentConfig"


# Do this to prevent import of broken Windows filetype registry that makes guesstype not work.
# https://www.thecodingforums.com/threads/mimetypes-guess_type-broken-in-windows-on-py2-7-and-python-3-x.952693/
mimetypes.init(
Expand Down
30 changes: 29 additions & 1 deletion kolibri/core/content/api.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import hashlib
import logging
import re
from base64 import urlsafe_b64decode
from collections import OrderedDict
from functools import reduce
from random import sample
Expand All @@ -14,11 +15,14 @@
from django.db.models import Subquery
from django.db.models.aggregates import Count
from django.http import Http404
from django.http import HttpResponse
from django.urls import reverse
from django.utils.cache import add_never_cache_headers
from django.utils.decorators import method_decorator
from django.utils.encoding import force_bytes
from django.utils.encoding import iri_to_uri
from django.utils.translation import gettext as _
from django.views import View
from django.views.decorators.cache import cache_page
from django.views.decorators.cache import never_cache
from django.views.decorators.http import etag
Expand Down Expand Up @@ -291,6 +295,18 @@ def filter_available(self, queryset, name, value):
return queryset.filter(root__available=value)


class ChannelThumbnailView(View):
def get(self, request, channel_id):
channel = get_object_or_404(models.ChannelMetadata, id=channel_id)
try:
header, b_64_thumbnail = channel.thumbnail.split(",", 1)
mimetype = header.split(":")[1].split(";")[0]
except ValueError:
raise Http404("No thumbnail available")
thumbnail = urlsafe_b64decode(b_64_thumbnail)
return HttpResponse(thumbnail, content_type=mimetype)


class BaseChannelMetadataMixin(object):
filter_backends = (DjangoFilterBackend,)
filterset_class = ChannelMetadataFilter
Expand Down Expand Up @@ -373,9 +389,21 @@ def filter_options(self, request, **kwargs):
return Response(data)


def _create_channel_thumbnail_url(item):
return (
reverse("kolibri:core:channel-thumbnail", args=[item["id"]])
if item["thumbnail"]
else ""
)


@method_decorator(remote_metadata_cache, name="dispatch")
class ChannelMetadataViewSet(BaseChannelMetadataMixin, RemoteViewSet):
pass
field_map = {
"thumbnail": _create_channel_thumbnail_url,
}

field_map.update(BaseChannelMetadataMixin.field_map)


MODALITIES = set(["QUIZ"])
Expand Down
11 changes: 10 additions & 1 deletion kolibri/core/content/api_urls.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from django.urls import include
from django.urls import path
from django.urls import re_path
from rest_framework import routers

from .api import ChannelMetadataViewSet
from .api import ChannelThumbnailView
from .api import ContentNodeBookmarksViewset
from .api import ContentNodeGranularViewset
from .api import ContentNodeProgressViewset
Expand Down Expand Up @@ -46,4 +48,11 @@
)
router.register(r"remotechannel", RemoteChannelViewSet, basename="remotechannel")

urlpatterns = [re_path(r"^", include(router.urls))]
urlpatterns = [
path(
"channel-thumbnail/<channel_id>/",
ChannelThumbnailView.as_view(),
name="channel-thumbnail",
),
re_path(r"^", include(router.urls)),
]
41 changes: 41 additions & 0 deletions kolibri/core/content/test/test_content_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import time
import unittest
import uuid
from base64 import urlsafe_b64decode

import mock
import requests
Expand Down Expand Up @@ -1998,3 +1999,43 @@ class PrefixedProxyContentMetadataTestCase(ProxyContentMetadataTestCase):
@property
def baseurl(self):
return self.live_server_url + "/test/"


class ChannelThumbnailViewTestCase(APITestCase):
def setUp(self):
self.content_node = content.ContentNode.objects.create(
pk="6a406ac66b224106aa2e93f73a94333d",
channel_id="f8ec4a5d14cd4716890999da596032d2",
content_id="ded4a083e75f4689b386fd2b706e792a",
)
self.thumbnail = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABjElEQVR42mNk"
self.channel_metadata = content.ChannelMetadata.objects.create(
id="63acff41781543828861ade41dbdd7ff",
name="no exercise channel metadata",
thumbnail=self.thumbnail,
root=self.content_node,
)

def test_channel_thumbnail_view(self):
response = self.client.get(
reverse("kolibri:core:channel-thumbnail", args=[self.channel_metadata.id])
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response["Content-Type"], "image/png")
self.assertEqual(
response.content, urlsafe_b64decode(self.thumbnail.split(",")[1])
)

def test_channel_thumbnail_view_not_found(self):
response = self.client.get(
reverse("kolibri:core:channel-thumbnail", args=["deadpool"])
)
self.assertEqual(response.status_code, 404)

def test_channel_thumbnail_view_no_thumbnail(self):
self.channel_metadata.thumbnail = ""
self.channel_metadata.save()
response = self.client.get(
reverse("kolibri:core:channel-thumbnail", args=[self.channel_metadata.id])
)
self.assertEqual(response.status_code, 404)
3 changes: 2 additions & 1 deletion kolibri/core/content/utils/content_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,8 @@ def _get_channel_data(self, channel_id, channel_version):
return channel_data

def _set_channel_data(self, channel_data):
assert isinstance(channel_data, ContentManifestChannelData)
if not isinstance(channel_data, ContentManifestChannelData):
raise TypeError("channel_data must be a ContentManifestChannelData")
self._channels_dict.setdefault(channel_data.channel_id, {})[
channel_data.channel_version
] = channel_data
Expand Down
1 change: 0 additions & 1 deletion kolibri/core/device/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@
default_app_config = "kolibri.core.device.apps.KolibriDeviceAppConfig"
2 changes: 1 addition & 1 deletion kolibri/core/discovery/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class NetworkLocationViewSet(viewsets.ModelViewSet):
serializer_class = NetworkLocationSerializer
queryset = NetworkLocation.objects.exclude(location_type=LocationTypes.Reserved)
filter_backends = [DjangoFilterBackend]
filter_fields = [
filterset_fields = [
"id",
"subset_of_users_device",
"instance_id",
Expand Down
10 changes: 5 additions & 5 deletions kolibri/core/exams/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,14 @@ def filter_querysets(self, exam_queryset, draft_queryset):
exam_queryset = instantiated_backend.filter_queryset(
self.request, exam_queryset, self
)
# Do some special handling if the backend has a get_filter_class method
# this is required, as the get_filter_class makes an assertion based on
# Do some special handling if the backend has a get_filterset_class method
# this is required, as the get_filterset_class makes an assertion based on
# the model Meta of the FilterSet - which is set to Exam, so this will
# fail if we try to use the ExamFilter on the DraftExam queryset
# This is a workaround to allow the DraftExam queryset to be filtered
if hasattr(instantiated_backend, "get_filter_class"):
if hasattr(instantiated_backend, "get_filterset_class"):
# First get the filter class using the exam queryset
filter_class = instantiated_backend.get_filter_class(
filter_class = instantiated_backend.get_filterset_class(
self, exam_queryset
)
# If the filter class is not None, then we can use it to filter the draft_queryset
Expand All @@ -136,7 +136,7 @@ def filter_querysets(self, exam_queryset, draft_queryset):
request=self.request,
).qs
else:
# If the backend doesn't have a get_filter_class method, then we can just
# If the backend doesn't have a get_filterset_class method, then we can just
# filter the draft_queryset as normal
draft_queryset = instantiated_backend.filter_queryset(
self.request, draft_queryset, self
Expand Down
2 changes: 1 addition & 1 deletion kolibri/core/lessons/viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def _map_lesson_classroom(item):
class LessonViewset(ValuesViewset):
serializer_class = LessonSerializer
filter_backends = (KolibriAuthPermissionsFilter, DjangoFilterBackend)
filter_fields = ("collection", "id")
filterset_fields = ("collection", "id")
permission_classes = (LessonPermissions,)
queryset = Lesson.objects.all().order_by("-date_created")

Expand Down
1 change: 0 additions & 1 deletion kolibri/core/logger/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@
default_app_config = "kolibri.core.logger.apps.KolibriLoggerConfig"
2 changes: 1 addition & 1 deletion kolibri/core/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def allow_bulk_destroy(self, qs, filtered):
"""
# Only let a bulk destroy if the queryset is being filtered by a valid filter_field parameter
return any(
key in self.filter_fields for key in self.request.query_params.keys()
key in self.filterset_fields for key in self.request.query_params.keys()
)

def bulk_destroy(self, request, *args, **kwargs):
Expand Down
Loading