From e037d7d1e5dcbd0dbf8f9c46a7fcc8b257eced3c Mon Sep 17 00:00:00 2001 From: taoerman Date: Wed, 22 Oct 2025 17:53:23 -0700 Subject: [PATCH 1/6] Prevent inconsistent states beteween public channels and community channels --- .../pages/Channels/ChannelActionsDropdown.vue | 20 ++ .../views/TreeView/TreeViewBase.vue | 22 +- contentcuration/contentcuration/models.py | 22 ++ .../tests/test_mutual_exclusivity.py | 251 ++++++++++++++++++ .../contentcuration/viewsets/channel.py | 9 + .../viewsets/community_library_submission.py | 9 + 6 files changed, 332 insertions(+), 1 deletion(-) create mode 100644 contentcuration/contentcuration/tests/test_mutual_exclusivity.py diff --git a/contentcuration/contentcuration/frontend/administration/pages/Channels/ChannelActionsDropdown.vue b/contentcuration/contentcuration/frontend/administration/pages/Channels/ChannelActionsDropdown.vue index 62f992d766..a5e2b25210 100644 --- a/contentcuration/contentcuration/frontend/administration/pages/Channels/ChannelActionsDropdown.vue +++ b/contentcuration/contentcuration/frontend/administration/pages/Channels/ChannelActionsDropdown.vue @@ -96,6 +96,22 @@ > Make private + + + This channel has been added to the Community Library and cannot be marked + public. + - + + + {{ $tr('publicChannelCannotSubmitToCommunityLibrary') }} + + {{ $tr('submitToCommunityLibrary') }} Date: Thu, 23 Oct 2025 01:02:49 +0000 Subject: [PATCH 2/6] [pre-commit.ci lite] apply automatic fixes --- contentcuration/contentcuration/models.py | 2 +- .../tests/test_mutual_exclusivity.py | 86 +++++++++++-------- .../contentcuration/viewsets/channel.py | 2 +- .../viewsets/community_library_submission.py | 5 +- 4 files changed, 55 insertions(+), 40 deletions(-) diff --git a/contentcuration/contentcuration/models.py b/contentcuration/contentcuration/models.py index 5fa93600b9..84ecb2690f 100644 --- a/contentcuration/contentcuration/models.py +++ b/contentcuration/contentcuration/models.py @@ -2641,7 +2641,7 @@ def save(self, *args, **kwargs): "Channel version must be less than or equal to the current channel version", code="impossibly_high_channel_version", ) - + if self.channel.public: raise ValidationError( "Cannot create a community library submission for a public channel.", diff --git a/contentcuration/contentcuration/tests/test_mutual_exclusivity.py b/contentcuration/contentcuration/tests/test_mutual_exclusivity.py index 5c39ae9259..167e415f0c 100644 --- a/contentcuration/contentcuration/tests/test_mutual_exclusivity.py +++ b/contentcuration/contentcuration/tests/test_mutual_exclusivity.py @@ -1,31 +1,32 @@ +from unittest import mock +from django.contrib.auth import get_user_model from django.core.exceptions import ValidationError from django.test import TestCase -from django.contrib.auth import get_user_model from django.urls import reverse -from rest_framework.test import APITestCase from rest_framework import status -from unittest import mock +from rest_framework.test import APITestCase -from contentcuration.models import Channel, CommunityLibrarySubmission from contentcuration.constants import community_library_submission as constants -from contentcuration.viewsets.channel import AdminChannelSerializer +from contentcuration.models import Channel +from contentcuration.models import CommunityLibrarySubmission from contentcuration.tests import testdata -from contentcuration.tests.base import StudioTestCase, StudioAPITestCase +from contentcuration.tests.base import StudioAPITestCase +from contentcuration.tests.base import StudioTestCase +from contentcuration.viewsets.channel import AdminChannelSerializer User = get_user_model() class ChannelMutualExclusivityTestCase(StudioTestCase): - def setUp(self): self.user = testdata.user() self.channel = Channel.objects.create( actor_id=self.user.id, name="Test Channel", description="Test Description", - version=0 + version=0, ) self.channel.version = 1 self.channel.save() @@ -41,10 +42,13 @@ def test_public_channel_cannot_be_submitted_to_community_library(self): channel=self.channel, channel_version=1, author=self.user, - description="Test submission" + description="Test submission", ) - self.assertIn("Cannot create a community library submission for a public channel", str(context.exception)) + self.assertIn( + "Cannot create a community library submission for a public channel", + str(context.exception), + ) def test_community_channel_cannot_be_marked_public(self): """Test that a community channel cannot be marked public.""" @@ -53,14 +57,17 @@ def test_community_channel_cannot_be_marked_public(self): channel_version=1, author=self.user, description="Test submission", - status=constants.STATUS_APPROVED + status=constants.STATUS_APPROVED, ) self.channel.public = True with self.assertRaises(ValidationError) as context: self.channel.clean() - self.assertIn("This channel has been added to the Community Library and cannot be marked public", str(context.exception)) + self.assertIn( + "This channel has been added to the Community Library and cannot be marked public", + str(context.exception), + ) def test_is_community_channel_method(self): """Test the is_community_channel method.""" @@ -71,7 +78,7 @@ def test_is_community_channel_method(self): channel_version=1, author=self.user, description="Test submission", - status=constants.STATUS_APPROVED + status=constants.STATUS_APPROVED, ) self.assertTrue(self.channel.is_community_channel()) @@ -82,7 +89,7 @@ def test_is_community_channel_method(self): channel_version=2, author=self.user, description="Test submission 2", - status=constants.STATUS_LIVE + status=constants.STATUS_LIVE, ) self.assertTrue(self.channel.is_community_channel()) @@ -93,14 +100,14 @@ def test_is_community_channel_method(self): channel_version=3, author=self.user, description="Test submission 3", - status=constants.STATUS_PENDING + status=constants.STATUS_PENDING, ) self.assertTrue(self.channel.is_community_channel()) def test_non_community_channel_can_be_marked_public(self): """Test that a non-community channel can be marked public.""" self.channel.public = True - self.channel.clean() + self.channel.clean() self.channel.save() self.assertTrue(self.channel.public) @@ -110,7 +117,7 @@ def test_non_public_channel_can_be_submitted_to_community_library(self): channel=self.channel, channel_version=1, author=self.user, - description="Test submission" + description="Test submission", ) self.assertEqual(submission.channel, self.channel) @@ -124,7 +131,7 @@ def setUp(self): actor_id=self.user.id, name="Test Channel", description="Test Description", - version=0 + version=0, ) self.channel.version = 1 @@ -138,24 +145,23 @@ def test_serializer_validates_community_channel_cannot_be_public(self): channel_version=1, author=self.user, description="Test submission", - status=constants.STATUS_APPROVED + status=constants.STATUS_APPROVED, ) serializer = AdminChannelSerializer( - instance=self.channel, - data={"public": True}, - partial=True + instance=self.channel, data={"public": True}, partial=True ) self.assertFalse(serializer.is_valid()) - self.assertIn("This channel has been added to the Community Library and cannot be marked public", str(serializer.errors)) + self.assertIn( + "This channel has been added to the Community Library and cannot be marked public", + str(serializer.errors), + ) def test_serializer_allows_non_community_channel_to_be_public(self): """Test that serializer allows non-community channel to be public.""" serializer = AdminChannelSerializer( - instance=self.channel, - data={"public": True}, - partial=True + instance=self.channel, data={"public": True}, partial=True ) self.assertTrue(serializer.is_valid()) @@ -170,13 +176,13 @@ def test_serializer_allows_community_channel_to_remain_non_public(self): channel_version=1, author=self.user, description="Test submission", - status=constants.STATUS_APPROVED + status=constants.STATUS_APPROVED, ) serializer = AdminChannelSerializer( instance=self.channel, data={"source_url": "https://example.com"}, - partial=True + partial=True, ) self.assertTrue(serializer.is_valid()) @@ -191,19 +197,19 @@ class CommunityLibrarySubmissionMutualExclusivityAPITestCase(StudioAPITestCase): def setUp(self): super().setUp() - + self.ensure_db_exists_patcher = mock.patch( "contentcuration.utils.publish.ensure_versioned_database_exists" ) self.ensure_db_exists_patcher.start() - + self.user = testdata.user() self.channel = testdata.channel() self.channel.public = False self.channel.version = 1 self.channel.editors.add(self.user) self.channel.save() - + def tearDown(self): self.ensure_db_exists_patcher.stop() super().tearDown() @@ -219,12 +225,15 @@ def test_api_prevents_public_channel_submission_to_community_library(self): data = { "channel": self.channel.id, "description": "Test submission", - "countries": [] + "countries": [], } response = self.client.post(url, data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertIn("Cannot create a community library submission for a public channel", str(response.data)) + self.assertIn( + "Cannot create a community library submission for a public channel", + str(response.data), + ) def test_api_prevents_approving_submission_for_public_channel(self): """Test that API prevents approving submission for channel that became public.""" @@ -235,17 +244,22 @@ def test_api_prevents_approving_submission_for_public_channel(self): status=constants.STATUS_PENDING, author=self.user, ) - + self.channel.public = True self.channel.save() self.client.force_authenticate(user=self.admin_user) - url = reverse("admin-community-library-submission-resolve", kwargs={"pk": submission.id}) + url = reverse( + "admin-community-library-submission-resolve", kwargs={"pk": submission.id} + ) data = { "status": constants.STATUS_APPROVED, } response = self.client.post(url, data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertIn("Cannot approve a community library submission for a channel that has been marked public", str(response.data)) + self.assertIn( + "Cannot approve a community library submission for a channel that has been marked public", + str(response.data), + ) diff --git a/contentcuration/contentcuration/viewsets/channel.py b/contentcuration/contentcuration/viewsets/channel.py index cd4bbeee19..78b76bfa70 100644 --- a/contentcuration/contentcuration/viewsets/channel.py +++ b/contentcuration/contentcuration/viewsets/channel.py @@ -1112,7 +1112,7 @@ class Meta: nested_writes = True def validate_public(self, value): - if value and hasattr(self, 'instance') and self.instance: + if value and hasattr(self, "instance") and self.instance: if self.instance.is_community_channel(): raise ValidationError( "This channel has been added to the Community Library and cannot be marked public.", diff --git a/contentcuration/contentcuration/viewsets/community_library_submission.py b/contentcuration/contentcuration/viewsets/community_library_submission.py index 6c93e37836..771e0afd34 100644 --- a/contentcuration/contentcuration/viewsets/community_library_submission.py +++ b/contentcuration/contentcuration/viewsets/community_library_submission.py @@ -122,8 +122,9 @@ def update(self, instance, validated_data): ) if ( - "status" in validated_data - and validated_data["status"] == community_library_submission_constants.STATUS_APPROVED + "status" in validated_data + and validated_data["status"] + == community_library_submission_constants.STATUS_APPROVED and instance.channel.public ): raise ValidationError( From 331227b0ba6772d5ad8e01e86cb82ea180a9b0b3 Mon Sep 17 00:00:00 2001 From: taoerman Date: Wed, 22 Oct 2025 18:24:54 -0700 Subject: [PATCH 3/6] fix linting --- .../contentcuration/tests/test_mutual_exclusivity.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/contentcuration/contentcuration/tests/test_mutual_exclusivity.py b/contentcuration/contentcuration/tests/test_mutual_exclusivity.py index 5c39ae9259..6d1ca95a2d 100644 --- a/contentcuration/contentcuration/tests/test_mutual_exclusivity.py +++ b/contentcuration/contentcuration/tests/test_mutual_exclusivity.py @@ -1,9 +1,7 @@ from django.core.exceptions import ValidationError -from django.test import TestCase from django.contrib.auth import get_user_model from django.urls import reverse -from rest_framework.test import APITestCase from rest_framework import status from unittest import mock @@ -48,7 +46,7 @@ def test_public_channel_cannot_be_submitted_to_community_library(self): def test_community_channel_cannot_be_marked_public(self): """Test that a community channel cannot be marked public.""" - submission = CommunityLibrarySubmission.objects.create( + CommunityLibrarySubmission.objects.create( channel=self.channel, channel_version=1, author=self.user, From e18a9b4c89c5b37407019493a42f50cfa4b0865f Mon Sep 17 00:00:00 2001 From: taoerman Date: Wed, 22 Oct 2025 18:31:28 -0700 Subject: [PATCH 4/6] fix linting --- .../contentcuration/tests/test_mutual_exclusivity.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/contentcuration/contentcuration/tests/test_mutual_exclusivity.py b/contentcuration/contentcuration/tests/test_mutual_exclusivity.py index 097616fb68..0fd4b38524 100644 --- a/contentcuration/contentcuration/tests/test_mutual_exclusivity.py +++ b/contentcuration/contentcuration/tests/test_mutual_exclusivity.py @@ -2,10 +2,8 @@ from django.contrib.auth import get_user_model from django.core.exceptions import ValidationError -from django.test import TestCase from django.urls import reverse from rest_framework import status -from rest_framework.test import APITestCase from contentcuration.constants import community_library_submission as constants from contentcuration.models import Channel From 1968670014894d8ecf7ce1d16cb03f10a61da6d4 Mon Sep 17 00:00:00 2001 From: taoerman Date: Mon, 27 Oct 2025 08:50:13 -0700 Subject: [PATCH 5/6] fixing code according to comments --- .../components/ConfirmationDialog.vue | 15 +++++++++++ .../pages/Channels/ChannelActionsDropdown.vue | 27 +++++++------------ .../views/TreeView/TreeViewBase.vue | 22 +-------------- .../test_public_models_mutual_exclusivity.py} | 0 4 files changed, 26 insertions(+), 38 deletions(-) rename contentcuration/{contentcuration/tests/test_mutual_exclusivity.py => kolibri_public/tests/test_public_models_mutual_exclusivity.py} (100%) diff --git a/contentcuration/contentcuration/frontend/administration/components/ConfirmationDialog.vue b/contentcuration/contentcuration/frontend/administration/components/ConfirmationDialog.vue index ff5c050742..c150df641f 100644 --- a/contentcuration/contentcuration/frontend/administration/components/ConfirmationDialog.vue +++ b/contentcuration/contentcuration/frontend/administration/components/ConfirmationDialog.vue @@ -5,6 +5,12 @@ :header="title" :text="text" > +
+ {{ errorText }} +
- - - {{ $tr('publicChannelCannotSubmitToCommunityLibrary') }} - - + {{ $tr('submitToCommunityLibrary') }} Date: Mon, 27 Oct 2025 15:52:58 +0000 Subject: [PATCH 6/6] [pre-commit.ci lite] apply automatic fixes --- .../frontend/administration/components/ConfirmationDialog.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contentcuration/contentcuration/frontend/administration/components/ConfirmationDialog.vue b/contentcuration/contentcuration/frontend/administration/components/ConfirmationDialog.vue index c150df641f..023f248673 100644 --- a/contentcuration/contentcuration/frontend/administration/components/ConfirmationDialog.vue +++ b/contentcuration/contentcuration/frontend/administration/components/ConfirmationDialog.vue @@ -7,7 +7,7 @@ >
{{ errorText }}