Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ 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,
};

if (params.has_community_library_submission === undefined) {
extendedParams.deleted = Boolean(params.deleted) && params.deleted.toString() === 'true';
Copy link
Member Author

@taoerman taoerman Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. When filtering by Community Library:

    • params.has_community_library_submission is present
    • The if condition is false, so deleted is not set
    • Backend doesn't filter by deleted → shows both deleted and non-deleted channels
  2. When filtering by other types:

    • params.has_community_library_submission is undefined
    • The if condition is true, so deleted is set based on params.deleted
    • Backend filters by deleted status as expected

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

}

const paramsSerializer = {
indexes: null, // Handle arrays by providing the same query param multiple times
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,29 +254,14 @@
@syncing="syncInProgress"
/>
<QuickEditModal />
<MessageDialog
v-model="showDeleteModal"
:header="$tr('deleteTitle')"
>
{{ $tr('deletePrompt') }}
<template #buttons="{ close }">
<VSpacer />
<VBtn
color="primary"
flat
@click="close"
>
{{ $tr('cancel') }}
</VBtn>
<VBtn
color="primary"
data-test="delete"
@click="handleDelete"
>
{{ $tr('deleteChannelButton') }}
</VBtn>
</template>
</MessageDialog>
<RemoveChannelModal
v-if="showDeleteModal && currentChannel"
:channel-id="currentChannel.id"
:can-edit="canEdit"
data-test="delete-modal"
@delete="handleDelete"
@close="showDeleteModal = false"
/>
<VSpeedDial
v-if="showClipboardSpeedDial"
v-model="showClipboard"
Expand Down Expand Up @@ -352,9 +337,9 @@
import MainNavigationDrawer from 'shared/views/MainNavigationDrawer';
import ToolBar from 'shared/views/ToolBar';
import ChannelTokenModal from 'shared/views/channel/ChannelTokenModal';
import RemoveChannelModal from 'shared/views/channel/RemoveChannelModal';
import OfflineText from 'shared/views/OfflineText';
import ContentNodeIcon from 'shared/views/ContentNodeIcon';
import MessageDialog from 'shared/views/MessageDialog';
import { RouteNames as ChannelRouteNames } from 'frontend/channelList/constants';
import { titleMixin } from 'shared/mixins';
import DraggableRegion from 'shared/views/draggable/DraggableRegion';
Expand All @@ -371,12 +356,12 @@
SubmitToCommunityLibrarySidePanel,
ProgressModal,
ChannelTokenModal,
RemoveChannelModal,
SyncResourcesModal,
Clipboard,
OfflineText,
ContentNodeIcon,
DraggablePlaceholder,
MessageDialog,
SavingIndicator,
QuickEditModal,
},
Expand Down Expand Up @@ -574,11 +559,6 @@
inviteCollaborators: 'Invite collaborators',
shareToken: 'Share token',

// Delete channel section
deleteChannelButton: 'Delete channel',
deleteTitle: 'Delete this channel',
deletePrompt: 'This channel will be permanently deleted. This cannot be undone.',
cancel: 'Cancel',
channelDeletedSnackbar: 'Channel deleted',
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,17 +215,14 @@
</VCardActions>
</VCard>
<!-- Delete dialog -->
<KModal
<RemoveChannelModal
v-if="deleteDialog"
:title="canEdit ? $tr('deleteTitle') : $tr('removeTitle')"
:submitText="canEdit ? $tr('deleteChannel') : $tr('removeBtn')"
:cancelText="$tr('cancel')"
:channel-id="channelId"
:can-edit="canEdit"
data-test="delete-modal"
@submit="handleDelete"
@cancel="deleteDialog = false"
>
{{ canEdit ? $tr('deletePrompt') : $tr('removePrompt') }}
</KModal>
@delete="handleDelete"
@close="deleteDialog = false"
/>
<!-- Copy dialog -->
<ChannelTokenModal
v-if="channel && channel.published"
Expand All @@ -244,6 +241,7 @@
import { RouteNames } from '../../constants';
import ChannelStar from './ChannelStar';
import ChannelTokenModal from 'shared/views/channel/ChannelTokenModal';
import RemoveChannelModal from 'shared/views/channel/RemoveChannelModal';
import Thumbnail from 'shared/views/files/Thumbnail';
import Languages from 'shared/leUtils/Languages';

Expand All @@ -252,6 +250,7 @@
components: {
ChannelStar,
ChannelTokenModal,
RemoveChannelModal,
Thumbnail,
},
props: {
Expand Down Expand Up @@ -396,18 +395,11 @@
goToWebsite: 'Go to source website',
editChannel: 'Edit channel details',
copyToken: 'Copy channel token',
deleteChannel: 'Delete channel',
deleteTitle: 'Delete this channel',
removeChannel: 'Remove from channel list',
removeBtn: 'Remove',
removeTitle: 'Remove from channel list',
deletePrompt: 'This channel will be permanently deleted. This cannot be undone.',
removePrompt:
'You have view-only access to this channel. Confirm that you want to remove it from your list of channels.',
channelDeletedSnackbar: 'Channel deleted',
channelRemovedSnackbar: 'Channel removed',
channelLanguageNotSetIndicator: 'No language set',
cancel: 'Cancel',
deleteChannel: 'Delete channel',
removeChannel: 'Remove channel',
},
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<template>

<KModal
:title="canEdit ? $tr('deleteTitle') : $tr('removeTitle')"
:submitText="canEdit ? $tr('deleteChannel') : $tr('removeBtn')"
:cancelText="$tr('cancel')"
:submitDisabled="loading"
data-test="remove-channel-modal"
@submit="handleSubmit"
@cancel="close"
>
<div
v-if="loading"
class="py-4 text-center"
>
<KCircularLoader :size="24" />
</div>
<template v-else>
<div
v-if="canEdit && hasCommunityLibrarySubmission"
class="mb-3"
data-test="cl-warning"
>
{{ $tr('deleteChannelWithCLWarning') }}
</div>
{{ canEdit ? $tr('deletePrompt') : $tr('removePrompt') }}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, if canEdit is false, we are not deleting the channel itself, just removing the user from the list of readers. In that case, I think it wouldn't make much sense to show the deleteChannelWithCLWarning warning either, because the channel won't be removed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Alex, I thought my implement has matched your comment.

The warning only displays when both conditions are true:

{{ $tr('deleteChannelWithCLWarning') }}

When canEdit is false: the warning doesn't render (the v-if prevents it).
When canEdit is true: the warning only shows if the channel has a Community Library submission.
The data fetch also only runs when canEdit is true , avoiding unnecessary API calls.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, yes, mb didn't notice the v-if of the deleteChannelWithCLWarning node 😅. It is great, then!

</template>
</KModal>

</template>


<script>

import { ref, onMounted } from 'vue';
import client from 'shared/client';
import { Channel } from 'shared/data/resources';

export default {
name: 'RemoveChannelModal',
setup(props, { emit }) {
const loading = ref(false);
const hasCommunityLibrarySubmission = ref(false);

async function fetchCommunityLibrarySubmissionStatus() {
loading.value = true;
try {
const url = Channel.getUrlFunction('has_community_library_submission')(props.channelId);
const response = await client.get(url);
hasCommunityLibrarySubmission.value =
response.data?.has_community_library_submission ?? false;
} catch (error) {
hasCommunityLibrarySubmission.value = false;
} finally {
loading.value = false;
}
}

onMounted(() => {
if (props.canEdit) {
fetchCommunityLibrarySubmissionStatus();
}
});

function handleSubmit() {
emit('delete');
}

function close() {
emit('close');
}

return {
loading,
hasCommunityLibrarySubmission,
handleSubmit,
close,
};
},
props: {
channelId: {
type: String,
required: true,
},
canEdit: {
type: Boolean,
default: false,
},
},
emits: ['delete', 'close'],
$trs: {
deleteTitle: 'Delete this channel',
removeTitle: 'Remove from channel list',
deleteChannel: 'Delete channel',
removeBtn: 'Remove',
cancel: 'Cancel',
deletePrompt: 'This channel will be permanently deleted. This cannot be undone.',
removePrompt:
'You have view-only access to this channel. Confirm that you want to remove it from your list of channels.',
deleteChannelWithCLWarning:
'This channel has been shared with the Community Library. Deleting it here will not remove it from the Community Library — it may still be approved or remain available there.',
},
};

</script>


<style lang="scss" scoped></style>
38 changes: 28 additions & 10 deletions contentcuration/contentcuration/tests/viewsets/test_channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -819,25 +819,43 @@ 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)

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)
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 = channel_with_submission.version
submission.save()

channel_without_submission = testdata.channel()
channel_without_submission.editors.add(user)

testdata.channel() # Another channel without submission
self.client.force_authenticate(user=user)

response = self.client.get(
reverse_with_query(
"admin-channels-list",
query={"has_community_library_submission": True},
reverse(
"channel-has-community-library-submission",
kwargs={"pk": channel_with_submission.id},
),
format="json",
)
self.assertEqual(response.status_code, 200, response.content)
self.assertCountEqual(
[ch["id"] for ch in response.data],
[submission.channel.id],
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.assertEqual(response.status_code, 200, response.content)
self.assertFalse(response.data["has_community_library_submission"])

def test_create_channel(self):
user = testdata.user()
Expand Down
14 changes: 14 additions & 0 deletions contentcuration/contentcuration/viewsets/channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,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.
Expand Down Expand Up @@ -1050,6 +1063,7 @@ def annotate_queryset(self, queryset):


class AdminChannelFilter(BaseChannelFilter):

latest_community_library_submission_status = CharFilter(
method="filter_latest_community_library_submission_status"
)
Expand Down