diff --git a/contentcuration/contentcuration/tests/viewsets/base.py b/contentcuration/contentcuration/tests/viewsets/base.py index 97f5cb52f7..8d34afeb24 100644 --- a/contentcuration/contentcuration/tests/viewsets/base.py +++ b/contentcuration/contentcuration/tests/viewsets/base.py @@ -15,6 +15,7 @@ from contentcuration.viewsets.sync.utils import generate_publish_event as base_generate_publish_event from contentcuration.viewsets.sync.utils import generate_update_event as base_generate_update_event from contentcuration.viewsets.sync.utils import generate_update_descendants_event as base_generate_update_descendants_event +from contentcuration.viewsets.sync.utils import generate_publish_next_event as base_generate_publish_next_event def generate_copy_event(*args, **kwargs): @@ -66,6 +67,11 @@ def generate_publish_channel_event(channel_id): event["rev"] = random.randint(1, 10000000) return event +def generate_publish_next_event(channel_id): + event = base_generate_publish_next_event(channel_id) + event["rev"] = random.randint(1, 10000000) + return event + class SyncTestMixin(object): celery_task_always_eager = None diff --git a/contentcuration/contentcuration/tests/viewsets/test_channel.py b/contentcuration/contentcuration/tests/viewsets/test_channel.py index 76c5ac1ed3..afedc2a4db 100644 --- a/contentcuration/contentcuration/tests/viewsets/test_channel.py +++ b/contentcuration/contentcuration/tests/viewsets/test_channel.py @@ -17,14 +17,26 @@ from contentcuration.tests.viewsets.base import generate_delete_event from contentcuration.tests.viewsets.base import generate_deploy_channel_event from contentcuration.tests.viewsets.base import generate_publish_channel_event +from contentcuration.tests.viewsets.base import generate_publish_next_event from contentcuration.tests.viewsets.base import generate_sync_channel_event from contentcuration.tests.viewsets.base import generate_update_event from contentcuration.tests.viewsets.base import SyncTestMixin from contentcuration.viewsets.channel import _unpublished_changes_query from contentcuration.viewsets.sync.constants import CHANNEL +from mock import patch class SyncTestCase(SyncTestMixin, StudioAPITestCase): + @classmethod + def setUpClass(cls): + super(SyncTestCase, cls).setUpClass() + cls.patch_copy_db = patch('contentcuration.utils.publish.save_export_database') + cls.mock_save_export = cls.patch_copy_db.start() + + @classmethod + def tearDownClass(cls): + super(SyncTestCase, cls).tearDownClass() + cls.patch_copy_db.stop() @property def channel_metadata(self): @@ -391,6 +403,61 @@ def test_publish_does_not_make_publishable(self): self.assertEqual(_unpublished_changes_query(channel).count(), 0) + def test_publish_next(self): + channel = testdata.channel() + user = testdata.user() + channel.editors.add(user) + self.client.force_authenticate( + user + ) # This will skip all authentication checks + + channel.staging_tree = testdata.tree() + node = testdata.node({ + 'kind_id': 'video', 'title': 'title', 'children': []}) + node.complete = True + node.parent = channel.staging_tree + node.save() + channel.staging_tree.save() + channel.save() + self.assertEqual(channel.staging_tree.published, False) + + response = self.sync_changes( + [ + generate_publish_next_event(channel.id) + ] + ) + + self.assertEqual(response.status_code, 200) + modified_channel = models.Channel.objects.get(id=channel.id) + self.assertEqual(modified_channel.staging_tree.published, True) + + def test_publish_next_with_incomplete_staging_tree(self): + channel = testdata.channel() + user = testdata.user() + channel.editors.add(user) + self.client.force_authenticate( + user + ) # This will skip all authentication checks + + channel.staging_tree = cc.ContentNode( + kind_id=content_kinds.TOPIC, title="test", node_id="aaa" + ) + channel.staging_tree.save() + channel.save() + self.assertEqual(channel.staging_tree.published, False) + + response = self.sync_changes( + [ + generate_publish_next_event(channel.id) + ] + ) + + self.assertEqual(response.status_code, 200) + self.assertTrue( + "Channel is not ready to be published" in response.json()["errors"][0]["errors"][0]) + modified_channel = models.Channel.objects.get(id=channel.id) + self.assertEqual(modified_channel.staging_tree.published, False) + class CRUDTestCase(StudioAPITestCase): @property diff --git a/contentcuration/contentcuration/viewsets/channel.py b/contentcuration/contentcuration/viewsets/channel.py index 5cd9b84104..f4f51a48f4 100644 --- a/contentcuration/contentcuration/viewsets/channel.py +++ b/contentcuration/contentcuration/viewsets/channel.py @@ -560,6 +560,55 @@ def publish(self, pk, version_notes="", language=None): ], applied=True, unpublishable=True) raise + def publish_next_from_changes(self, changes): + errors = [] + for publish in changes: + try: + self.publish_next(publish["key"]) + except Exception as e: + log_sync_exception(e, user=self.request.user, change=publish) + publish["errors"] = [str(e)] + errors.append(publish) + return errors + + def publish_next(self, pk): + logging.debug("Entering the publish staging channel endpoint") + + channel = self.get_edit_queryset().get(pk=pk) + + if channel.deleted: + raise ValidationError("Cannot publish a deleted channel") + elif channel.staging_tree.publishing: + raise ValidationError("Channel staging tree is already publishing") + + channel.staging_tree.publishing = True + channel.staging_tree.save() + + with create_change_tracker(pk, CHANNEL, channel.id, self.request.user, + "export-channel-staging-tree") as progress_tracker: + try: + channel = publish_channel( + self.request.user.pk, + channel.id, + progress_tracker=progress_tracker, + use_staging_tree=True, + ) + Change.create_changes([ + generate_update_event( + channel.id, CHANNEL, { + "primary_token": channel.get_human_token().token, + }, channel_id=channel.id + ), + ], applied=True) + except ChannelIncompleteError: + channel.staging_tree.publishing = False + channel.staging_tree.save() + raise ValidationError("Channel is not ready to be published") + except Exception: + channel.staging_tree.publishing = False + channel.staging_tree.save() + raise + def sync_from_changes(self, changes): errors = [] for sync in changes: diff --git a/contentcuration/contentcuration/viewsets/sync/base.py b/contentcuration/contentcuration/viewsets/sync/base.py index 0a0e515bf0..68fa0336e2 100644 --- a/contentcuration/contentcuration/viewsets/sync/base.py +++ b/contentcuration/contentcuration/viewsets/sync/base.py @@ -24,6 +24,7 @@ from contentcuration.viewsets.sync.constants import DELETED from contentcuration.viewsets.sync.constants import DEPLOYED from contentcuration.viewsets.sync.constants import UPDATED_DESCENDANTS +from contentcuration.viewsets.sync.constants import PUBLISHED_NEXT from contentcuration.viewsets.sync.constants import EDITOR_M2M from contentcuration.viewsets.sync.constants import FILE from contentcuration.viewsets.sync.constants import INVITATION @@ -96,6 +97,7 @@ def get_change_type(obj): SYNCED: "sync_from_changes", DEPLOYED: "deploy_from_changes", UPDATED_DESCENDANTS: "update_descendants_from_changes", + PUBLISHED_NEXT: "publish_next_from_changes" } diff --git a/contentcuration/contentcuration/viewsets/sync/constants.py b/contentcuration/contentcuration/viewsets/sync/constants.py index 0f208813a1..4733aeeffe 100644 --- a/contentcuration/contentcuration/viewsets/sync/constants.py +++ b/contentcuration/contentcuration/viewsets/sync/constants.py @@ -8,6 +8,7 @@ SYNCED = 7 DEPLOYED = 8 UPDATED_DESCENDANTS = 9 +PUBLISHED_NEXT = 10 ALL_CHANGES = set([ @@ -20,6 +21,7 @@ SYNCED, DEPLOYED, UPDATED_DESCENDANTS, + PUBLISHED_NEXT, ]) # Client-side table constants diff --git a/contentcuration/contentcuration/viewsets/sync/utils.py b/contentcuration/contentcuration/viewsets/sync/utils.py index e47552735f..43bf280b22 100644 --- a/contentcuration/contentcuration/viewsets/sync/utils.py +++ b/contentcuration/contentcuration/viewsets/sync/utils.py @@ -14,6 +14,7 @@ from contentcuration.viewsets.sync.constants import PUBLISHED from contentcuration.viewsets.sync.constants import UPDATED from contentcuration.viewsets.sync.constants import UPDATED_DESCENDANTS +from contentcuration.viewsets.sync.constants import PUBLISHED_NEXT def validate_table(table): @@ -88,6 +89,12 @@ def generate_update_descendants_event(key, mods, channel_id=None, user_id=None): event["mods"] = mods return event +def generate_publish_next_event(key, version_notes="", language=None): + event = _generate_event(key, CHANNEL, PUBLISHED_NEXT, key, None) + event["version_notes"] = version_notes + event["language"] = language + return event + def log_sync_exception(e, user=None, change=None, changes=None): # Capture exception and report, but allow sync # to complete properly.