From 7f440fcd47c550185c53e85ff321e45848179688 Mon Sep 17 00:00:00 2001 From: taoerman Date: Wed, 19 Nov 2025 15:53:46 -0800 Subject: [PATCH 01/13] Show license audit and special permissions checks in the Submit to Community Library Side Panel merge# --- contentcuration/contentcuration/celery.py | 1 + .../SubmitToCommunityLibrarySidePanel/Box.vue | 20 +- .../LicenseStatus.vue | 111 ++++++++++ .../SpecialPermissionsList.vue | 200 ++++++++++++++++++ .../composables/useLicenseAudit.js | 160 ++++++++++++++ .../composables/useLicenseNames.js | 35 +++ .../composables/useSpecialPermissions.js | 115 ++++++++++ .../index.vue | 123 +++++++++-- .../frontend/shared/data/resources.js | 24 ++- .../strings/communityChannelsStrings.js | 42 ++++ contentcuration/contentcuration/urls.py | 8 + .../audited_special_permissions_license.py | 57 +++++ 12 files changed, 877 insertions(+), 19 deletions(-) create mode 100644 contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/LicenseStatus.vue create mode 100644 contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/SpecialPermissionsList.vue create mode 100644 contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLicenseAudit.js create mode 100644 contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLicenseNames.js create mode 100644 contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useSpecialPermissions.js create mode 100644 contentcuration/contentcuration/viewsets/audited_special_permissions_license.py diff --git a/contentcuration/contentcuration/celery.py b/contentcuration/contentcuration/celery.py index 9f74f2d2fc..5901d5d4be 100644 --- a/contentcuration/contentcuration/celery.py +++ b/contentcuration/contentcuration/celery.py @@ -12,3 +12,4 @@ # of setting it as an attribute on our custom Celery class app = CeleryApp("contentcuration", task_cls=CeleryTask) app.config_from_object(settings.CELERY) +app.autodiscover_tasks(["contentcuration"]) diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/Box.vue b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/Box.vue index 6486230c77..e47f52c4e9 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/Box.vue +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/Box.vue @@ -17,6 +17,7 @@
@@ -63,6 +64,8 @@ switch (props.kind) { case 'warning': return paletteTheme.red.v_100; + case 'success': + return paletteTheme.green.v_100; case 'info': return paletteTheme.grey.v_100; default: @@ -73,6 +76,8 @@ switch (props.kind) { case 'warning': return paletteTheme.red.v_300; + case 'success': + return paletteTheme.green.v_300; case 'info': return 'transparent'; default: @@ -83,6 +88,8 @@ switch (props.kind) { case 'warning': return 'error'; + case 'success': + return 'circleCheckmark'; case 'info': return 'infoOutline'; default: @@ -91,19 +98,28 @@ }); const titleColor = computed(() => { - return props.kind === 'warning' ? paletteTheme.red.v_600 : tokensTheme.text; + if (props.kind === 'warning') return paletteTheme.red.v_600; + if (props.kind === 'success') return paletteTheme.green.v_600; + return tokensTheme.text; }); const descriptionColor = computed(() => { return props.kind === 'warning' ? paletteTheme.grey.v_800 : tokensTheme.text; }); + const iconColor = computed(() => { + if (props.kind === 'warning') return paletteTheme.red.v_600; + if (props.kind === 'success') return paletteTheme.green.v_600; + return tokensTheme.text; + }); + return { boxBackgroundColor, boxBorderColor, titleColor, descriptionColor, icon, + iconColor, }; }, props: { @@ -111,7 +127,7 @@ type: String, required: false, default: 'info', - validator: value => ['warning', 'info'].includes(value), + validator: value => ['warning', 'success', 'info'].includes(value), }, loading: { type: Boolean, diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/LicenseStatus.vue b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/LicenseStatus.vue new file mode 100644 index 0000000000..6ec446461c --- /dev/null +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/LicenseStatus.vue @@ -0,0 +1,111 @@ + + + + diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/SpecialPermissionsList.vue b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/SpecialPermissionsList.vue new file mode 100644 index 0000000000..2a1af758e8 --- /dev/null +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/SpecialPermissionsList.vue @@ -0,0 +1,200 @@ + + + + + + diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLicenseAudit.js b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLicenseAudit.js new file mode 100644 index 0000000000..ca6613a44a --- /dev/null +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLicenseAudit.js @@ -0,0 +1,160 @@ +import { computed, onUnmounted, ref, unref } from 'vue'; +import { Channel } from 'shared/data/resources'; +import { usePublishedData } from './usePublishedData'; + +const POLLING_INTERVAL_MS = 2000; +const MAX_POLLING_DURATION_MS = 5 * 60 * 1000; + +export function useLicenseAudit(channelId, channelVersion) { + const { + isLoading: publishedDataIsLoading, + isFinished: publishedDataIsFinished, + data: publishedData, + fetchData: fetchPublishedData, + } = usePublishedData(channelId); + + const isAuditing = ref(false); + const auditTaskId = ref(null); + const pollingInterval = ref(null); + const pollingStartTime = ref(null); + const auditError = ref(null); + + const currentVersionData = computed(() => { + const version = unref(channelVersion); + if (!publishedData.value || version == null) { + return undefined; + } + const versionStr = String(version); + return publishedData.value[versionStr] || publishedData.value[Number(versionStr)]; + }); + + const hasAuditData = computed(() => { + const versionData = currentVersionData.value; + if (!versionData) { + return false; + } + + return ( + 'community_library_invalid_licenses' in versionData && + 'community_library_special_permissions' in versionData + ); + }); + + const invalidLicenses = computed(() => { + const versionData = currentVersionData.value; + if (!versionData) return undefined; + return versionData.community_library_invalid_licenses; + }); + + const specialPermissions = computed(() => { + const versionData = currentVersionData.value; + if (!versionData) return undefined; + return versionData.community_library_special_permissions; + }); + + const includedLicenses = computed(() => { + const versionData = currentVersionData.value; + if (!versionData) return undefined; + return versionData.included_licenses; + }); + + const isAuditInProgress = computed(() => { + if (isAuditComplete.value) return false; + return isAuditing.value || pollingInterval.value !== null; + }); + + const isAuditComplete = computed(() => { + return publishedDataIsFinished.value && hasAuditData.value; + }); + + function stopPolling() { + if (pollingInterval.value) { + clearInterval(pollingInterval.value); + pollingInterval.value = null; + } + pollingStartTime.value = null; + } + + function startPolling() { + if (pollingInterval.value) return; + + pollingStartTime.value = Date.now(); + + pollingInterval.value = setInterval(async () => { + if (Date.now() - pollingStartTime.value > MAX_POLLING_DURATION_MS) { + stopPolling(); + auditError.value = new Error('Audit timeout: Maximum polling duration exceeded'); + isAuditing.value = false; + return; + } + + try { + await fetchPublishedData(); + + if (hasAuditData.value) { + stopPolling(); + isAuditing.value = false; + auditError.value = null; + } + } catch (error) { + stopPolling(); + isAuditing.value = false; + auditError.value = error; + } + }, POLLING_INTERVAL_MS); + } + + async function triggerAudit() { + if (isAuditing.value) return; + + try { + isAuditing.value = true; + auditError.value = null; + + const response = await Channel.auditLicenses(channelId); + auditTaskId.value = response.task_id; + + startPolling(); + } catch (error) { + isAuditing.value = false; + auditError.value = error; + throw error; + } + } + + async function checkAndTriggerAudit() { + if (!publishedDataIsFinished.value) { + await fetchPublishedData(); + } + + if (hasAuditData.value || isAuditing.value) { + return; + } + + await triggerAudit(); + } + + onUnmounted(() => { + stopPolling(); + }); + + return { + isLoading: computed(() => { + if (isAuditComplete.value || auditError.value) return false; + return publishedDataIsLoading.value || isAuditInProgress.value; + }), + isFinished: computed(() => isAuditComplete.value), + isAuditing: isAuditInProgress, + invalidLicenses, + specialPermissions, + includedLicenses, + hasAuditData, + auditTaskId, + error: auditError, + + checkAndTriggerAudit, + triggerAudit, + fetchPublishedData, + }; +} + diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLicenseNames.js b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLicenseNames.js new file mode 100644 index 0000000000..33c7b30b02 --- /dev/null +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLicenseNames.js @@ -0,0 +1,35 @@ +import { computed, ref } from 'vue'; +import { findLicense } from 'shared/utils/helpers'; + +export function useLicenseNames(licenseIds) { + const isLoading = ref(false); + const error = ref(null); + + const licenseNames = computed(() => { + if (!licenseIds.value || licenseIds.value.length === 0) { + return []; + } + + return licenseIds.value + .map(id => { + const license = findLicense(id); + return license?.license_name || null; + }) + .filter(name => name !== null); + }); + + const formattedLicenseNames = computed(() => { + const names = licenseNames.value; + if (names.length === 0) return ''; + if (names.length === 1) return names[0]; + return names.join(', '); + }); + + return { + licenseNames, + formattedLicenseNames, + isLoading, + error, + }; +} + diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useSpecialPermissions.js b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useSpecialPermissions.js new file mode 100644 index 0000000000..cba4067636 --- /dev/null +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useSpecialPermissions.js @@ -0,0 +1,115 @@ +import { computed, ref, unref, watchEffect } from 'vue'; +import { AuditedSpecialPermissionsLicense } from 'shared/data/resources'; + +const ITEMS_PER_PAGE = 5; + +export function useSpecialPermissions(permissionIds) { + const permissions = ref([]); + const isLoading = ref(false); + const error = ref(null); + const currentPage = ref(1); + + const nonDistributablePermissions = computed(() => { + return permissions.value.filter(p => !p.distributable); + }); + + const totalPages = computed(() => { + return Math.ceil(nonDistributablePermissions.value.length / ITEMS_PER_PAGE); + }); + + const currentPagePermissions = computed(() => { + const start = (currentPage.value - 1) * ITEMS_PER_PAGE; + const end = start + ITEMS_PER_PAGE; + return nonDistributablePermissions.value.slice(start, end); + }); + + async function fetchPermissions(ids) { + if (!ids || ids.length === 0) { + permissions.value = []; + return; + } + + + + isLoading.value = true; + error.value = null; + + try { + const response = await AuditedSpecialPermissionsLicense.fetchCollection({ + by_ids: ids.join(','), + distributable: false, + }); + + const flattenedPermissions = []; + response.forEach(permission => { + const sentences = permission.description + .split('.') + .map(s => s.trim()) + .filter(s => s.length > 0); + + sentences.forEach((sentence, index) => { + let text = sentence; + if (!text.endsWith('.')) { + text += '.'; + } + + flattenedPermissions.push({ + id: `${permission.id}-${index}`, + originalId: permission.id, + description: text, + distributable: permission.distributable, + }); + }); + }); + permissions.value = flattenedPermissions; + } catch (err) { + + error.value = err; + permissions.value = []; + } finally { + isLoading.value = false; + } + } + + function nextPage() { + if (currentPage.value < totalPages.value) { + currentPage.value += 1; + } + } + + function previousPage() { + if (currentPage.value > 1) { + currentPage.value -= 1; + } + } + + const resolvedPermissionIds = computed(() => { + const ids = unref(permissionIds); + if (!ids || ids.length === 0) { + return []; + } + return Array.isArray(ids) ? ids : [ids]; + }); + + watchEffect(() => { + const ids = resolvedPermissionIds.value; + + if (ids.length === 0) { + permissions.value = []; + return; + } + fetchPermissions(ids); + }); + + return { + permissions: nonDistributablePermissions, + currentPagePermissions, + isLoading, + error, + currentPage, + totalPages, + nextPage, + previousPage, + }; +} + diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/index.vue b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/index.vue index 7b08ee54e8..ed4318d941 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/index.vue +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/index.vue @@ -130,6 +130,31 @@ {{ detectedCategories }} +
+ +
+
+ {{ checkingChannelCompatibility$() }} +
+
+ {{ checkingChannelCompatibilitySecondary$() }} +
+
+
+ +
tokensTheme.annotation); @@ -277,6 +309,7 @@ const isPublishing = computed(() => props.channel?.publishing === true); const currentChannelVersion = computed(() => props.channel?.version); const replacementConfirmed = ref(false); + const checkedSpecialPermissions = ref([]); const { isLoading: latestSubmissionIsLoading, @@ -368,18 +401,6 @@ ); }); - const canBeSubmitted = computed(() => { - if (isPublishing.value) return false; - const baseCondition = - canBeEdited.value && publishedDataIsFinished.value && description.value.length >= 1; - - if (needsReplacementConfirmation.value) { - return baseCondition && replacementConfirmed.value; - } - - return baseCondition; - }); - const { isLoading: publishedDataIsLoading, isFinished: publishedDataIsFinished, @@ -398,6 +419,44 @@ return channelVersion; }); + const { + isLoading: licenseAuditIsLoading, + isFinished: licenseAuditIsFinished, + invalidLicenses, + specialPermissions, + includedLicenses, + checkAndTriggerAudit: checkAndTriggerLicenseAudit, + } = useLicenseAudit(props.channel.id, currentChannelVersion); + + const allSpecialPermissionsChecked = ref(true); + + watch(specialPermissions, (newVal) => { + if (newVal && newVal.length > 0) { + allSpecialPermissionsChecked.value = false; + } else { + allSpecialPermissionsChecked.value = true; + } + }, { immediate: true }); + + const hasInvalidLicenses = computed(() => { + return invalidLicenses.value && invalidLicenses.value.length > 0; + }); + + const canBeSubmitted = computed(() => { + if (isPublishing.value) return false; + if (hasInvalidLicenses.value) return false; + if (!licenseAuditIsFinished.value) return false; + + const baseCondition = + canBeEdited.value && publishedDataIsFinished.value && description.value.length >= 1; + + if (needsReplacementConfirmation.value) { + return baseCondition && replacementConfirmed.value && allSpecialPermissionsChecked.value; + } + + return baseCondition && allSpecialPermissionsChecked.value; + }); + const latestPublishedData = computed(() => { if (!publishedData.value || !displayedVersion.value) return undefined; return publishedData.value[displayedVersion.value]; @@ -407,6 +466,7 @@ watch(isPublishing, async (newIsPublishing, oldIsPublishing) => { if (oldIsPublishing === true && newIsPublishing === false) { await fetchPublishedData(); + await checkAndTriggerLicenseAudit(); } }); @@ -415,6 +475,7 @@ if (!isPublishing.value) { await fetchPublishedData(); + await checkAndTriggerLicenseAudit(); } }); @@ -520,6 +581,11 @@ publishedDataIsFinished, detectedLanguages, detectedCategories, + licenseAuditIsLoading, + licenseAuditIsFinished, + invalidLicenses, + specialPermissions, + includedLicenses, onSubmit, // Translation functions submitToCommunityLibrary$, @@ -541,6 +607,10 @@ isPublishing, publishingMessage$, confirmReplacementText$, + checkingChannelCompatibility$, + checkingChannelCompatibilitySecondary$, + checkedSpecialPermissions, + allSpecialPermissionsChecked, }; }, props: { @@ -645,6 +715,35 @@ color: v-bind('infoTextColor'); } + .license-audit-loader { + display: flex; + flex-direction: row; + gap: 12px; + align-items: flex-start; + width: 100%; + padding: 16px 0; + } + + .audit-text-wrapper { + display: flex; + flex-direction: column; + gap: 4px; + flex: 1; + } + + .audit-text-primary { + font-size: 14px; + color: v-bind('infoTextColor'); + line-height: 140%; + } + + .audit-text-secondary { + font-size: 14px; + color: v-bind('infoTextColor'); + opacity: 0.7; + line-height: 140%; + } + .info-section { display: flex; flex-direction: column; diff --git a/contentcuration/contentcuration/frontend/shared/data/resources.js b/contentcuration/contentcuration/frontend/shared/data/resources.js index f04944080b..fba8787fd2 100644 --- a/contentcuration/contentcuration/frontend/shared/data/resources.js +++ b/contentcuration/contentcuration/frontend/shared/data/resources.js @@ -116,7 +116,7 @@ export function formatUUID4(uuid) { function mix(...mixins) { // Inherit from the last class to allow constructor inheritance - class Mix extends mixins.slice(-1)[0] {} + class Mix extends mixins.slice(-1)[0] { } // Programmatically add all the methods and accessors // of the mixins to class Mix. @@ -581,10 +581,10 @@ class IndexedDBResource { results: results.slice(0, maxResults), more: hasMore ? { - ...params, - // Dynamically set the pagination cursor based on the pagination field and operator. - [`${paginationField}__${operator}`]: results[maxResults - 1][paginationField], - } + ...params, + // Dynamically set the pagination cursor based on the pagination field and operator. + [`${paginationField}__${operator}`]: results[maxResults - 1][paginationField], + } : null, }; } @@ -1416,6 +1416,11 @@ export const Channel = new CreateModelResource({ const response = await client.get(window.Urls.channel_published_data(id)); return response.data; }, + auditLicenses(id) { + return client.post(window.Urls.channel_audit_licenses(id)).then(response => { + return response.data; + }); + }, }); function getChannelFromChannelScope() { @@ -2411,3 +2416,12 @@ export const CommunityLibrarySubmission = new APIResource({ }); }, }); + +export const AuditedSpecialPermissionsLicense = new APIResource({ + urlName: 'audited_special_permissions_license', + fetchCollection(params) { + return client.get(this.collectionUrl(), { params }).then(response => { + return response.data || []; + }); + }, +}); diff --git a/contentcuration/contentcuration/frontend/shared/strings/communityChannelsStrings.js b/contentcuration/contentcuration/frontend/shared/strings/communityChannelsStrings.js index 24a82e5126..00666132a4 100644 --- a/contentcuration/contentcuration/frontend/shared/strings/communityChannelsStrings.js +++ b/contentcuration/contentcuration/frontend/shared/strings/communityChannelsStrings.js @@ -237,4 +237,46 @@ export const communityChannelsStrings = createTranslator('CommunityChannelsStrin message: 'Dismiss', context: 'Action in the resubmit modal to dismiss the modal', }, + checkingChannelCompatibility: { + message: 'Checking channel compatibility for submission...', + context: + 'Message shown in the "Submit to Community Library" panel while the license audit is in progress', + }, + checkingChannelCompatibilitySecondary: { + message: 'This usually takes a few seconds...', + context: + 'Secondary message shown below the main checking message to indicate the expected duration', + }, + licenseCheckPassed: { + message: 'License check passed', + context: 'Title shown when license audit passes (no invalid licenses found)', + }, + allLicensesCompatible: { + message: 'All licenses are compatible with Community Library.', + context: + 'Message shown after listing compatible licenses when license check passes', + }, + incompatibleLicensesDetected: { + message: 'Incompatible license(s) detected', + context: 'Title shown when invalid licenses are detected in the channel', + }, + channelCannotBeDistributed: { + message: 'this channel cannot be distributed via Kolibri.', + context: + 'Message explaining that channels with incompatible licenses cannot be distributed', + }, + fixLicensingBeforeSubmission: { + message: 'Please fix licensing before submitting a new version.', + context: + 'Call to action message when incompatible licenses are detected', + }, + specialPermissionsDetected: { + message: 'Special Permissions license(s) detected', + context: 'Title shown when special permissions licenses are detected in the channel', + }, + confirmDistributionRights: { + message: 'Please confirm you have the right to distribute this content via Kolibri.', + context: + 'Message asking user to confirm they have distribution rights for special permissions content', + }, }); diff --git a/contentcuration/contentcuration/urls.py b/contentcuration/contentcuration/urls.py index 7f47857f8e..5bd9deb355 100644 --- a/contentcuration/contentcuration/urls.py +++ b/contentcuration/contentcuration/urls.py @@ -34,6 +34,9 @@ import contentcuration.views.zip as zip_views from contentcuration.views import pwa from contentcuration.viewsets.assessmentitem import AssessmentItemViewSet +from contentcuration.viewsets.audited_special_permissions_license import ( + AuditedSpecialPermissionsLicenseViewSet, +) from contentcuration.viewsets.bookmark import BookmarkViewSet from contentcuration.viewsets.channel import AdminChannelViewSet from contentcuration.viewsets.channel import CatalogViewSet @@ -103,6 +106,11 @@ def get_redirect_url(self, *args, **kwargs): AdminCommunityLibrarySubmissionViewSet, basename="admin-community-library-submission", ) +router.register( + r"audited-special-permissions-license", + AuditedSpecialPermissionsLicenseViewSet, + basename="audited-special-permissions-license", +) urlpatterns = [ re_path(r"^api/", include(router.urls)), diff --git a/contentcuration/contentcuration/viewsets/audited_special_permissions_license.py b/contentcuration/contentcuration/viewsets/audited_special_permissions_license.py new file mode 100644 index 0000000000..e61bdceba0 --- /dev/null +++ b/contentcuration/contentcuration/viewsets/audited_special_permissions_license.py @@ -0,0 +1,57 @@ +from django_filters.rest_framework import BooleanFilter +from django_filters.rest_framework import CharFilter +from django_filters.rest_framework import DjangoFilterBackend +from django_filters.rest_framework import FilterSet +from rest_framework.permissions import IsAuthenticated + +from contentcuration.viewsets.base import ReadOnlyValuesViewset +from contentcuration.models import AuditedSpecialPermissionsLicense + + +class AuditedSpecialPermissionsLicenseFilter(FilterSet): + """ + Filter for AuditedSpecialPermissionsLicense viewset. + Supports filtering by IDs and distributable status. + """ + + by_ids = CharFilter(method="filter_by_ids") + distributable = BooleanFilter() + + def filter_by_ids(self, queryset, name, value): + + try: + id_list = [uuid.strip() for uuid in value.split(",")[:50]] + return queryset.filter(id__in=id_list) + except (ValueError, AttributeError): + return queryset.none() + + class Meta: + model = None + fields = ("by_ids", "distributable") + + def __init__(self, *args, **kwargs): + + self.Meta.model = AuditedSpecialPermissionsLicense + super().__init__(*args, **kwargs) + + +class AuditedSpecialPermissionsLicenseViewSet(ReadOnlyValuesViewset): + """ + Read-only viewset for AuditedSpecialPermissionsLicense. + Allows filtering by IDs and distributable status. + """ + + permission_classes = [IsAuthenticated] + filter_backends = [DjangoFilterBackend] + filterset_class = AuditedSpecialPermissionsLicenseFilter + + values = ( + "id", + "description", + "distributable", + ) + + def get_queryset(self): + + return AuditedSpecialPermissionsLicense.objects.all() + From e2601ef4ad993b249f0240abf34e78bd4639210e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Thu, 20 Nov 2025 00:01:32 +0000 Subject: [PATCH 02/13] [pre-commit.ci lite] apply automatic fixes --- .../LicenseStatus.vue | 46 +++++++------ .../SpecialPermissionsList.vue | 65 ++++++++++--------- .../composables/useLicenseAudit.js | 7 +- .../composables/useLicenseNames.js | 1 - .../composables/useSpecialPermissions.js | 4 -- .../index.vue | 39 ++++++----- .../frontend/shared/data/resources.js | 10 +-- .../strings/communityChannelsStrings.js | 9 +-- .../audited_special_permissions_license.py | 5 +- 9 files changed, 95 insertions(+), 91 deletions(-) diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/LicenseStatus.vue b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/LicenseStatus.vue index 6ec446461c..5c699d86ae 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/LicenseStatus.vue +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/LicenseStatus.vue @@ -1,4 +1,5 @@ + + diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/SpecialPermissionsList.vue b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/SpecialPermissionsList.vue index 2a1af758e8..32dbd47482 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/SpecialPermissionsList.vue +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/SpecialPermissionsList.vue @@ -1,4 +1,5 @@
+ + + + diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLicenseAudit.js b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLicenseAudit.js index ca6613a44a..c0cbad149c 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLicenseAudit.js +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLicenseAudit.js @@ -1,9 +1,9 @@ import { computed, onUnmounted, ref, unref } from 'vue'; -import { Channel } from 'shared/data/resources'; import { usePublishedData } from './usePublishedData'; +import { Channel } from 'shared/data/resources'; -const POLLING_INTERVAL_MS = 2000; -const MAX_POLLING_DURATION_MS = 5 * 60 * 1000; +const POLLING_INTERVAL_MS = 2000; +const MAX_POLLING_DURATION_MS = 5 * 60 * 1000; export function useLicenseAudit(channelId, channelVersion) { const { @@ -157,4 +157,3 @@ export function useLicenseAudit(channelId, channelVersion) { fetchPublishedData, }; } - diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLicenseNames.js b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLicenseNames.js index 33c7b30b02..66e8741ea6 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLicenseNames.js +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLicenseNames.js @@ -32,4 +32,3 @@ export function useLicenseNames(licenseIds) { error, }; } - diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useSpecialPermissions.js b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useSpecialPermissions.js index cba4067636..61e241a2fb 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useSpecialPermissions.js +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useSpecialPermissions.js @@ -29,8 +29,6 @@ export function useSpecialPermissions(permissionIds) { return; } - - isLoading.value = true; error.value = null; @@ -63,7 +61,6 @@ export function useSpecialPermissions(permissionIds) { }); permissions.value = flattenedPermissions; } catch (err) { - error.value = err; permissions.value = []; } finally { @@ -112,4 +109,3 @@ export function useSpecialPermissions(permissionIds) { previousPage, }; } - diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/index.vue b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/index.vue index ed4318d941..4c69da33cb 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/index.vue +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/index.vue @@ -150,9 +150,14 @@ :included-licenses="includedLicenses" />
@@ -238,6 +243,8 @@ import { useLicenseAudit } from './composables/useLicenseAudit'; import { usePublishedData } from './composables/usePublishedData'; + import LicenseStatus from './LicenseStatus.vue'; + import SpecialPermissionsList from './SpecialPermissionsList.vue'; import { translateMetadataString } from 'shared/utils/metadataStringsTranslation'; import countriesUtil from 'shared/utils/countries'; import { communityChannelsStrings } from 'shared/strings/communityChannelsStrings'; @@ -247,8 +254,6 @@ import CountryField from 'shared/views/form/CountryField'; import LanguagesMap from 'shared/leUtils/Languages'; import { CategoriesLookup, CommunityLibraryStatus } from 'shared/constants'; - import LicenseStatus from './LicenseStatus.vue'; - import SpecialPermissionsList from './SpecialPermissionsList.vue'; export default { name: 'SubmitToCommunityLibrarySidePanel', @@ -430,13 +435,17 @@ const allSpecialPermissionsChecked = ref(true); - watch(specialPermissions, (newVal) => { - if (newVal && newVal.length > 0) { - allSpecialPermissionsChecked.value = false; - } else { - allSpecialPermissionsChecked.value = true; - } - }, { immediate: true }); + watch( + specialPermissions, + newVal => { + if (newVal && newVal.length > 0) { + allSpecialPermissionsChecked.value = false; + } else { + allSpecialPermissionsChecked.value = true; + } + }, + { immediate: true }, + ); const hasInvalidLicenses = computed(() => { return invalidLicenses.value && invalidLicenses.value.length > 0; @@ -446,7 +455,7 @@ if (isPublishing.value) return false; if (hasInvalidLicenses.value) return false; if (!licenseAuditIsFinished.value) return false; - + const baseCondition = canBeEdited.value && publishedDataIsFinished.value && description.value.length >= 1; @@ -726,22 +735,22 @@ .audit-text-wrapper { display: flex; + flex: 1; flex-direction: column; gap: 4px; - flex: 1; } .audit-text-primary { font-size: 14px; - color: v-bind('infoTextColor'); line-height: 140%; + color: v-bind('infoTextColor'); } .audit-text-secondary { font-size: 14px; + line-height: 140%; color: v-bind('infoTextColor'); opacity: 0.7; - line-height: 140%; } .info-section { diff --git a/contentcuration/contentcuration/frontend/shared/data/resources.js b/contentcuration/contentcuration/frontend/shared/data/resources.js index fba8787fd2..95c097acf1 100644 --- a/contentcuration/contentcuration/frontend/shared/data/resources.js +++ b/contentcuration/contentcuration/frontend/shared/data/resources.js @@ -116,7 +116,7 @@ export function formatUUID4(uuid) { function mix(...mixins) { // Inherit from the last class to allow constructor inheritance - class Mix extends mixins.slice(-1)[0] { } + class Mix extends mixins.slice(-1)[0] {} // Programmatically add all the methods and accessors // of the mixins to class Mix. @@ -581,10 +581,10 @@ class IndexedDBResource { results: results.slice(0, maxResults), more: hasMore ? { - ...params, - // Dynamically set the pagination cursor based on the pagination field and operator. - [`${paginationField}__${operator}`]: results[maxResults - 1][paginationField], - } + ...params, + // Dynamically set the pagination cursor based on the pagination field and operator. + [`${paginationField}__${operator}`]: results[maxResults - 1][paginationField], + } : null, }; } diff --git a/contentcuration/contentcuration/frontend/shared/strings/communityChannelsStrings.js b/contentcuration/contentcuration/frontend/shared/strings/communityChannelsStrings.js index 00666132a4..c83d116959 100644 --- a/contentcuration/contentcuration/frontend/shared/strings/communityChannelsStrings.js +++ b/contentcuration/contentcuration/frontend/shared/strings/communityChannelsStrings.js @@ -253,8 +253,7 @@ export const communityChannelsStrings = createTranslator('CommunityChannelsStrin }, allLicensesCompatible: { message: 'All licenses are compatible with Community Library.', - context: - 'Message shown after listing compatible licenses when license check passes', + context: 'Message shown after listing compatible licenses when license check passes', }, incompatibleLicensesDetected: { message: 'Incompatible license(s) detected', @@ -262,13 +261,11 @@ export const communityChannelsStrings = createTranslator('CommunityChannelsStrin }, channelCannotBeDistributed: { message: 'this channel cannot be distributed via Kolibri.', - context: - 'Message explaining that channels with incompatible licenses cannot be distributed', + context: 'Message explaining that channels with incompatible licenses cannot be distributed', }, fixLicensingBeforeSubmission: { message: 'Please fix licensing before submitting a new version.', - context: - 'Call to action message when incompatible licenses are detected', + context: 'Call to action message when incompatible licenses are detected', }, specialPermissionsDetected: { message: 'Special Permissions license(s) detected', diff --git a/contentcuration/contentcuration/viewsets/audited_special_permissions_license.py b/contentcuration/contentcuration/viewsets/audited_special_permissions_license.py index e61bdceba0..d6db7a313d 100644 --- a/contentcuration/contentcuration/viewsets/audited_special_permissions_license.py +++ b/contentcuration/contentcuration/viewsets/audited_special_permissions_license.py @@ -4,8 +4,8 @@ from django_filters.rest_framework import FilterSet from rest_framework.permissions import IsAuthenticated -from contentcuration.viewsets.base import ReadOnlyValuesViewset from contentcuration.models import AuditedSpecialPermissionsLicense +from contentcuration.viewsets.base import ReadOnlyValuesViewset class AuditedSpecialPermissionsLicenseFilter(FilterSet): @@ -18,7 +18,7 @@ class AuditedSpecialPermissionsLicenseFilter(FilterSet): distributable = BooleanFilter() def filter_by_ids(self, queryset, name, value): - + try: id_list = [uuid.strip() for uuid in value.split(",")[:50]] return queryset.filter(id__in=id_list) @@ -54,4 +54,3 @@ class AuditedSpecialPermissionsLicenseViewSet(ReadOnlyValuesViewset): def get_queryset(self): return AuditedSpecialPermissionsLicense.objects.all() - From 6dfe37b716217687893435e8999ada894e11775a Mon Sep 17 00:00:00 2001 From: taoerman Date: Wed, 19 Nov 2025 16:10:27 -0800 Subject: [PATCH 03/13] fix linting --- .../SpecialPermissionsList.vue | 1 - .../SubmitToCommunityLibrarySidePanel.spec.js | 13 +++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/SpecialPermissionsList.vue b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/SpecialPermissionsList.vue index 32dbd47482..d467690d5e 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/SpecialPermissionsList.vue +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/SpecialPermissionsList.vue @@ -116,7 +116,6 @@ currentPagePermissions, isLoading, checkedIds: computed(() => props.modelValue), - allChecked, currentPage, totalPages, togglePermission, diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/__tests__/SubmitToCommunityLibrarySidePanel.spec.js b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/__tests__/SubmitToCommunityLibrarySidePanel.spec.js index 5d3d907f88..e876543b54 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/__tests__/SubmitToCommunityLibrarySidePanel.spec.js +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/__tests__/SubmitToCommunityLibrarySidePanel.spec.js @@ -8,6 +8,7 @@ import StatusChip from '../StatusChip.vue'; import { usePublishedData } from '../composables/usePublishedData'; import { useLatestCommunityLibrarySubmission } from '../composables/useLatestCommunityLibrarySubmission'; +import { useLicenseAudit } from '../composables/useLicenseAudit'; import { Categories, CommunityLibraryStatus } from 'shared/constants'; import { communityChannelsStrings } from 'shared/strings/communityChannelsStrings'; import { CommunityLibrarySubmission } from 'shared/data/resources'; @@ -19,6 +20,9 @@ jest.mock('../composables/usePublishedData', () => ({ jest.mock('../composables/useLatestCommunityLibrarySubmission', () => ({ useLatestCommunityLibrarySubmission: jest.fn(), })); +jest.mock('../composables/useLicenseAudit', () => ({ + useLicenseAudit: jest.fn(), +})); jest.mock('shared/data/resources', () => ({ CommunityLibrarySubmission: { create: jest.fn(() => Promise.resolve()), @@ -57,6 +61,15 @@ async function makeWrapper({ channel, publishedData, latestSubmission }) { fetchData: fetchLatestSubmission, }); + useLicenseAudit.mockReturnValue({ + isLoading: ref(false), + isFinished: ref(true), + invalidLicenses: ref([]), + specialPermissions: ref([]), + includedLicenses: ref([]), + checkAndTriggerAudit: jest.fn(), + }); + const wrapper = mount(SubmitToCommunityLibrarySidePanel, { store, propsData: { From ce62b6f9392418b95444fba40304f90127cf1ee5 Mon Sep 17 00:00:00 2001 From: taoerman Date: Fri, 21 Nov 2025 22:32:37 -0800 Subject: [PATCH 04/13] fix bug --- contentcuration/contentcuration/celery.py | 1 - .../LicenseStatus.vue | 50 +++----- .../SpecialPermissionsList.vue | 58 ++++----- .../composables/useLicenseAudit.js | 113 +++++++----------- .../composables/useLicenseNames.js | 43 ++----- .../composables/useSpecialPermissions.js | 32 +++-- .../index.vue | 21 +--- .../strings/communityChannelsStrings.js | 8 ++ .../audited_special_permissions_license.py | 16 +-- 9 files changed, 135 insertions(+), 207 deletions(-) diff --git a/contentcuration/contentcuration/celery.py b/contentcuration/contentcuration/celery.py index 5901d5d4be..9f74f2d2fc 100644 --- a/contentcuration/contentcuration/celery.py +++ b/contentcuration/contentcuration/celery.py @@ -12,4 +12,3 @@ # of setting it as an attribute on our custom Celery class app = CeleryApp("contentcuration", task_cls=CeleryTask) app.config_from_object(settings.CELERY) -app.autodiscover_tasks(["contentcuration"]) diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/LicenseStatus.vue b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/LicenseStatus.vue index 5c699d86ae..4ece6c713b 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/LicenseStatus.vue +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/LicenseStatus.vue @@ -2,7 +2,6 @@ @@ -59,22 +61,22 @@ @@ -169,8 +164,8 @@ flex-direction: column; gap: 12px; padding: 16px; - background-color: v-bind('paletteTheme.grey.v_100'); - border: 1px solid v-bind('paletteTheme.grey.v_200'); + background-color: v-bind('$themePalette.grey.v_100'); + border: 1px solid v-bind('$themePalette.grey.v_200'); border-radius: 4px; } @@ -192,11 +187,6 @@ color: v-bind('$themeTokens.text'); } - .nav-button { - color: v-bind('paletteTheme.grey.v_700') !important; - text-decoration: none; - } - .loader-wrapper { display: flex; justify-content: center; diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLicenseAudit.js b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLicenseAudit.js index c0cbad149c..7cdbbc7487 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLicenseAudit.js +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLicenseAudit.js @@ -1,26 +1,31 @@ -import { computed, onUnmounted, ref, unref } from 'vue'; -import { usePublishedData } from './usePublishedData'; +import { computed, ref, unref, watch } from 'vue'; import { Channel } from 'shared/data/resources'; -const POLLING_INTERVAL_MS = 2000; -const MAX_POLLING_DURATION_MS = 5 * 60 * 1000; - -export function useLicenseAudit(channelId, channelVersion) { - const { - isLoading: publishedDataIsLoading, - isFinished: publishedDataIsFinished, - data: publishedData, - fetchData: fetchPublishedData, - } = usePublishedData(channelId); - +export function useLicenseAudit(channelRef, channelVersionRef) { const isAuditing = ref(false); const auditTaskId = ref(null); - const pollingInterval = ref(null); - const pollingStartTime = ref(null); const auditError = ref(null); + const publishedData = ref(null); + + // Watch for changes to the channel's published_data + // This will automatically update when the backend audit completes + watch( + () => unref(channelRef)?.published_data, + newPublishedData => { + if (newPublishedData) { + publishedData.value = newPublishedData; + // If we were auditing and now have data, we're done + if (isAuditing.value) { + isAuditing.value = false; + auditError.value = null; + } + } + }, + { immediate: true, deep: true }, + ); const currentVersionData = computed(() => { - const version = unref(channelVersion); + const version = unref(channelVersionRef); if (!publishedData.value || version == null) { return undefined; } @@ -58,52 +63,10 @@ export function useLicenseAudit(channelId, channelVersion) { return versionData.included_licenses; }); - const isAuditInProgress = computed(() => { - if (isAuditComplete.value) return false; - return isAuditing.value || pollingInterval.value !== null; - }); - const isAuditComplete = computed(() => { - return publishedDataIsFinished.value && hasAuditData.value; + return publishedData.value !== null && hasAuditData.value; }); - function stopPolling() { - if (pollingInterval.value) { - clearInterval(pollingInterval.value); - pollingInterval.value = null; - } - pollingStartTime.value = null; - } - - function startPolling() { - if (pollingInterval.value) return; - - pollingStartTime.value = Date.now(); - - pollingInterval.value = setInterval(async () => { - if (Date.now() - pollingStartTime.value > MAX_POLLING_DURATION_MS) { - stopPolling(); - auditError.value = new Error('Audit timeout: Maximum polling duration exceeded'); - isAuditing.value = false; - return; - } - - try { - await fetchPublishedData(); - - if (hasAuditData.value) { - stopPolling(); - isAuditing.value = false; - auditError.value = null; - } - } catch (error) { - stopPolling(); - isAuditing.value = false; - auditError.value = error; - } - }, POLLING_INTERVAL_MS); - } - async function triggerAudit() { if (isAuditing.value) return; @@ -111,10 +74,16 @@ export function useLicenseAudit(channelId, channelVersion) { isAuditing.value = true; auditError.value = null; + const channelId = unref(channelRef)?.id; + if (!channelId) { + throw new Error('Channel ID is required to trigger audit'); + } + const response = await Channel.auditLicenses(channelId); auditTaskId.value = response.task_id; - startPolling(); + // No need to poll - the channel's published_data will update automatically + // when the backend audit completes } catch (error) { isAuditing.value = false; auditError.value = error; @@ -122,8 +91,22 @@ export function useLicenseAudit(channelId, channelVersion) { } } + async function fetchPublishedData() { + const channelId = unref(channelRef)?.id; + if (!channelId) return; + + try { + const data = await Channel.getPublishedData(channelId); + publishedData.value = data; + } catch (error) { + auditError.value = error; + throw error; + } + } + async function checkAndTriggerAudit() { - if (!publishedDataIsFinished.value) { + // Fetch published data if we don't have it yet + if (!publishedData.value) { await fetchPublishedData(); } @@ -134,17 +117,13 @@ export function useLicenseAudit(channelId, channelVersion) { await triggerAudit(); } - onUnmounted(() => { - stopPolling(); - }); - return { isLoading: computed(() => { if (isAuditComplete.value || auditError.value) return false; - return publishedDataIsLoading.value || isAuditInProgress.value; + return isAuditing.value; }), isFinished: computed(() => isAuditComplete.value), - isAuditing: isAuditInProgress, + isAuditing, invalidLicenses, specialPermissions, includedLicenses, diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLicenseNames.js b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLicenseNames.js index 66e8741ea6..bc2cba81fd 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLicenseNames.js +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLicenseNames.js @@ -1,34 +1,15 @@ -import { computed, ref } from 'vue'; import { findLicense } from 'shared/utils/helpers'; -export function useLicenseNames(licenseIds) { - const isLoading = ref(false); - const error = ref(null); - - const licenseNames = computed(() => { - if (!licenseIds.value || licenseIds.value.length === 0) { - return []; - } - - return licenseIds.value - .map(id => { - const license = findLicense(id); - return license?.license_name || null; - }) - .filter(name => name !== null); - }); - - const formattedLicenseNames = computed(() => { - const names = licenseNames.value; - if (names.length === 0) return ''; - if (names.length === 1) return names[0]; - return names.join(', '); - }); - - return { - licenseNames, - formattedLicenseNames, - isLoading, - error, - }; +export function formatLicenseNames(licenseIds) { + if (!licenseIds || !Array.isArray(licenseIds) || licenseIds.length === 0) { + return ''; + } + + return licenseIds + .map(id => { + const license = findLicense(id); + return license?.license_name || null; + }) + .filter(name => name !== null) + .join(', '); } diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useSpecialPermissions.js b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useSpecialPermissions.js index 61e241a2fb..bcb52b0f9e 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useSpecialPermissions.js +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useSpecialPermissions.js @@ -1,4 +1,4 @@ -import { computed, ref, unref, watchEffect } from 'vue'; +import { computed, ref, unref, watch } from 'vue'; import { AuditedSpecialPermissionsLicense } from 'shared/data/resources'; const ITEMS_PER_PAGE = 5; @@ -9,18 +9,14 @@ export function useSpecialPermissions(permissionIds) { const error = ref(null); const currentPage = ref(1); - const nonDistributablePermissions = computed(() => { - return permissions.value.filter(p => !p.distributable); - }); - const totalPages = computed(() => { - return Math.ceil(nonDistributablePermissions.value.length / ITEMS_PER_PAGE); + return Math.ceil(permissions.value.length / ITEMS_PER_PAGE); }); const currentPagePermissions = computed(() => { const start = (currentPage.value - 1) * ITEMS_PER_PAGE; const end = start + ITEMS_PER_PAGE; - return nonDistributablePermissions.value.slice(start, end); + return permissions.value.slice(start, end); }); async function fetchPermissions(ids) { @@ -88,18 +84,20 @@ export function useSpecialPermissions(permissionIds) { return Array.isArray(ids) ? ids : [ids]; }); - watchEffect(() => { - const ids = resolvedPermissionIds.value; - - if (ids.length === 0) { - permissions.value = []; - return; - } - fetchPermissions(ids); - }); + watch( + resolvedPermissionIds, + ids => { + if (ids.length === 0) { + permissions.value = []; + return; + } + fetchPermissions(ids); + }, + { immediate: true }, + ); return { - permissions: nonDistributablePermissions, + permissions, currentPagePermissions, isLoading, error, diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/index.vue b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/index.vue index 4c69da33cb..75de56676b 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/index.vue +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/index.vue @@ -231,7 +231,7 @@ + diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/InvalidLicensesNotice.vue b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/InvalidLicensesNotice.vue new file mode 100644 index 0000000000..5b7de0199e --- /dev/null +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/InvalidLicensesNotice.vue @@ -0,0 +1,57 @@ + + + + + diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/LicenseStatus.vue b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/LicenseStatus.vue deleted file mode 100644 index cfc82e96e1..0000000000 --- a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/LicenseStatus.vue +++ /dev/null @@ -1,97 +0,0 @@ - - - - diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/SpecialPermissionsList.vue b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/SpecialPermissionsList.vue index d0b9a438e6..ae2ea4b63c 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/SpecialPermissionsList.vue +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/SpecialPermissionsList.vue @@ -40,9 +40,11 @@ icon="chevronLeft" @click="previousPage" > - {{ previousPage$() }} + {{ previousPageAction$() }} - {{ currentPage }} of {{ totalPages }} + + {{ pageIndicator$({ currentPage, totalPages }) }} + - {{ nextPage$() }} + {{ nextPageAction$() }}
@@ -61,18 +63,21 @@ - diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/InvalidLicensesNotice.vue b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/InvalidLicensesNotice.vue index 5b7de0199e..5e92cdb8c5 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/InvalidLicensesNotice.vue +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/InvalidLicensesNotice.vue @@ -27,17 +27,9 @@ components: { Box, }, - props: { - invalidLicenses: { - type: Array, - required: true, - }, - }, setup(props) { - const { - incompatibleLicensesDetected$, - incompatibleLicensesDescription$, - } = communityChannelsStrings; + const { incompatibleLicensesDetected$, incompatibleLicensesDescription$ } = + communityChannelsStrings; const invalidLicenseNames = computed(() => formatLicenseNames(props.invalidLicenses)); const descriptionText = computed(() => { @@ -51,7 +43,12 @@ descriptionText, }; }, + props: { + invalidLicenses: { + type: Array, + required: true, + }, + }, }; - diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/__mocks__/useLatestCommunityLibrarySubmission.js b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/__mocks__/useLatestCommunityLibrarySubmission.js index 4eae3158d6..d51326620f 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/__mocks__/useLatestCommunityLibrarySubmission.js +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/__mocks__/useLatestCommunityLibrarySubmission.js @@ -14,7 +14,8 @@ function useLatestCommunityLibrarySubmissionMock(overrides = {}) { }; } -const useLatestCommunityLibrarySubmission = jest.fn(() => useLatestCommunityLibrarySubmissionMock()); +const useLatestCommunityLibrarySubmission = jest.fn(() => + useLatestCommunityLibrarySubmissionMock(), +); module.exports = { useLatestCommunityLibrarySubmission, useLatestCommunityLibrarySubmissionMock }; - diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/__mocks__/useLicenseAudit.js b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/__mocks__/useLicenseAudit.js index e6fae1f9bb..b64d91141f 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/__mocks__/useLicenseAudit.js +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/__mocks__/useLicenseAudit.js @@ -25,4 +25,3 @@ function useLicenseAuditMock(overrides = {}) { const useLicenseAudit = jest.fn(() => useLicenseAuditMock()); module.exports = { useLicenseAudit, useLicenseAuditMock }; - diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/__mocks__/usePublishedData.js b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/__mocks__/usePublishedData.js index bc4c7cc5d2..b70933e65a 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/__mocks__/usePublishedData.js +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/__mocks__/usePublishedData.js @@ -17,4 +17,3 @@ function usePublishedDataMock(overrides = {}) { const usePublishedData = jest.fn(() => usePublishedDataMock()); module.exports = { usePublishedData, usePublishedDataMock }; - diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLicenseNames.js b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLicenseNames.js index e2261ee9ed..82f01f016a 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLicenseNames.js +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLicenseNames.js @@ -19,12 +19,12 @@ export function formatLicenseNames(licenseIds, options = {}) { .map(id => { const license = findLicense(id); const licenseName = license?.license_name; - + // Exclude licenses specified in the excludes option if (!licenseName || excludes.includes(licenseName)) { return null; } - + // Translate the license name return constantStrings.$tr(licenseName); }) diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/index.vue b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/index.vue index 3a8b7311f2..6d327dd186 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/index.vue +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/index.vue @@ -145,11 +145,17 @@
Date: Mon, 1 Dec 2025 09:48:24 -0800 Subject: [PATCH 10/13] fix linting --- .../composables/useLicenseNames.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLicenseNames.js b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLicenseNames.js index 82f01f016a..078acde17e 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLicenseNames.js +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLicenseNames.js @@ -5,7 +5,8 @@ import { constantStrings } from 'shared/mixins'; * Formats and translates license names from license IDs * @param {Array} licenseIds - Array of license IDs * @param {Object} options - Optional configuration - * @param {Array} options.excludes - Array of license names to exclude (e.g., ['Special Permissions']) + * @param {Array} options.excludes - Array of license names to exclude + * (e.g., ['Special Permissions']) * @returns {string} Comma-separated string of translated license names */ export function formatLicenseNames(licenseIds, options = {}) { @@ -19,13 +20,11 @@ export function formatLicenseNames(licenseIds, options = {}) { .map(id => { const license = findLicense(id); const licenseName = license?.license_name; - - // Exclude licenses specified in the excludes option + if (!licenseName || excludes.includes(licenseName)) { return null; } - - // Translate the license name + return constantStrings.$tr(licenseName); }) .filter(name => name !== null && name !== undefined && name !== '') From 778b79cbbbbb40c0ed35000eb907da9b3ce3317c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 17:50:58 +0000 Subject: [PATCH 11/13] [pre-commit.ci lite] apply automatic fixes --- .../composables/useLicenseNames.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLicenseNames.js b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLicenseNames.js index 078acde17e..14b6d06254 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLicenseNames.js +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLicenseNames.js @@ -20,11 +20,11 @@ export function formatLicenseNames(licenseIds, options = {}) { .map(id => { const license = findLicense(id); const licenseName = license?.license_name; - + if (!licenseName || excludes.includes(licenseName)) { return null; } - + return constantStrings.$tr(licenseName); }) .filter(name => name !== null && name !== undefined && name !== '') From 5d929eac7d150faf1f0f419ab8e93dd2e82d8cd4 Mon Sep 17 00:00:00 2001 From: taoerman Date: Tue, 2 Dec 2025 14:33:17 -0800 Subject: [PATCH 12/13] fix code --- .../SubmitToCommunityLibrarySidePanel.spec.js | 9 --- .../useLatestCommunityLibrarySubmission.js | 8 +-- .../composables/__mocks__/useLicenseAudit.js | 8 +-- .../composables/__mocks__/usePublishedData.js | 8 +-- .../composables/useLicenseAudit.js | 9 +-- .../composables/useLicenseNames.js | 32 ---------- .../composables/useSpecialPermissions.js | 29 ++------- .../index.vue | 64 +++++++++++-------- .../CompatibleLicensesNotice.vue | 4 +- .../InvalidLicensesNotice.vue | 4 +- .../SpecialPermissionsList.vue | 12 ++-- .../frontend/shared/utils/helpers.js | 31 +++++++++ 12 files changed, 95 insertions(+), 123 deletions(-) delete mode 100644 contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLicenseNames.js rename contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/{ => licenseCheck}/CompatibleLicensesNotice.vue (91%) rename contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/{ => licenseCheck}/InvalidLicensesNotice.vue (91%) rename contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/{ => licenseCheck}/SpecialPermissionsList.vue (95%) diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/__tests__/SubmitToCommunityLibrarySidePanel.spec.js b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/__tests__/SubmitToCommunityLibrarySidePanel.spec.js index 960af6e863..49801549e0 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/__tests__/SubmitToCommunityLibrarySidePanel.spec.js +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/__tests__/SubmitToCommunityLibrarySidePanel.spec.js @@ -55,15 +55,6 @@ async function makeWrapper({ channel, publishedData, latestSubmission }) { fetchData: fetchLatestSubmission, }); - useLicenseAudit.mockReturnValue({ - isLoading: ref(false), - isFinished: ref(true), - invalidLicenses: ref([]), - specialPermissions: ref([]), - includedLicenses: ref([]), - checkAndTriggerAudit: jest.fn(), - }); - const wrapper = mount(SubmitToCommunityLibrarySidePanel, { store, propsData: { diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/__mocks__/useLatestCommunityLibrarySubmission.js b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/__mocks__/useLatestCommunityLibrarySubmission.js index d51326620f..5cc223bcfc 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/__mocks__/useLatestCommunityLibrarySubmission.js +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/__mocks__/useLatestCommunityLibrarySubmission.js @@ -1,4 +1,4 @@ -const { computed, ref } = require('vue'); +import { computed, ref } from 'vue'; const MOCK_DEFAULTS = { isLoading: ref(true), @@ -7,15 +7,13 @@ const MOCK_DEFAULTS = { fetchData: jest.fn(() => Promise.resolve()), }; -function useLatestCommunityLibrarySubmissionMock(overrides = {}) { +export function useLatestCommunityLibrarySubmissionMock(overrides = {}) { return { ...MOCK_DEFAULTS, ...overrides, }; } -const useLatestCommunityLibrarySubmission = jest.fn(() => +export const useLatestCommunityLibrarySubmission = jest.fn(() => useLatestCommunityLibrarySubmissionMock(), ); - -module.exports = { useLatestCommunityLibrarySubmission, useLatestCommunityLibrarySubmissionMock }; diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/__mocks__/useLicenseAudit.js b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/__mocks__/useLicenseAudit.js index b64d91141f..a8d77b7526 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/__mocks__/useLicenseAudit.js +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/__mocks__/useLicenseAudit.js @@ -1,4 +1,4 @@ -const { computed, ref } = require('vue'); +import { computed, ref } from 'vue'; const MOCK_DEFAULTS = { isLoading: computed(() => false), @@ -15,13 +15,11 @@ const MOCK_DEFAULTS = { fetchPublishedData: jest.fn(), }; -function useLicenseAuditMock(overrides = {}) { +export function useLicenseAuditMock(overrides = {}) { return { ...MOCK_DEFAULTS, ...overrides, }; } -const useLicenseAudit = jest.fn(() => useLicenseAuditMock()); - -module.exports = { useLicenseAudit, useLicenseAuditMock }; +export const useLicenseAudit = jest.fn(() => useLicenseAuditMock()); diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/__mocks__/usePublishedData.js b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/__mocks__/usePublishedData.js index b70933e65a..8759458f73 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/__mocks__/usePublishedData.js +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/__mocks__/usePublishedData.js @@ -1,4 +1,4 @@ -const { computed, ref } = require('vue'); +import { computed, ref } from 'vue'; const MOCK_DEFAULTS = { isLoading: ref(true), @@ -7,13 +7,11 @@ const MOCK_DEFAULTS = { fetchData: jest.fn(() => Promise.resolve()), }; -function usePublishedDataMock(overrides = {}) { +export function usePublishedDataMock(overrides = {}) { return { ...MOCK_DEFAULTS, ...overrides, }; } -const usePublishedData = jest.fn(() => usePublishedDataMock()); - -module.exports = { usePublishedData, usePublishedDataMock }; +export const usePublishedData = jest.fn(() => usePublishedDataMock()); diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLicenseAudit.js b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLicenseAudit.js index 44aaba7b21..ea4cc092d4 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLicenseAudit.js +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLicenseAudit.js @@ -43,20 +43,17 @@ export function useLicenseAudit(channelRef, channelVersionRef) { const invalidLicenses = computed(() => { const versionData = currentVersionData.value; - if (!versionData) return undefined; - return versionData.community_library_invalid_licenses; + return versionData?.community_library_invalid_licenses || []; }); const specialPermissions = computed(() => { const versionData = currentVersionData.value; - if (!versionData) return undefined; - return versionData.community_library_special_permissions; + return versionData?.community_library_special_permissions || []; }); const includedLicenses = computed(() => { const versionData = currentVersionData.value; - if (!versionData) return undefined; - return versionData.included_licenses; + return versionData?.included_licenses || []; }); const isAuditComplete = computed(() => { diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLicenseNames.js b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLicenseNames.js deleted file mode 100644 index 14b6d06254..0000000000 --- a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useLicenseNames.js +++ /dev/null @@ -1,32 +0,0 @@ -import { findLicense } from 'shared/utils/helpers'; -import { constantStrings } from 'shared/mixins'; - -/** - * Formats and translates license names from license IDs - * @param {Array} licenseIds - Array of license IDs - * @param {Object} options - Optional configuration - * @param {Array} options.excludes - Array of license names to exclude - * (e.g., ['Special Permissions']) - * @returns {string} Comma-separated string of translated license names - */ -export function formatLicenseNames(licenseIds, options = {}) { - if (!licenseIds || !Array.isArray(licenseIds) || licenseIds.length === 0) { - return ''; - } - - const { excludes = [] } = options; - - return licenseIds - .map(id => { - const license = findLicense(id); - const licenseName = license?.license_name; - - if (!licenseName || excludes.includes(licenseName)) { - return null; - } - - return constantStrings.$tr(licenseName); - }) - .filter(name => name !== null && name !== undefined && name !== '') - .join(', '); -} diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useSpecialPermissions.js b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useSpecialPermissions.js index 30e882f1cd..e8338b5709 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useSpecialPermissions.js +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/composables/useSpecialPermissions.js @@ -1,7 +1,7 @@ import { computed, ref, unref, watch } from 'vue'; import { AuditedSpecialPermissionsLicense } from 'shared/data/resources'; -const ITEMS_PER_PAGE = 5; +const ITEMS_PER_PAGE = 3; /** * Composable that fetches and paginates audited special-permissions licenses @@ -54,28 +54,11 @@ export function useSpecialPermissions(permissionIds) { distributable: false, }); - const flattenedPermissions = []; - response.forEach(permission => { - const sentences = permission.description - .split('.') - .map(s => s.trim()) - .filter(s => s.length > 0); - - sentences.forEach((sentence, index) => { - let text = sentence; - if (!text.endsWith('.')) { - text += '.'; - } - - flattenedPermissions.push({ - id: `${permission.id}-${index}`, - originalId: permission.id, - description: text, - distributable: permission.distributable, - }); - }); - }); - permissions.value = flattenedPermissions; + permissions.value = response.map(permission => ({ + id: permission.id, + description: permission.description, + distributable: permission.distributable, + })); } catch (err) { error.value = err; permissions.value = []; diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/index.vue b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/index.vue index 6d327dd186..550e5c3c8b 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/index.vue +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/index.vue @@ -112,26 +112,34 @@ }) }} -
@@ -145,21 +153,15 @@
import { computed } from 'vue'; - import Box from './Box.vue'; - import { formatLicenseNames } from './composables/useLicenseNames'; + import Box from '../Box.vue'; + import { formatLicenseNames } from 'shared/utils/helpers'; import { communityChannelsStrings } from 'shared/strings/communityChannelsStrings'; export default { diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/InvalidLicensesNotice.vue b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/licenseCheck/InvalidLicensesNotice.vue similarity index 91% rename from contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/InvalidLicensesNotice.vue rename to contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/licenseCheck/InvalidLicensesNotice.vue index 5e92cdb8c5..b1ba5f3707 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/InvalidLicensesNotice.vue +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/licenseCheck/InvalidLicensesNotice.vue @@ -18,8 +18,8 @@