From bb55d03cc724b9591d10f54628f211681b133fed Mon Sep 17 00:00:00 2001 From: taoerman Date: Tue, 11 Nov 2025 20:41:33 -0800 Subject: [PATCH 1/7] Handle deletion of a Channel with a related Community Library Submission --- .../vuex/channelAdmin/actions.js | 8 +- .../views/TreeView/TreeViewBase.vue | 9 +++ .../channelList/views/Channel/ChannelItem.vue | 9 +++ .../tests/viewsets/test_channel.py | 81 +++++++++++++++++++ .../contentcuration/viewsets/channel.py | 15 +++- 5 files changed, 120 insertions(+), 2 deletions(-) diff --git a/contentcuration/contentcuration/frontend/administration/vuex/channelAdmin/actions.js b/contentcuration/contentcuration/frontend/administration/vuex/channelAdmin/actions.js index 23b9f3400a..1f50ef50a2 100644 --- a/contentcuration/contentcuration/frontend/administration/vuex/channelAdmin/actions.js +++ b/contentcuration/contentcuration/frontend/administration/vuex/channelAdmin/actions.js @@ -6,9 +6,15 @@ import { Channel } from 'shared/data/resources'; export function loadChannels({ commit }, params) { const extendedParams = { ...params, - deleted: Boolean(params.deleted) && params.deleted.toString() === 'true', page_size: params.page_size || 25, }; + + const isCommunityLibraryFilter = + params.has_community_library_submission === true || + params.has_community_library_submission === 'true'; + if (!isCommunityLibraryFilter) { + extendedParams.deleted = Boolean(params.deleted) && params.deleted.toString() === 'true'; + } const paramsSerializer = { indexes: null, // Handle arrays by providing the same query param multiple times diff --git a/contentcuration/contentcuration/frontend/channelEdit/views/TreeView/TreeViewBase.vue b/contentcuration/contentcuration/frontend/channelEdit/views/TreeView/TreeViewBase.vue index 6ca82f5d2d..bcc5f34141 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/views/TreeView/TreeViewBase.vue +++ b/contentcuration/contentcuration/frontend/channelEdit/views/TreeView/TreeViewBase.vue @@ -258,6 +258,13 @@ v-model="showDeleteModal" :header="$tr('deleteTitle')" > +
+ {{ $tr('deleteChannelWithCLWarning') }} +
{{ $tr('deletePrompt') }} + + + + + + + diff --git a/contentcuration/contentcuration/tests/viewsets/test_channel.py b/contentcuration/contentcuration/tests/viewsets/test_channel.py index be2e25fc2b..ed43f4e6c3 100644 --- a/contentcuration/contentcuration/tests/viewsets/test_channel.py +++ b/contentcuration/contentcuration/tests/viewsets/test_channel.py @@ -819,73 +819,8 @@ def test_admin_channel_filter__community_library_live(self): [submission1.channel.id], ) - def test_admin_channel_filter__has_community_library_submission(self): - self.client.force_authenticate(user=self.admin_user) - - submission = testdata.community_library_submission() - - testdata.channel() # Another channel without submission - - response = self.client.get( - reverse_with_query( - "admin-channels-list", - query={"has_community_library_submission": True}, - ), - format="json", - ) - self.assertEqual(response.status_code, 200, response.content) - self.assertCountEqual( - [ch["id"] for ch in response.data], - [submission.channel.id], - ) - - def test_admin_channel_filter__community_library_includes_soft_deleted(self): - """Test that filtering by CommunityLibrary includes soft-deleted channels""" - self.client.force_authenticate(user=self.admin_user) - - user = testdata.user() - channel_with_submission = testdata.channel() - channel_with_submission.editors.add(user) - channel_with_submission.version = 1 - channel_with_submission.save() - submission = testdata.community_library_submission() - submission.channel = channel_with_submission - submission.author = user - submission.channel_version = 1 - submission.save() - - channel_with_submission.deleted = True - channel_with_submission.save(actor_id=self.admin_user.id) - - channel_not_deleted = testdata.channel() - channel_not_deleted.editors.add(user) - channel_not_deleted.version = 1 - channel_not_deleted.save() - submission2 = testdata.community_library_submission() - submission2.channel = channel_not_deleted - submission2.author = user - submission2.channel_version = 1 - submission2.save() - - response = self.client.get( - reverse_with_query( - "admin-channels-list", - query={"has_community_library_submission": True}, - ), - format="json", - ) - self.assertEqual(response.status_code, 200, response.content) - channel_ids = [ch["id"] for ch in response.data] - self.assertIn(channel_with_submission.id, channel_ids) - self.assertIn(channel_not_deleted.id, channel_ids) - deleted_channel_data = next( - (ch for ch in response.data if ch["id"] == channel_with_submission.id), None - ) - self.assertIsNotNone(deleted_channel_data) - self.assertTrue(deleted_channel_data["deleted"]) - - def test_channel_list__has_community_library_submission_field(self): - """Test that has_community_library_submission field is included in channel list response""" + def test_has_community_library_submission_endpoint(self): + """Test the on-demand has_community_library_submission endpoint""" user = testdata.user() channel_with_submission = testdata.channel() channel_with_submission.editors.add(user) @@ -901,24 +836,20 @@ def test_channel_list__has_community_library_submission_field(self): channel_without_submission.editors.add(user) self.client.force_authenticate(user=user) + response = self.client.get( - reverse("channel-list"), data={"edit": True}, format="json" + reverse("channel-has-community-library-submission", kwargs={"pk": channel_with_submission.id}), + format="json" ) self.assertEqual(response.status_code, 200, response.content) - - channel_with_submission_data = next( - (ch for ch in response.data if ch["id"] == channel_with_submission.id), - None, - ) - channel_without_submission_data = next( - (ch for ch in response.data if ch["id"] == channel_without_submission.id), - None, + self.assertTrue(response.data["has_community_library_submission"]) + + response = self.client.get( + reverse("channel-has-community-library-submission", kwargs={"pk": channel_without_submission.id}), + format="json" ) - - self.assertIsNotNone(channel_with_submission_data) - self.assertIsNotNone(channel_without_submission_data) - self.assertTrue(channel_with_submission_data["has_community_library_submission"]) - self.assertFalse(channel_without_submission_data["has_community_library_submission"]) + self.assertEqual(response.status_code, 200, response.content) + self.assertFalse(response.data["has_community_library_submission"]) def test_create_channel(self): user = testdata.user() diff --git a/contentcuration/contentcuration/viewsets/channel.py b/contentcuration/contentcuration/viewsets/channel.py index 9940a6d49b..2e1ea0859b 100644 --- a/contentcuration/contentcuration/viewsets/channel.py +++ b/contentcuration/contentcuration/viewsets/channel.py @@ -67,9 +67,11 @@ from contentcuration.viewsets.base import create_change_tracker from contentcuration.viewsets.base import ReadOnlyValuesViewset from contentcuration.viewsets.base import RequiredFilterSet +from contentcuration.viewsets.base import RequiredFiltersFilterBackend from contentcuration.viewsets.base import RESTDestroyModelMixin from contentcuration.viewsets.base import RESTUpdateModelMixin from contentcuration.viewsets.base import ValuesViewset +from contentcuration.viewsets.base import ValuesViewsetOrderingFilter from contentcuration.viewsets.common import ContentDefaultsSerializer from contentcuration.viewsets.common import JSONFieldDictSerializer from contentcuration.viewsets.common import SQCount @@ -446,7 +448,7 @@ class ChannelViewSet(ValuesViewset): ordering = "-modified" field_map = channel_field_map - values = base_channel_values + ("edit", "view", "unpublished_changes", "has_community_library_submission") + values = base_channel_values + ("edit", "view", "unpublished_changes") def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) @@ -523,13 +525,6 @@ def annotate_queryset(self, queryset): unpublished_changes=Exists(_unpublished_changes_query(OuterRef("id"))) ) - # check if channel has any Community Library submissions - queryset = queryset.annotate( - has_community_library_submission=Exists( - CommunityLibrarySubmission.objects.filter(channel_id=OuterRef("id")) - ) - ) - return queryset def publish_from_changes(self, changes): @@ -906,6 +901,19 @@ def get_published_data(self, request, pk=None) -> Response: return Response(channel.published_data) + @action( + detail=True, + methods=["get"], + url_path="has_community_library_submission", + url_name="has-community-library-submission", + ) + def has_community_library_submission(self, request, pk=None) -> Response: + channel = self.get_object() + has_submission = CommunityLibrarySubmission.objects.filter( + channel_id=channel.id + ).exists() + return Response({"has_community_library_submission": has_submission}) + def _channel_exists(self, channel_id) -> bool: """ Check if a channel exists. @@ -1067,12 +1075,6 @@ class AdminChannelFilter(BaseChannelFilter): method="filter_has_community_library_submission", ) - def filter_deleted(self, queryset, name, value): - has_cl_filter = self.request.query_params.get("has_community_library_submission") - if has_cl_filter and has_cl_filter.lower() == "true": - return queryset - return queryset.filter(deleted=value) - def filter_keywords(self, queryset, name, value): keywords = value.split(" ") editors_first_name = reduce( From c4aa056bfac2f81768e88923e872fddea3cf9abe Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Wed, 12 Nov 2025 23:33:14 +0000 Subject: [PATCH 4/7] [pre-commit.ci lite] apply automatic fixes --- .../views/channel/RemoveChannelModal.vue | 40 +++++++++---------- .../tests/viewsets/test_channel.py | 18 ++++++--- 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/contentcuration/contentcuration/frontend/shared/views/channel/RemoveChannelModal.vue b/contentcuration/contentcuration/frontend/shared/views/channel/RemoveChannelModal.vue index 9fdf3f2140..c6d16b53f5 100644 --- a/contentcuration/contentcuration/frontend/shared/views/channel/RemoveChannelModal.vue +++ b/contentcuration/contentcuration/frontend/shared/views/channel/RemoveChannelModal.vue @@ -12,7 +12,7 @@ >
{ + watch(dialog, newValue => { if (newValue && props.canEdit) { fetchCommunityLibrarySubmissionStatus(); } else { @@ -85,7 +70,8 @@ const detailUrl = window.Urls.channel_detail(props.channelId); const url = `${detailUrl.replace(/\/$/, '')}/has_community_library_submission`; const response = await client.get(url); - hasCommunityLibrarySubmission.value = response.data?.has_community_library_submission ?? false; + hasCommunityLibrarySubmission.value = + response.data?.has_community_library_submission ?? false; } catch (error) { hasCommunityLibrarySubmission.value = false; } finally { @@ -101,7 +87,7 @@ dialog.value = false; } - const $tr = (messageId) => { + const $tr = messageId => { return proxy.$tr(messageId); }; @@ -115,6 +101,21 @@ $tr, }; }, + props: { + value: { + type: Boolean, + default: false, + }, + channelId: { + type: String, + required: true, + }, + canEdit: { + type: Boolean, + default: false, + }, + }, + emits: ['input', 'delete'], $trs: { deleteTitle: 'Delete this channel', removeTitle: 'Remove from channel list', @@ -133,4 +134,3 @@ - diff --git a/contentcuration/contentcuration/tests/viewsets/test_channel.py b/contentcuration/contentcuration/tests/viewsets/test_channel.py index 6af7e906c1..814acdabb6 100644 --- a/contentcuration/contentcuration/tests/viewsets/test_channel.py +++ b/contentcuration/contentcuration/tests/viewsets/test_channel.py @@ -836,17 +836,23 @@ def test_has_community_library_submission_endpoint(self): channel_without_submission.editors.add(user) self.client.force_authenticate(user=user) - + response = self.client.get( - reverse("channel-has-community-library-submission", kwargs={"pk": channel_with_submission.id}), - format="json" + reverse( + "channel-has-community-library-submission", + kwargs={"pk": channel_with_submission.id}, + ), + format="json", ) self.assertEqual(response.status_code, 200, response.content) self.assertTrue(response.data["has_community_library_submission"]) - + response = self.client.get( - reverse("channel-has-community-library-submission", kwargs={"pk": channel_without_submission.id}), - format="json" + reverse( + "channel-has-community-library-submission", + kwargs={"pk": channel_without_submission.id}, + ), + format="json", ) self.assertEqual(response.status_code, 200, response.content) self.assertFalse(response.data["has_community_library_submission"]) From 1aecfdfdacc8c4a41956074a41e9fcb3b5cbc620 Mon Sep 17 00:00:00 2001 From: taoerman Date: Wed, 12 Nov 2025 15:58:12 -0800 Subject: [PATCH 5/7] fix linting --- .../frontend/channelList/views/Channel/ChannelItem.vue | 3 ++- .../frontend/shared/views/channel/RemoveChannelModal.vue | 1 - contentcuration/contentcuration/viewsets/channel.py | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/contentcuration/contentcuration/frontend/channelList/views/Channel/ChannelItem.vue b/contentcuration/contentcuration/frontend/channelList/views/Channel/ChannelItem.vue index d52f22a63e..3b5a3d373e 100644 --- a/contentcuration/contentcuration/frontend/channelList/views/Channel/ChannelItem.vue +++ b/contentcuration/contentcuration/frontend/channelList/views/Channel/ChannelItem.vue @@ -397,7 +397,8 @@ channelDeletedSnackbar: 'Channel deleted', channelRemovedSnackbar: 'Channel removed', channelLanguageNotSetIndicator: 'No language set', - cancel: 'Cancel', + deleteChannel: 'Delete channel', + removeChannel: 'Remove channel', }, }; diff --git a/contentcuration/contentcuration/frontend/shared/views/channel/RemoveChannelModal.vue b/contentcuration/contentcuration/frontend/shared/views/channel/RemoveChannelModal.vue index 9fdf3f2140..574d5c2ba6 100644 --- a/contentcuration/contentcuration/frontend/shared/views/channel/RemoveChannelModal.vue +++ b/contentcuration/contentcuration/frontend/shared/views/channel/RemoveChannelModal.vue @@ -109,7 +109,6 @@ loading, hasCommunityLibrarySubmission, dialog, - fetchCommunityLibrarySubmissionStatus, handleSubmit, close, $tr, diff --git a/contentcuration/contentcuration/viewsets/channel.py b/contentcuration/contentcuration/viewsets/channel.py index 1ad40b29db..d80b57bee9 100644 --- a/contentcuration/contentcuration/viewsets/channel.py +++ b/contentcuration/contentcuration/viewsets/channel.py @@ -1063,6 +1063,11 @@ def annotate_queryset(self, queryset): class AdminChannelFilter(BaseChannelFilter): + # Make filters optional for admin viewset + def __init__(self, *args, **kwargs): + kwargs["required"] = False + super().__init__(*args, **kwargs) + latest_community_library_submission_status = CharFilter( method="filter_latest_community_library_submission_status" ) From 42aac79c9f67da49c9705c1b3f784b132f7ab129 Mon Sep 17 00:00:00 2001 From: taoerman Date: Wed, 12 Nov 2025 16:54:50 -0800 Subject: [PATCH 6/7] fix code --- .../frontend/administration/vuex/channelAdmin/actions.js | 4 ++++ contentcuration/contentcuration/viewsets/channel.py | 4 ---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contentcuration/contentcuration/frontend/administration/vuex/channelAdmin/actions.js b/contentcuration/contentcuration/frontend/administration/vuex/channelAdmin/actions.js index 0fc380cde9..c58cd6350b 100644 --- a/contentcuration/contentcuration/frontend/administration/vuex/channelAdmin/actions.js +++ b/contentcuration/contentcuration/frontend/administration/vuex/channelAdmin/actions.js @@ -9,6 +9,10 @@ export function loadChannels({ commit }, params) { page_size: params.page_size || 25, }; + if (params.has_community_library_submission === undefined) { + extendedParams.deleted = Boolean(params.deleted) && params.deleted.toString() === 'true'; + } + const paramsSerializer = { indexes: null, // Handle arrays by providing the same query param multiple times }; diff --git a/contentcuration/contentcuration/viewsets/channel.py b/contentcuration/contentcuration/viewsets/channel.py index d80b57bee9..7d28c6387f 100644 --- a/contentcuration/contentcuration/viewsets/channel.py +++ b/contentcuration/contentcuration/viewsets/channel.py @@ -1063,10 +1063,6 @@ def annotate_queryset(self, queryset): class AdminChannelFilter(BaseChannelFilter): - # Make filters optional for admin viewset - def __init__(self, *args, **kwargs): - kwargs["required"] = False - super().__init__(*args, **kwargs) latest_community_library_submission_status = CharFilter( method="filter_latest_community_library_submission_status" From f0d03a2c7f4cc5421752f2f6a80cf40f49b40632 Mon Sep 17 00:00:00 2001 From: taoerman Date: Sun, 16 Nov 2025 19:58:41 -0800 Subject: [PATCH 7/7] fix code --- .../views/TreeView/TreeViewBase.vue | 4 +- .../channelList/views/Channel/ChannelItem.vue | 3 +- .../views/channel/RemoveChannelModal.vue | 55 +++++-------------- 3 files changed, 18 insertions(+), 44 deletions(-) diff --git a/contentcuration/contentcuration/frontend/channelEdit/views/TreeView/TreeViewBase.vue b/contentcuration/contentcuration/frontend/channelEdit/views/TreeView/TreeViewBase.vue index 3ba33c9d67..7b602dbd46 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/views/TreeView/TreeViewBase.vue +++ b/contentcuration/contentcuration/frontend/channelEdit/views/TreeView/TreeViewBase.vue @@ -255,12 +255,12 @@ /> - +