Skip to content

Commit

Permalink
Merge pull request #11426 from marcellamaki/intermediate-lesson-loadi…
Browse files Browse the repository at this point in the history
…ng-state

Update MissingResourceAlert to report sync status
  • Loading branch information
rtibbles authored Oct 23, 2023
2 parents bcc470c + bd1b333 commit 6b3d577
Show file tree
Hide file tree
Showing 10 changed files with 202 additions and 21 deletions.
3 changes: 3 additions & 0 deletions kolibri/core/assets/src/composables/useUserSyncStatus.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const deviceStatus = ref(null);
const deviceStatusSentiment = ref(null);
const hasDownloads = ref(false);
const lastDownloadRemoved = ref(null);
const syncDownloadsInProgress = ref(false);
const timeoutInterval = computed(() => {
return get(status) === SyncStatus.QUEUED ? 1000 : 10000;
});
Expand Down Expand Up @@ -51,6 +52,7 @@ export function pollUserSyncStatusTask() {
lastDownloadRemoved.value = syncData[0].last_download_removed
? new Date(syncData[0].last_download_removed)
: null;
syncDownloadsInProgress.value = syncData[0].sync_downloads_in_progress;
} else {
status.value = SyncStatus.NOT_CONNECTED;
}
Expand Down Expand Up @@ -83,5 +85,6 @@ export default function useUserSyncStatus() {
deviceStatusSentiment,
hasDownloads,
lastDownloadRemoved,
syncDownloadsInProgress,
};
}
2 changes: 2 additions & 0 deletions kolibri/core/assets/src/core-app/apiSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ import * as sync from '../views/sync/syncComponentSet';
import PageRoot from '../views/PageRoot';
import NotificationsRoot from '../views/NotificationsRoot';
import useMinimumKolibriVersion from '../composables/useMinimumKolibriVersion';
import useUserSyncStatus from '../composables/useUserSyncStatus';
import useUser from '../composables/useUser';

// webpack optimization
Expand Down Expand Up @@ -232,6 +233,7 @@ export default {
useKShow,
useMinimumKolibriVersion,
useUser,
useUserSyncStatus,
},
},
resources,
Expand Down
1 change: 1 addition & 0 deletions kolibri/core/content/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1368,6 +1368,7 @@ def annotate_queryset(self, queryset):
source_id=OuterRef("source_id"),
contentnode_id=OuterRef("contentnode_id"),
requested_at__gte=OuterRef("requested_at"),
reason=OuterRef("reason"),
).exclude(status=ContentRequestStatus.Failed)
)
).filter(has_removal=False)
Expand Down
31 changes: 30 additions & 1 deletion kolibri/core/device/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
from kolibri.core.content.models import ContentDownloadRequest
from kolibri.core.content.models import ContentRemovalRequest
from kolibri.core.content.models import ContentRequestReason
from kolibri.core.content.models import ContentRequestStatus
from kolibri.core.content.permissions import CanManageContent
from kolibri.core.content.utils.channels import get_mounted_drive_by_id
from kolibri.core.content.utils.channels import get_mounted_drives_with_channel_info
Expand Down Expand Up @@ -302,6 +303,7 @@ class UserSyncStatusViewSet(ReadOnlyValuesViewset):
"user",
"has_downloads",
"last_download_removed",
"sync_downloads_in_progress",
)

field_map = {
Expand Down Expand Up @@ -338,14 +340,40 @@ def annotate_queryset(self, queryset):
instance_id=OuterRef("sync_session__client_instance_id"),
)

# Use the same condition used in the ContentRequest API endpoint
# otherwise, this will signal that users have downloads
# but when they navigate to the Downloads page, they may not see
# any downloads.
downloads_without_removals_queryset = ContentDownloadRequest.objects.annotate(
has_removal=Exists(
ContentRemovalRequest.objects.filter(
source_model=OuterRef("source_model"),
source_id=OuterRef("source_id"),
contentnode_id=OuterRef("contentnode_id"),
requested_at__gte=OuterRef("requested_at"),
reason=OuterRef("reason"),
).exclude(status=ContentRequestStatus.Failed)
)
).filter(has_removal=False)

has_download = Exists(
ContentDownloadRequest.objects.filter(
downloads_without_removals_queryset.filter(
source_id=OuterRef("user_id"),
source_model=FacilityUser.morango_model_name,
reason=ContentRequestReason.UserInitiated,
)
)

has_in_progress_sync_initiated_download = Exists(
downloads_without_removals_queryset.filter(
source_id=OuterRef("user_id"),
source_model=FacilityUser.morango_model_name,
reason=ContentRequestReason.SyncInitiated,
).exclude(
status__in=[ContentRequestStatus.Failed, ContentRequestStatus.Completed]
)
)

last_download_removal = Subquery(
ContentRemovalRequest.objects.filter(
source_id=OuterRef("user_id"),
Expand All @@ -366,6 +394,7 @@ def annotate_queryset(self, queryset):
),
has_downloads=has_download,
last_download_removed=last_download_removal,
sync_downloads_in_progress=has_in_progress_sync_initiated_download,
)
return queryset

Expand Down
60 changes: 58 additions & 2 deletions kolibri/core/device/test/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
from kolibri.core.auth.test.test_api import FacilityFactory
from kolibri.core.auth.test.test_api import FacilityUserFactory
from kolibri.core.content.models import ContentDownloadRequest
from kolibri.core.content.models import ContentRemovalRequest
from kolibri.core.content.models import ContentRequestReason
from kolibri.core.device.models import DevicePermissions
from kolibri.core.device.models import DeviceSettings
from kolibri.core.device.models import DeviceStatus
Expand Down Expand Up @@ -929,10 +931,64 @@ def test_downloads_queryset(self):
device_status = self._create_device_status("oopsie", StatusSentiment.Neutral)
self.syncsession1.client_instance_id = device_status.instance_id
self.syncsession1.save()
response = self.client.get(reverse("kolibri:core:usersyncstatus-list"))
content_request.save()
response = self.client.get(reverse("kolibri:core:usersyncstatus-list"))
self.assertEqual(len(response.data), 1)
self.assertEqual(response.data[0]["user"], self.user1.id)
self.assertEqual(response.data[0]["status"], user_sync_statuses.RECENTLY_SYNCED)
self.assertFalse(response.data[0]["has_downloads"])
self.assertTrue(response.data[0]["has_downloads"])
self.assertIsNone(response.data[0]["last_download_removed"])

def test_downloads_queryset__content_request_removed(self):
self.client.login(
username=self.user1.username,
password=DUMMY_PASSWORD,
facility=self.facility,
)
self._create_transfer_session(
active=False,
)
content_request = ContentDownloadRequest.build_for_user(self.user1)
content_request.contentnode_id = uuid.uuid4().hex
content_request.save()
content_removal_request = ContentRemovalRequest.build_for_user(self.user1)
content_removal_request.contentnode_id = content_request.contentnode_id
content_removal_request.save()
response = self.client.get(reverse("kolibri:core:usersyncstatus-list"))
self.assertFalse(response.data[0]["has_downloads"])

def test_downloads_queryset__sync_downloads_in_progress(self):
self.client.login(
username=self.user1.username,
password=DUMMY_PASSWORD,
facility=self.facility,
)
self._create_transfer_session(
active=False,
)
content_request = ContentDownloadRequest.build_for_user(self.user1)
content_request.contentnode_id = uuid.uuid4().hex
content_request.reason = ContentRequestReason.SyncInitiated
content_request.save()
response = self.client.get(reverse("kolibri:core:usersyncstatus-list"))
self.assertTrue(response.data[0]["sync_downloads_in_progress"])

def test_downloads_queryset__sync_downloads_in_progress_removed(self):
self.client.login(
username=self.user1.username,
password=DUMMY_PASSWORD,
facility=self.facility,
)
self._create_transfer_session(
active=False,
)
content_request = ContentDownloadRequest.build_for_user(self.user1)
content_request.contentnode_id = uuid.uuid4().hex
content_request.reason = ContentRequestReason.SyncInitiated
content_request.save()
content_removal_request = ContentRemovalRequest.build_for_user(self.user1)
content_removal_request.contentnode_id = content_request.contentnode_id
content_removal_request.reason = ContentRequestReason.SyncInitiated
content_removal_request.save()
response = self.client.get(reverse("kolibri:core:usersyncstatus-list"))
self.assertFalse(response.data[0]["sync_downloads_in_progress"])
6 changes: 3 additions & 3 deletions kolibri/plugins/learn/assets/src/views/ExamPage/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
:answerState="currentAttempt.answer"
@interaction="saveAnswer"
/>
<MissingResourceAlert v-else :multiple="false" />
<ResourceSyncingUiAlert v-else :multiple="false" />
</KPageContainer>

<BottomAppBar :dir="bottomBarLayoutDirection" :maxWidth="null">
Expand Down Expand Up @@ -186,7 +186,7 @@
import TimeDuration from 'kolibri.coreVue.components.TimeDuration';
import commonCoreStrings from 'kolibri.coreVue.mixins.commonCoreStrings';
import ImmersivePage from 'kolibri.coreVue.components.ImmersivePage';
import MissingResourceAlert from 'kolibri-common/components/MissingResourceAlert';
import ResourceSyncingUiAlert from '../ResourceSyncingUiAlert';
import useProgressTracking from '../../composables/useProgressTracking';
import { PageNames, ClassesPageNames } from '../../constants';
import { LearnerClassroomResource } from '../../apiResources';
Expand All @@ -208,7 +208,7 @@
TimeDuration,
SuggestedTime,
ImmersivePage,
MissingResourceAlert,
ResourceSyncingUiAlert,
},
mixins: [responsiveWindowMixin, commonCoreStrings],
setup() {
Expand Down
10 changes: 7 additions & 3 deletions kolibri/plugins/learn/assets/src/views/HomePage/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

<LearnAppBarPage :appBarTitle="learnString('learnLabel')">
<div v-if="!loading" id="main" role="main">
<MissingResourceAlert v-if="missingResources" />
<ResourceSyncingUiAlert
v-if="missingResources"
@syncComplete="hydrateHomePage"
/>
<YourClasses
v-if="displayClasses"
class="section"
Expand Down Expand Up @@ -58,8 +61,8 @@
import { get } from '@vueuse/core';
import client from 'kolibri.client';
import urls from 'kolibri.urls';
import MissingResourceAlert from 'kolibri-common/components/MissingResourceAlert';
import useUser from 'kolibri.coreVue.composables.useUser';
import ResourceSyncingUiAlert from '../ResourceSyncingUiAlert';
import useChannels from '../../composables/useChannels';
import useDeviceSettings from '../../composables/useDeviceSettings';
import useLearnerResources, {
Expand Down Expand Up @@ -91,7 +94,7 @@
ContinueLearning,
ExploreChannels,
LearnAppBarPage,
MissingResourceAlert,
ResourceSyncingUiAlert,
},
mixins: [commonLearnStrings],
setup() {
Expand Down Expand Up @@ -206,6 +209,7 @@
displayExploreChannels,
displayClasses,
missingResources,
hydrateHomePage,
};
},
props: {
Expand Down
81 changes: 81 additions & 0 deletions kolibri/plugins/learn/assets/src/views/ResourceSyncingUiAlert.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<template>

<MissingResourceAlert
:multiple="multiple"
>
<template v-if="isLearnerOnlyImport && isSyncing" #syncAlert>
{{ syncMessage }}
</template>
</MissingResourceAlert>

</template>


<script>
import { get } from '@vueuse/core';
import { computed, watch } from 'kolibri.lib.vueCompositionApi';
import MissingResourceAlert from 'kolibri-common/components/MissingResourceAlert.vue';
import useUserSyncStatus from 'kolibri.coreVue.composables.useUserSyncStatus';
import { SyncStatus } from 'kolibri.coreVue.vuex.constants';
import { createTranslator } from 'kolibri.utils.i18n';
import useUser from 'kolibri.coreVue.composables.useUser';
const syncStatusDescriptionStrings = createTranslator('SyncStatusDescription', {
syncingDescription: {
message: 'The device is currently syncing.',
context: 'Description of the device syncing status.',
},
queuedDescription: {
message: 'The device is waiting to sync.',
context: 'Description of the device syncing status',
},
});
export default {
name: 'ResourceSyncingUiAlert',
components: {
MissingResourceAlert,
},
setup(props, { emit }) {
const { status, syncDownloadsInProgress } = useUserSyncStatus();
const { isLearnerOnlyImport } = useUser();
const isSyncing = computed(() => {
return (
get(status) === SyncStatus.QUEUED ||
get(status) === SyncStatus.SYNCING ||
get(syncDownloadsInProgress)
);
});
watch(isSyncing, (newVal, oldVal) => {
if (newVal === false && oldVal === true) {
emit('syncComplete');
}
});
return {
status,
syncDownloadsInProgress,
isLearnerOnlyImport,
isSyncing,
};
},
props: {
multiple: {
type: Boolean,
default: true,
},
},
computed: {
syncMessage() {
/* eslint-disable kolibri/vue-no-undefined-string-uses */
return this.status == SyncStatus.QUEUED
? syncStatusDescriptionStrings.$tr('queuedDescription')
: syncStatusDescriptionStrings.$tr('syncingDescription');
/* eslint-enable */
},
},
};
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
{{ currentLesson.description }}
</p>
</div>
<MissingResourceAlert v-if="lessonResources.length > contentNodes.length" />
<ResourceSyncingUiAlert v-if="lessonResources.length > contentNodes.length" />
</section>

<section v-if="lessonHasResources" class="content-cards">
Expand Down Expand Up @@ -57,7 +57,7 @@
import ProgressIcon from 'kolibri.coreVue.components.ProgressIcon';
import ContentIcon from 'kolibri.coreVue.components.ContentIcon';
import commonCoreStrings from 'kolibri.coreVue.mixins.commonCoreStrings';
import MissingResourceAlert from 'kolibri-common/components/MissingResourceAlert';
import ResourceSyncingUiAlert from '../ResourceSyncingUiAlert';
import useContentLink from '../../composables/useContentLink';
import useContentNodeProgress from '../../composables/useContentNodeProgress';
import { PageNames, ClassesPageNames } from '../../constants';
Expand All @@ -78,7 +78,7 @@
ContentIcon,
ProgressIcon,
LearnAppBarPage,
MissingResourceAlert,
ResourceSyncingUiAlert,
},
mixins: [commonCoreStrings, commonLearnStrings, responsiveWindowMixin],
setup() {
Expand Down
23 changes: 14 additions & 9 deletions packages/kolibri-common/components/MissingResourceAlert.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,21 @@
type="warning"
:style="{ marginBottom: 0, marginTop: '8px' }"
>
<span>
{{ coreString(
multiple ? 'someResourcesMissingOrNotSupported' :
'resourceNotFoundOnDevice'
) }}
<span v-if="$slots.syncAlert">
<slot name="syncAlert"></slot>
</span>
<span v-else>
<span>
{{ coreString(
multiple ? 'someResourcesMissingOrNotSupported' :
'resourceNotFoundOnDevice'
) }}
&nbsp;
<KButton appearance="basic-link" @click="open = true">
{{ $tr('learnMore') }}
</KButton>
</span>
</span>
&nbsp;
<KButton appearance="basic-link" @click="open = true">
{{ $tr('learnMore') }}
</KButton>
</UiAlert>
<KModal
v-if="open"
Expand Down

0 comments on commit 6b3d577

Please sign in to comment.