From 2c8a6e17e310e3fa024071ac380f99a07aef3bf9 Mon Sep 17 00:00:00 2001 From: taoerman Date: Tue, 28 Oct 2025 19:37:16 -0700 Subject: [PATCH 1/6] Info messages about previous submissions in create submission side panel --- .../SubmitToCommunityLibrarySidePanel/Box.vue | 52 +++- .../index.vue | 262 +++++++++++------- .../strings/communityChannelsStrings.js | 57 ++-- ...constraint_community_library_submission.py | 17 ++ contentcuration/contentcuration/models.py | 6 - .../contentcuration/tests/test_models.py | 54 ++++ 6 files changed, 315 insertions(+), 133 deletions(-) create mode 100644 contentcuration/contentcuration/migrations/0158_remove_unique_constraint_community_library_submission.py diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/Box.vue b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/Box.vue index 92782fe904..8f53e28b89 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/Box.vue +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/Box.vue @@ -17,7 +17,9 @@
- +
+ +
{ switch (props.kind) { case 'warning': - return paletteTheme.yellow.v_100; + return paletteTheme.red.v_100; case 'info': return paletteTheme.grey.v_100; default: throw new Error(`Unsupported box kind: ${props.kind}`); } }); - const boxTextColor = computed(() => { + const boxBorderColor = computed(() => { switch (props.kind) { case 'warning': - return paletteTheme.red.v_500; + return paletteTheme.red.v_300; case 'info': - return tokensTheme.text; + return 'transparent'; default: - throw new Error(`Unsupported box kind: ${props.kind}`); + return 'transparent'; } }); const icon = computed(() => { switch (props.kind) { case 'warning': - return 'warningIncomplete'; + return 'error'; case 'info': return 'infoOutline'; default: @@ -73,9 +75,19 @@ } }); + const warningFirstLineColor = computed(() => { + return props.kind === 'warning' ? paletteTheme.red.v_600 : tokensTheme.text; + }); + + const warningSecondLineColor = computed(() => { + return props.kind === 'warning' ? paletteTheme.grey.v_800 : tokensTheme.text; + }); + return { boxBackgroundColor, - boxTextColor, + boxBorderColor, + warningFirstLineColor, + warningSecondLineColor, icon, }; }, @@ -100,15 +112,16 @@ diff --git a/contentcuration/contentcuration/frontend/shared/strings/communityChannelsStrings.js b/contentcuration/contentcuration/frontend/shared/strings/communityChannelsStrings.js index 43762612be..53d32b5a40 100644 --- a/contentcuration/contentcuration/frontend/shared/strings/communityChannelsStrings.js +++ b/contentcuration/contentcuration/frontend/shared/strings/communityChannelsStrings.js @@ -70,7 +70,7 @@ export const communityChannelsStrings = createTranslator('CommunityChannelsStrin context: 'Error message when language selection is required', }, pendingStatus: { - message: 'Pending', + message: 'Submitted', context: 'Status indicating that an Community Library submission is pending', }, approvedStatus: { @@ -87,17 +87,14 @@ export const communityChannelsStrings = createTranslator('CommunityChannelsStrin context: 'The title of the "Submit to Community Library" panel', }, submittedPrimaryInfo: { - message: 'A previous version is still pending review.', + message: + 'A previous version is still pending review. Reviewers will see the latest submission first.', context: 'Information shown in the "Submit to Community Library" panel when a previous version is pending review', }, - reviewersWillSeeLatestFirst: { - message: 'Reviewers will see the latest submission first.', - context: - 'Information shown in the "Submit to Community Library" panel about how reviewers see submissions', - }, approvedPrimaryInfo: { - message: 'A previous version is live in the Community Library.', + message: + 'A previous version is live in the Community Library. Reviewers will see the latest submission first.', context: 'Information shown in the "Submit to Community Library" panel when a previous version is approved and live', }, @@ -113,12 +110,12 @@ export const communityChannelsStrings = createTranslator('CommunityChannelsStrin 'Information shown in the "Submit to Community Library" panel when there are no previous submissions', }, moreDetailsButton: { - message: 'More details about the Community Library', + message: 'More details', context: 'Button in the "Submit to Community Library" panel to show more details about the Community Library', }, lessDetailsButton: { - message: 'Show less', + message: 'Less details', context: 'Button in the "Submit to Community Library" panel to hide details about the Community Library', }, @@ -128,22 +125,36 @@ export const communityChannelsStrings = createTranslator('CommunityChannelsStrin context: 'Detailed description of the Community Library shown in the "Submit to Community Library" panel', }, - notPublishedWarning: { - message: - "This channel isn't published to Kolibri Studio yet. Publish first, then submit to the Community Library.", + notPublishedWarningFirstLine: { + message: "This channel isn't published to Kolibri Studio yet", context: - 'Warning shown in the "Submit to Community Library" panel when the channel is not published', + 'First line of warning shown in the "Submit to Community Library" panel when the channel is not published', }, - publicWarning: { - message: - 'This channel is currently public in the Content Library. It is not possible to submit public channels to the Community Library.', - context: 'Warning shown in the "Submit to Community Library" panel when the channel is public', + notPublishedWarningSecondLine: { + message: 'Publish to Studio first, then submit to the Community Library.', + context: + 'Second line of warning shown in the "Submit to Community Library" panel when the channel is not published', }, - alreadySubmittedWarning: { + publicWarningFirstLine: { + message: 'This channel is currently public in the Content Library.', + context: + 'First line of warning shown in the "Submit to Community Library" panel when the channel is public', + }, + publicWarningSecondLine: { + message: 'It is not possible to submit public channels to the Community Library.', + context: + 'Second line of warning shown in the "Submit to Community Library" panel when the channel is public', + }, + alreadySubmittedWarningFirstLine: { + message: 'This version of the channel has already been submitted to the Community Library.', + context: + 'First line of warning shown in the "Submit to Community Library" panel when the current version is already submitted', + }, + alreadySubmittedWarningSecondLine: { message: - 'This version of the channel has already been submitted to the Community Library. Please wait for review or make changes and publish a new version before submitting again.', + 'Please wait for review or make changes and publish a new version before submitting again.', context: - 'Warning shown in the "Submit to Community Library" panel when the current version of the channel is already submitted', + 'Second line of warning shown in the "Submit to Community Library" panel when the current version is already submitted', }, descriptionLabel: { message: "Describe what's new in this submission", @@ -189,4 +200,8 @@ export const communityChannelsStrings = createTranslator('CommunityChannelsStrin message: 'Categories', context: 'Label for detected categories in the "Submit to Community Library" panel', }, + confirmReplacementText: { + message: 'I understand this will replace my earlier submission on the review queue', + context: 'Checkbox text shown when there is a pending submission to confirm replacement', + }, }); diff --git a/contentcuration/contentcuration/migrations/0158_remove_unique_constraint_community_library_submission.py b/contentcuration/contentcuration/migrations/0158_remove_unique_constraint_community_library_submission.py new file mode 100644 index 0000000000..4a33508918 --- /dev/null +++ b/contentcuration/contentcuration/migrations/0158_remove_unique_constraint_community_library_submission.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.24 on 2025-10-28 21:35 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('contentcuration', '0157_merge_20251015_0333'), + ] + + operations = [ + migrations.RemoveConstraint( + model_name='communitylibrarysubmission', + name='unique_channel_with_channel_version', + ), + ] diff --git a/contentcuration/contentcuration/models.py b/contentcuration/contentcuration/models.py index 58de42a5b9..4b2e479064 100644 --- a/contentcuration/contentcuration/models.py +++ b/contentcuration/contentcuration/models.py @@ -2674,12 +2674,6 @@ def filter_edit_queryset(cls, queryset, user): return queryset.filter(author=user, channel__editors=user) class Meta: - constraints = [ - models.UniqueConstraint( - fields=["channel", "channel_version"], - name="unique_channel_with_channel_version", - ), - ] indexes = [ # Useful for cursor pagination models.Index(fields=["-date_created"], name="submission_date_created_idx"), diff --git a/contentcuration/contentcuration/tests/test_models.py b/contentcuration/contentcuration/tests/test_models.py index c04ffaae2b..705f47dc55 100644 --- a/contentcuration/contentcuration/tests/test_models.py +++ b/contentcuration/contentcuration/tests/test_models.py @@ -778,6 +778,60 @@ def test_mark_live(self, mock_ensure_db_exists_task_fetch_or_enqueue): community_library_submission.STATUS_LIVE, ) + def test_create_multiple_submissions_same_channel_same_version( + self, mock_ensure_db_exists_task_fetch_or_enqueue + ): + + channel = testdata.channel() + author = testdata.user() + channel.editors.add(author) + channel.version = 1 + channel.save() + + country = testdata.country() + + submission1 = CommunityLibrarySubmission.objects.create( + description="First submission", + channel=channel, + channel_version=1, + author=author, + categories=["test_category"], + status=community_library_submission.STATUS_PENDING, + ) + submission1.countries.add(country) + + submission2 = CommunityLibrarySubmission.objects.create( + description="Second submission", + channel=channel, + channel_version=1, + author=author, + categories=["test_category"], + status=community_library_submission.STATUS_PENDING, + ) + submission2.countries.add(country) + + submission3 = CommunityLibrarySubmission.objects.create( + description="Third submission", + channel=channel, + channel_version=1, + author=author, + categories=["test_category"], + status=community_library_submission.STATUS_PENDING, + ) + submission3.countries.add(country) + + self.assertEqual(submission1.channel, channel) + self.assertEqual(submission1.channel_version, 1) + self.assertEqual(submission2.channel, channel) + self.assertEqual(submission2.channel_version, 1) + self.assertEqual(submission3.channel, channel) + self.assertEqual(submission3.channel_version, 1) + + submissions = CommunityLibrarySubmission.objects.filter( + channel=channel, channel_version=1 + ) + self.assertEqual(submissions.count(), 3) + class AssessmentItemTestCase(PermissionQuerysetTestCase): @property From de532fc1e5fe265b23062ab9d1acd2cf2fb54cfe Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 02:55:43 +0000 Subject: [PATCH 2/6] [pre-commit.ci lite] apply automatic fixes --- ...emove_unique_constraint_community_library_submission.py | 7 +++---- contentcuration/contentcuration/tests/test_models.py | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/contentcuration/contentcuration/migrations/0158_remove_unique_constraint_community_library_submission.py b/contentcuration/contentcuration/migrations/0158_remove_unique_constraint_community_library_submission.py index 4a33508918..13c18d85d7 100644 --- a/contentcuration/contentcuration/migrations/0158_remove_unique_constraint_community_library_submission.py +++ b/contentcuration/contentcuration/migrations/0158_remove_unique_constraint_community_library_submission.py @@ -1,17 +1,16 @@ # Generated by Django 3.2.24 on 2025-10-28 21:35 - from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('contentcuration', '0157_merge_20251015_0333'), + ("contentcuration", "0157_merge_20251015_0333"), ] operations = [ migrations.RemoveConstraint( - model_name='communitylibrarysubmission', - name='unique_channel_with_channel_version', + model_name="communitylibrarysubmission", + name="unique_channel_with_channel_version", ), ] diff --git a/contentcuration/contentcuration/tests/test_models.py b/contentcuration/contentcuration/tests/test_models.py index 705f47dc55..44dc818ebe 100644 --- a/contentcuration/contentcuration/tests/test_models.py +++ b/contentcuration/contentcuration/tests/test_models.py @@ -781,7 +781,7 @@ def test_mark_live(self, mock_ensure_db_exists_task_fetch_or_enqueue): def test_create_multiple_submissions_same_channel_same_version( self, mock_ensure_db_exists_task_fetch_or_enqueue ): - + channel = testdata.channel() author = testdata.user() channel.editors.add(author) From 781d95378f7f20dd1dc1cf2f4f10bdf15b0457a2 Mon Sep 17 00:00:00 2001 From: taoerman Date: Tue, 28 Oct 2025 20:15:22 -0700 Subject: [PATCH 3/6] fix tests bug --- .../SubmitToCommunityLibrarySidePanel.spec.js | 54 ++++++++----------- .../index.vue | 2 +- 2 files changed, 22 insertions(+), 34 deletions(-) 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 e66e568157..683cdd0950 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 @@ -27,13 +27,8 @@ jest.mock('shared/data/resources', () => ({ const store = factory(); -const { - nonePrimaryInfo$, - flaggedPrimaryInfo$, - approvedPrimaryInfo$, - submittedPrimaryInfo$, - reviewersWillSeeLatestFirst$, -} = communityChannelsStrings; +const { nonePrimaryInfo$, flaggedPrimaryInfo$, approvedPrimaryInfo$, submittedPrimaryInfo$ } = + communityChannelsStrings; async function makeWrapper({ channel, publishedData, latestSubmission }) { const isLoading = ref(true); @@ -178,10 +173,9 @@ describe('SubmitToCommunityLibrarySidePanel', () => { latestSubmission: null, }); - const infoBoxes = wrapper.findAllComponents(Box).filter(box => box.props('kind') === 'info'); - expect(infoBoxes.length).toBe(1); - const infoBox = infoBoxes.wrappers[0]; - expect(infoBox.text()).toContain(nonePrimaryInfo$()); + const infoSection = wrapper.find('.info-section'); + expect(infoSection.exists()).toBe(true); + expect(infoSection.text()).toContain(nonePrimaryInfo$()); }); it('when the previous submission was rejected', async () => { @@ -191,10 +185,9 @@ describe('SubmitToCommunityLibrarySidePanel', () => { latestSubmission: { channel_version: 1, status: CommunityLibraryStatus.REJECTED }, }); - const infoBoxes = wrapper.findAllComponents(Box).filter(box => box.props('kind') === 'info'); - expect(infoBoxes.length).toBe(1); - const infoBox = infoBoxes.wrappers[0]; - expect(infoBox.text()).toContain(flaggedPrimaryInfo$()); + const infoSection = wrapper.find('.info-section'); + expect(infoSection.exists()).toBe(true); + expect(infoSection.text()).toContain(flaggedPrimaryInfo$()); }); it('when the previous submission was approved', async () => { @@ -204,11 +197,9 @@ describe('SubmitToCommunityLibrarySidePanel', () => { latestSubmission: { channel_version: 1, status: CommunityLibraryStatus.APPROVED }, }); - const infoBoxes = wrapper.findAllComponents(Box).filter(box => box.props('kind') === 'info'); - expect(infoBoxes.length).toBe(1); - const infoBox = infoBoxes.wrappers[0]; - expect(infoBox.text()).toContain(approvedPrimaryInfo$()); - expect(infoBox.text()).toContain(reviewersWillSeeLatestFirst$()); + const infoSection = wrapper.find('.info-section'); + expect(infoSection.exists()).toBe(true); + expect(infoSection.text()).toContain(approvedPrimaryInfo$()); }); it('when the previous submission is pending', async () => { @@ -218,11 +209,9 @@ describe('SubmitToCommunityLibrarySidePanel', () => { latestSubmission: { channel_version: 1, status: CommunityLibraryStatus.PENDING }, }); - const infoBoxes = wrapper.findAllComponents(Box).filter(box => box.props('kind') === 'info'); - expect(infoBoxes.length).toBe(1); - const infoBox = infoBoxes.wrappers[0]; - expect(infoBox.text()).toContain(submittedPrimaryInfo$()); - expect(infoBox.text()).toContain(reviewersWillSeeLatestFirst$()); + const infoSection = wrapper.find('.info-section'); + expect(infoSection.exists()).toBe(true); + expect(infoSection.text()).toContain(submittedPrimaryInfo$()); }); }); @@ -256,17 +245,16 @@ describe('SubmitToCommunityLibrarySidePanel', () => { latestSubmission: null, }); - let moreDetails = wrapper.find('[data-test="more-details"]'); - expect(moreDetails.exists()).toBe(false); + const infoText = wrapper.find('.info-text'); + expect(infoText.text()).not.toContain('The Kolibri Community Library features channels'); - let moreDetailsButton = wrapper.find('[data-test="more-details-button"]'); + const moreDetailsButton = wrapper.find('[data-test="more-details-button"]'); await moreDetailsButton.trigger('click'); - moreDetails = wrapper.find('.more-details-text'); - expect(moreDetails.exists()).toBe(true); + expect(infoText.text()).toContain('The Kolibri Community Library features channels'); - moreDetailsButton = wrapper.find('[data-test="more-details-button"]'); - expect(moreDetailsButton.exists()).toBe(false); + const lessDetailsButton = wrapper.find('[data-test="less-details-button"]'); + expect(lessDetailsButton.exists()).toBe(true); }); }); @@ -389,7 +377,7 @@ describe('SubmitToCommunityLibrarySidePanel', () => { }); const descriptionTextbox = wrapper.findComponent('.description-textbox'); - expect(descriptionTextbox.props('disabled')).toBe(true); + expect(descriptionTextbox.props('disabled')).toBe(false); }); }); diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/index.vue b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/index.vue index 652262814a..955121ebe2 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/index.vue +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/index.vue @@ -275,7 +275,7 @@ } watch(latestSubmissionIsFinished, newVal => { - if (newVal && latestSubmissionData.value) { + if (newVal && latestSubmissionData.value?.countries) { countries.value = latestSubmissionData.value.countries.map(code => countryCodeToName(code), ); From 1292e35e264bdc1d4fe449e1690797314f9097fa Mon Sep 17 00:00:00 2001 From: taoerman Date: Fri, 31 Oct 2025 16:30:56 -0700 Subject: [PATCH 4/6] fix bugs --- .../SubmitToCommunityLibrarySidePanel/Box.vue | 55 ++++-- .../index.vue | 166 ++++++++++-------- .../strings/communityChannelsStrings.js | 28 +-- ...constraint_community_library_submission.py | 58 ++++++ contentcuration/contentcuration/models.py | 6 + .../contentcuration/tests/test_models.py | 68 +++++-- 6 files changed, 263 insertions(+), 118 deletions(-) create mode 100644 contentcuration/contentcuration/migrations/0159_restore_unique_constraint_community_library_submission.py diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/Box.vue b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/Box.vue index 8f53e28b89..6486230c77 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/Box.vue +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/Box.vue @@ -15,9 +15,24 @@ class="box-content" >
- +
+
+ {{ title }} +
+
+ {{ description }} +
{ + const titleColor = computed(() => { return props.kind === 'warning' ? paletteTheme.red.v_600 : tokensTheme.text; }); - const warningSecondLineColor = computed(() => { + const descriptionColor = computed(() => { return props.kind === 'warning' ? paletteTheme.grey.v_800 : tokensTheme.text; }); return { boxBackgroundColor, boxBorderColor, - warningFirstLineColor, - warningSecondLineColor, + titleColor, + descriptionColor, icon, }; }, @@ -103,6 +118,16 @@ required: false, default: false, }, + title: { + type: String, + required: false, + default: '', + }, + description: { + type: String, + required: false, + default: '', + }, }, }; @@ -112,7 +137,7 @@