Skip to content

Conversation

@taoerman
Copy link
Member

@taoerman taoerman commented Nov 12, 2025

Summary

Shows a warning in delete confirmation modals when deleting a channel that has Community Library submissions, explaining that deletion in Studio does not remove it from the Community Library.

The backend exposes a has_community_library_submission boolean field for the frontend to detect these channels.

When admins filter by "Community Library" in the admin channels list, soft-deleted channels with Community Library submissions are now included, allowing admins to audit and manage them.

Includes backend tests for both features.

References

Fixed #5459

Reviewer guidance

Run unit tests Or test manually!

Copy link
Member

@AlexVelezLl AlexVelezLl left a comment

Choose a reason for hiding this comment

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

Just a general concern regarding computing the has_community_library_submission for every channel load.

Comment on lines 1075 to 1081
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)
Copy link
Member

Choose a reason for hiding this comment

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

Oh, I think we can leave this responsibility to the frontend, so if we ever want to filter by deleted channels that have been submitted to the Community Library, we can just adjust the frontend code!

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed, Thanks!

Comment on lines 531 to 536
# check if channel has any Community Library submissions
queryset = queryset.annotate(
has_community_library_submission=Exists(
CommunityLibrarySubmission.objects.filter(channel_id=OuterRef("id"))
)
)
Copy link
Member

Choose a reason for hiding this comment

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

I am a bit skeptical about computing the has_community_library_submission here for every channel for a feature that may only appear in very few cases, such as removing a channel.

I think an alternative may be to create a RemoveChannelModal and load one Community Library submission for a given channel within that modal. With this, we will only load this data when needed, rather than on every single channel load. What do you think? @taoerman

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed, Thanks!

};

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!

Copy link
Member

@AlexVelezLl AlexVelezLl left a comment

Choose a reason for hiding this comment

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

Just a couple of notes before merging this PR! 👐

Comment on lines 17 to 20
<VProgressCircular
indeterminate
size="24"
/>
Copy link
Member

Choose a reason for hiding this comment

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

Could we use the KCircularLoader component instead? As we are getting rid of the Vuetify components, its important to stop using them!

Copy link
Member Author

Choose a reason for hiding this comment

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

Got it!

import { defineComponent, ref, computed, watch, getCurrentInstance } from 'vue';
import client from 'shared/client';
export default defineComponent({
Copy link
Member

Choose a reason for hiding this comment

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

Oh, we don't need here the defineComponent function, since we are working with component files, Vue automatically interprets that the default export of this file is the definition of the component.

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed, Thanks!

Comment on lines 58 to 65
watch(dialog, newValue => {
if (newValue && props.canEdit) {
fetchCommunityLibrarySubmissionStatus();
} else {
hasCommunityLibrarySubmission.value = false;
loading.value = false;
}
});
Copy link
Member

Choose a reason for hiding this comment

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

I think that to prevent having to reset this data when the prop.value changes, a more common pattern we have across our products is to use the v-if directive rather than the v-model to mount and unmount these components. And expose a @close event to set the rendering variable to false.

So, if we call this component like this instead:

    <RemoveChannelModal
      v-if="deleteDialog"
      :channel-id="channelId"
      :can-edit="canEdit"
      data-test="delete-modal"
      @delete="handleDelete"
      @close="deleteDialog = false"
    />

Then we may only need to rely on the onMounted hook to fetch the data, and we wouldn't need to care about resetting the ref values, as they will always be defined when the modal is opened.

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed, thanks!

Comment on lines 70 to 71
const detailUrl = window.Urls.channel_detail(props.channelId);
const url = `${detailUrl.replace(/\/$/, '')}/has_community_library_submission`;
Copy link
Member

Choose a reason for hiding this comment

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

Here, we don't really need to invoke the channel_detail first and then concatenate the has_community_library_submission endpoint. What we can do instead is to abstract this call inside the Channel Resource and use the getUrlFunction instead, and pass the URL name that we defined in the viewset. Here is an example of how we do this for the language_exists action https://github.com/taoerman/studio/blob/ce0416e0eee2b01548811d40b514eb41d2905779/contentcuration/contentcuration/frontend/shared/data/resources.js#L1397

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks Alex, very helpful!

Comment on lines 90 to 92
const $tr = messageId => {
return proxy.$tr(messageId);
};
Copy link
Member

Choose a reason for hiding this comment

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

Here we don't need to define this $tr here! We already have a $tr function globally available in the template 👐

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed! Thanks!

>
{{ $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!

};

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

Choose a reason for hiding this comment

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

Nice!

Copy link
Member

@AlexVelezLl AlexVelezLl left a comment

Choose a reason for hiding this comment

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

Code changes look good to me, and manual QA checks out. Thanks @taoerman!

@AlexVelezLl AlexVelezLl merged commit 8ce7058 into learningequality:unstable Nov 17, 2025
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Handle deletion of a Channel with a related Community Library Submission

2 participants