diff --git a/kolibri/core/content/api.py b/kolibri/core/content/api.py index 0646b7e3ea6..29d84209222 100644 --- a/kolibri/core/content/api.py +++ b/kolibri/core/content/api.py @@ -50,6 +50,7 @@ from kolibri.core.content import serializers 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.tasks import automatic_resource_import @@ -1349,7 +1350,9 @@ class ContentRequestViewset(ReadOnlyValuesViewset, CreateModelMixin): ) def get_queryset(self): - return ContentDownloadRequest.objects.filter(source_id=self.request.user.id) + return ContentDownloadRequest.objects.filter( + source_id=self.request.user.id, reason=ContentRequestReason.UserInitiated + ) def annotate_queryset(self, queryset): return queryset.annotate( @@ -1373,11 +1376,12 @@ def delete(self, request, pk=None): ) existing_download_request = ( - existing_deletion_request - ) = ContentRemovalRequest.objects.filter( - id=request_id, - source_id=request.user.id, - ).first() + self.get_queryset() + .filter( + id=request_id, + ) + .first() + ) if existing_download_request is None: return Response( @@ -1386,7 +1390,8 @@ def delete(self, request, pk=None): ) existing_deletion_request = ContentRemovalRequest.objects.filter( - id=request_id, + contentnode_id=existing_download_request.contentnode_id, + reason=existing_download_request.reason, source_id=request.user.id, ).first() diff --git a/kolibri/plugins/learn/assets/src/composables/useDownloadRequests.js b/kolibri/plugins/learn/assets/src/composables/useDownloadRequests.js index 032bf18e649..03924ed5b93 100644 --- a/kolibri/plugins/learn/assets/src/composables/useDownloadRequests.js +++ b/kolibri/plugins/learn/assets/src/composables/useDownloadRequests.js @@ -4,12 +4,12 @@ import { getCurrentInstance, reactive, ref } from 'kolibri.lib.vueCompositionApi'; import { ContentRequestResource } from 'kolibri.resources'; -import Vue from 'kolibri.lib.vue'; import { createTranslator } from 'kolibri.utils.i18n'; import { get, set } from '@vueuse/core'; import redirectBrowser from 'kolibri.utils.redirectBrowser'; import urls from 'kolibri.urls'; import client from 'kolibri.client'; +import Vue from 'kolibri.lib.vue'; import useDevices from './useDevices'; const downloadRequestsTranslator = createTranslator('DownloadRequests', { @@ -40,7 +40,9 @@ export default function useDownloadRequests(store) { function fetchUserDownloadRequests(params) { return ContentRequestResource.list(params) .then(downloadRequests => { - set(downloadRequestMap, downloadRequests); + for (const obj of downloadRequests) { + set(downloadRequestMap, obj.id, obj); + } set(loading, false); }) .then(store.dispatch('notLoading')); @@ -100,17 +102,8 @@ export default function useDownloadRequests(store) { ContentRequestResource.deleteModel({ id: content.id, contentnode_id: content.contentnode_id, - }).then(Vue.delete(downloadRequestMap, content.id)); - return Promise.resolve(); - } - - function removeDownloadsRequest(contentList) { - contentList.forEach(content => { - ContentRequestResource.deleteModel({ - id: content.id, - contentnode_id: content.contentnode_id, - }).then(Vue.delete(downloadRequestMap, content.id)); }); + Vue.delete(downloadRequestMap, content.id); return Promise.resolve(); } @@ -138,7 +131,6 @@ export default function useDownloadRequests(store) { addDownloadRequest, loading, removeDownloadRequest, - removeDownloadsRequest, downloadRequestsTranslator, isDownloadingByLearner, isDownloadedByLearner, diff --git a/kolibri/plugins/learn/assets/src/my_downloads/views/DownloadsList/index.vue b/kolibri/plugins/learn/assets/src/my_downloads/views/DownloadsList/index.vue index 8da0c060eb8..1211e6274a8 100644 --- a/kolibri/plugins/learn/assets/src/my_downloads/views/DownloadsList/index.vue +++ b/kolibri/plugins/learn/assets/src/my_downloads/views/DownloadsList/index.vue @@ -6,7 +6,7 @@ v-model="currentPage" :itemsPerPage="itemsPerPage" :totalPageNumber="totalPageNumber" - :numFilteredItems="totalDownloads" + :numFilteredItems="downloads.length" > -

+

{{ coreString('noResourcesDownloaded') }}

@@ -111,8 +111,11 @@ import CoreTable from 'kolibri.coreVue.components.CoreTable'; import commonCoreStrings from 'kolibri.coreVue.mixins.commonCoreStrings'; import PaginatedListContainerWithBackend from 'kolibri-common/components/PaginatedListContainerWithBackend'; + import { computed, getCurrentInstance, watch, ref } from 'kolibri.lib.vueCompositionApi'; + import { get, set } from '@vueuse/core'; import useContentLink from '../../../composables/useContentLink'; import useLearningActivities from '../../../composables/useLearningActivities'; + import useDownloadRequests from '../../../composables/useDownloadRequests'; import SelectionBottomBar from './SelectionBottomBar.vue'; import ConfirmationDeleteModal from './ConfirmationDeleteModal.vue'; @@ -128,25 +131,70 @@ setup() { const { genExternalContentURLBackLinkCurrentPage } = useContentLink(); const { getLearningActivityIcon } = useLearningActivities(); - + const { downloadRequestMap, availableSpace } = useDownloadRequests(); + const totalPageNumber = ref(0); + const store = getCurrentInstance().proxy.$store; + const query = computed(() => get(route).query); + const route = computed(() => store.state.route); + const sort = computed(() => query.value.sort); + const pageSizeNumber = computed(() => Number(query.value.page_size || 25)); + const activityType = computed(() => query.value.activity || 'all'); + const downloads = ref([]); + const sortedFilteredDownloads = () => { + let downloadsToDisplay = []; + if (downloadRequestMap) { + for (const [, value] of Object.entries(downloadRequestMap)) { + downloadsToDisplay.push(value); + } + if (activityType) { + if (activityType.value !== 'all') { + downloadsToDisplay = downloadsToDisplay.filter(download => + download.metadata.learning_activities.includes(activityType.value) + ); + } + } + if (sort) { + switch (sort.value) { + case 'newest': + downloadsToDisplay.sort( + (a, b) => new Date(b.requested_at) - new Date(a.requested_at) + ); + break; + case 'oldest': + downloadsToDisplay.sort( + (a, b) => new Date(a.requested_at) - new Date(b.requested_at) + ); + break; + case 'smallest': + downloadsToDisplay.sort((a, b) => a.metadata.file_size - b.metadata.file_size); + break; + case 'largest': + downloadsToDisplay.sort((a, b) => b.metadata.file_size - a.metadata.file_size); + break; + default: + // If no valid sort option provided, return unsorted array + break; + } + } + } + set(totalPageNumber, Math.ceil(downloadsToDisplay.length / pageSizeNumber.value)); + set(downloads, downloadsToDisplay); + }; + sortedFilteredDownloads(); + watch(route, () => { + sortedFilteredDownloads(); + }); return { + downloadRequestMap, + downloads, getLearningActivityIcon, + sortedFilteredDownloads, + totalPageNumber, + availableSpace, genExternalContentURLBackLinkCurrentPage, }; }, props: { - downloads: { - type: Array, - required: true, - }, - totalDownloads: { - type: Number, - required: true, - }, - totalPageNumber: { - type: Number, - required: true, - }, loading: { type: Boolean, required: false, @@ -198,26 +246,24 @@ areAllSelected() { return Object.keys(this.downloads).every(id => this.selectedDownloads.includes(id)); }, + areAnyAvailable() { + if (this.downloads && this.downloads.length > 0) { + return this.downloads.filter(download => download.status === 'COMPLETED').length > 0; + } + return false; + }, }, watch: { - selectedDownloads(newVal, oldVal) { - if (newVal.length === 0) { - this.selectedDownloadsSize = 0; - return; - } - const addedDownloads = newVal.filter(id => !oldVal.includes(id)); - const removedDownloads = oldVal.filter(id => !newVal.includes(id)); - const addedDownloadsSize = addedDownloads.reduce( - (acc, id) => acc + (this.downloads[id] ? this.downloads[id].metadata.file_size : 0), + selectedDownloads(newVal) { + this.selectedDownloadsSize = newVal.reduce( + (acc, object) => acc + object.metadata.file_size, 0 ); - const removedDownloadsSize = removedDownloads.reduce( - (acc, id) => acc + (this.downloads[id].metadata.file_size ? this.downloads[id] : 0), - 0 - ); - this.selectedDownloadsSize += addedDownloadsSize - removedDownloadsSize; }, }, + mounted() { + this.areAnyAvailable; + }, methods: { nonCompleteStatus(download) { return download.status !== 'COMPLETED'; @@ -225,11 +271,11 @@ selectAll() { if (this.areAllSelected) { this.selectedDownloads = this.selectedDownloads.filter( - resourceId => !Object.keys(this.downloads).includes(resourceId) + download => !this.paginatedDownloads.includes(download) ); } else { this.selectedDownloads = this.selectedDownloads.concat( - Object.keys(this.downloads).filter(id => !this.selectedDownloads.includes(id)) + this.paginatedDownloads.filter(download => !this.selectedDownloads.includes(download)) ); } }, @@ -245,13 +291,12 @@ }, removeResource(download) { this.$emit('removeResources', [download]); + this.sortedFilteredDownloads(); }, removeResources() { this.$emit('removeResources', this.resourcesToDelete); - this.selectedDownloads = this.selectedDownloads.filter( - resourceId => !this.resourcesToDelete.includes(resourceId) - ); this.resourcesToDelete = []; + this.sortedFilteredDownloads(); }, getIcon(activities) { return this.getLearningActivityIcon(activities); @@ -278,7 +323,7 @@ message = this.coreString('waitingToDownload'); break; case 'IN_PROGRESS': - message = this.coreString('inProgress'); + message = this.coreString('inProgressLabel'); break; case 'COMPLETED' && this.now - download.requested_at < 10000: message = this.coreString('justNow'); diff --git a/kolibri/plugins/learn/assets/src/my_downloads/views/MyDownloads.vue b/kolibri/plugins/learn/assets/src/my_downloads/views/MyDownloads.vue index 0f22d461312..7309f228022 100644 --- a/kolibri/plugins/learn/assets/src/my_downloads/views/MyDownloads.vue +++ b/kolibri/plugins/learn/assets/src/my_downloads/views/MyDownloads.vue @@ -38,11 +38,9 @@ + @@ -54,7 +52,7 @@