From c855bc4093ff4a60b79919b8caf5d69d8d488c88 Mon Sep 17 00:00:00 2001 From: Marcella Maki Date: Fri, 11 Aug 2023 15:44:08 -0400 Subject: [PATCH 1/3] Fix removing download requests to actually remove download request properly --- kolibri/core/content/api.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) 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() From 360485b1513b23925f93641cb377c95c5f486dc9 Mon Sep 17 00:00:00 2001 From: Marcella Maki Date: Fri, 11 Aug 2023 16:00:06 -0400 Subject: [PATCH 2/3] Front end refactoring, to better manage reactivity and page state --- .../src/composables/useDownloadRequests.js | 11 +- .../views/DownloadsList/index.vue | 119 ++++++++++++------ .../src/my_downloads/views/MyDownloads.vue | 65 +++------- 3 files changed, 107 insertions(+), 88 deletions(-) diff --git a/kolibri/plugins/learn/assets/src/composables/useDownloadRequests.js b/kolibri/plugins/learn/assets/src/composables/useDownloadRequests.js index 032bf18e649..64af9a13909 100644 --- a/kolibri/plugins/learn/assets/src/composables/useDownloadRequests.js +++ b/kolibri/plugins/learn/assets/src/composables/useDownloadRequests.js @@ -4,7 +4,6 @@ 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'; @@ -40,7 +39,11 @@ export default function useDownloadRequests(store) { function fetchUserDownloadRequests(params) { return ContentRequestResource.list(params) .then(downloadRequests => { - set(downloadRequestMap, downloadRequests); + const downloads = downloadRequests.reduce((acc, obj) => { + acc[obj.id] = obj; + return acc; + }, {}); + set(downloadRequestMap, downloads); set(loading, false); }) .then(store.dispatch('notLoading')); @@ -100,7 +103,7 @@ export default function useDownloadRequests(store) { ContentRequestResource.deleteModel({ id: content.id, contentnode_id: content.contentnode_id, - }).then(Vue.delete(downloadRequestMap, content.id)); + }); return Promise.resolve(); } @@ -109,7 +112,7 @@ export default function useDownloadRequests(store) { ContentRequestResource.deleteModel({ id: content.id, contentnode_id: content.contentnode_id, - }).then(Vue.delete(downloadRequestMap, content.id)); + }); }); return Promise.resolve(); } 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..f813a660bd2 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.value)) { + downloadsToDisplay.push(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; + } + } + if (activityType) { + if (activityType.value !== 'all') { + downloadsToDisplay = downloadsToDisplay.filter(download => + download.metadata.learning_activities.includes(activityType.value) + ); + } + } + } + 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,6 +291,7 @@ }, removeResource(download) { this.$emit('removeResources', [download]); + this.sortedFilteredDownloads(); }, removeResources() { this.$emit('removeResources', this.resourcesToDelete); @@ -278,7 +325,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..e18173b4b20 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 @@ + @@ -60,6 +58,7 @@ import { computed, getCurrentInstance, watch, ref } from 'kolibri.lib.vueCompositionApi'; import commonCoreStrings from 'kolibri.coreVue.mixins.commonCoreStrings'; import responsiveWindowMixin from 'kolibri.coreVue.mixins.responsiveWindowMixin'; + import Vue from 'kolibri.lib.vue'; import useDownloadRequests from '../../composables/useDownloadRequests'; import DownloadsList from './DownloadsList'; import ActivityFilter from './Filters/ActivityFilter.vue'; @@ -103,47 +102,12 @@ activityType: activityType.value, }); }; - const sortedFilteredDownloads = () => { - let downloadsToDisplay; - if (downloadRequestMap && downloadRequestMap.value.length > 0) { - downloadsToDisplay = downloadRequestMap.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; - } - } - if (activityType) { - if (activityType.value !== 'all') { - downloadsToDisplay = downloadsToDisplay.filter(download => - download.metadata.learning_activities.includes(activityType.value) - ); - } - } - } - set(totalPageNumber, Math.ceil(downloadsToDisplay.length / pageSizeNumber.value)); - return downloadsToDisplay; - }; fetchDownloads(); fetchAvailableFreespace(); - watch(route, sortedFilteredDownloads); + watch(route, fetchDownloads); + watch(downloadRequestMap, () => { + set(totalPageNumber, downloadRequestMap.totalPageNumber); + }); return { downloadRequestMap, @@ -151,20 +115,21 @@ availableSpace, totalPageNumber, fetchAvailableFreespace, - sortedFilteredDownloads, + sort, removeDownloadRequest, removeDownloadsRequest, }; }, computed: { sizeOfMyDownloads() { - let totalSize = 0; + let size; if (this.downloadRequestMap && this.downloadRequestMap.value) { - this.downloadRequestMap.value.map( - item => (totalSize = totalSize + item.metadata.file_size) + size = Object.values(this.downloadRequestMap.value).reduce( + (acc, object) => acc + object.metadata.file_size, + 0 ); } - return totalSize; + return size; }, }, methods: { @@ -178,8 +143,12 @@ removeResources(resources) { if (resources.length === 1) { this.removeDownloadRequest(resources[0]); + Vue.delete(this.downloadRequestMap.value, resources[0].id); } else { - this.removeDownloadsRequest(resources.map(resource => ({ id: resource }))); + resources.map(resource => { + this.removeDownloadsRequest({ id: resource.id }); + Vue.delete(this.downloadRequestMap.value, resource.id); + }); } }, }, From 2f0dd63dcf6820d276a3aaa03f6f56b0cd7705d6 Mon Sep 17 00:00:00 2001 From: Marcella Maki Date: Tue, 15 Aug 2023 18:36:35 -0400 Subject: [PATCH 3/3] PR updates to address feedback --- .../src/composables/useDownloadRequests.js | 21 ++++----------- .../views/DownloadsList/index.vue | 22 +++++++--------- .../src/my_downloads/views/MyDownloads.vue | 26 +++++-------------- 3 files changed, 22 insertions(+), 47 deletions(-) diff --git a/kolibri/plugins/learn/assets/src/composables/useDownloadRequests.js b/kolibri/plugins/learn/assets/src/composables/useDownloadRequests.js index 64af9a13909..03924ed5b93 100644 --- a/kolibri/plugins/learn/assets/src/composables/useDownloadRequests.js +++ b/kolibri/plugins/learn/assets/src/composables/useDownloadRequests.js @@ -9,6 +9,7 @@ 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', { @@ -39,11 +40,9 @@ export default function useDownloadRequests(store) { function fetchUserDownloadRequests(params) { return ContentRequestResource.list(params) .then(downloadRequests => { - const downloads = downloadRequests.reduce((acc, obj) => { - acc[obj.id] = obj; - return acc; - }, {}); - set(downloadRequestMap, downloads); + for (const obj of downloadRequests) { + set(downloadRequestMap, obj.id, obj); + } set(loading, false); }) .then(store.dispatch('notLoading')); @@ -104,16 +103,7 @@ export default function useDownloadRequests(store) { id: content.id, contentnode_id: content.contentnode_id, }); - return Promise.resolve(); - } - - function removeDownloadsRequest(contentList) { - contentList.forEach(content => { - ContentRequestResource.deleteModel({ - id: content.id, - contentnode_id: content.contentnode_id, - }); - }); + Vue.delete(downloadRequestMap, content.id); return Promise.resolve(); } @@ -141,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 f813a660bd2..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 @@ -68,7 +68,7 @@ v-else :text="coreString('viewAction')" appearance="flat-button" - :href="genExternalContentURLBackLinkCurrentPage(download.id)" + :href="genExternalContentURLBackLinkCurrentPage(download.contentnode_id)" /> @@ -143,9 +143,16 @@ const sortedFilteredDownloads = () => { let downloadsToDisplay = []; if (downloadRequestMap) { - for (const [, value] of Object.entries(downloadRequestMap.value)) { + 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': @@ -169,13 +176,6 @@ break; } } - if (activityType) { - if (activityType.value !== 'all') { - downloadsToDisplay = downloadsToDisplay.filter(download => - download.metadata.learning_activities.includes(activityType.value) - ); - } - } } set(totalPageNumber, Math.ceil(downloadsToDisplay.length / pageSizeNumber.value)); set(downloads, downloadsToDisplay); @@ -295,10 +295,8 @@ }, 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); 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 e18173b4b20..7309f228022 100644 --- a/kolibri/plugins/learn/assets/src/my_downloads/views/MyDownloads.vue +++ b/kolibri/plugins/learn/assets/src/my_downloads/views/MyDownloads.vue @@ -52,13 +52,12 @@