Skip to content

Commit b57cfd2

Browse files
authored
Merge pull request #4980 from taoerman/unstable
Add optional use_staging_tree parameter to publish_channel function
2 parents d701fa8 + 486e066 commit b57cfd2

3 files changed

Lines changed: 180 additions & 33 deletions

File tree

contentcuration/contentcuration/tests/test_exportchannel.py

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
from .base import StudioTestCase
2727
from .helpers import clear_tasks
28-
from .testdata import channel
28+
from .testdata import channel, tree
2929
from .testdata import create_studio_file
3030
from .testdata import node as create_node
3131
from .testdata import slideshow
@@ -40,6 +40,8 @@
4040
from contentcuration.utils.publish import fill_published_fields
4141
from contentcuration.utils.publish import map_prerequisites
4242
from contentcuration.utils.publish import MIN_SCHEMA_VERSION
43+
from contentcuration.utils.publish import NoneContentNodeTreeError
44+
from contentcuration.utils.publish import publish_channel
4345
from contentcuration.utils.publish import set_channel_icon_encoding
4446
from contentcuration.viewsets.base import create_change_tracker
4547

@@ -600,3 +602,125 @@ def test_failed_task_objects_cleaned_up_when_publishing(self):
600602
new_task_result = TaskResult.objects.filter(task_name=task_name, status=states.STARTED).first()
601603
new_custom_task_metadata = CustomTaskMetadata.objects.get(channel_id=channel_id, user=self.user, signature=signature)
602604
assert new_custom_task_metadata.task_id == new_task_result.task_id
605+
606+
class PublishStagingTreeTestCase(StudioTestCase):
607+
@classmethod
608+
def setUpClass(cls):
609+
super(PublishStagingTreeTestCase, cls).setUpClass()
610+
cls.patch_copy_db = patch('contentcuration.utils.publish.save_export_database')
611+
cls.mock_save_export = cls.patch_copy_db.start()
612+
613+
@classmethod
614+
def tearDownClass(cls):
615+
super(PublishStagingTreeTestCase, cls).tearDownClass()
616+
cls.patch_copy_db.stop()
617+
618+
def setUp(self):
619+
super(PublishStagingTreeTestCase, self).setUp()
620+
621+
self.channel_version = 3
622+
self.incomplete_video_in_staging = 'Incomplete video in staging tree'
623+
self.complete_video_in_staging = 'Complete video in staging tree'
624+
self.incomplete_video_in_main = 'Incomplete video in main tree'
625+
self.complete_video_in_main = 'Complete video in main tree'
626+
627+
self.content_channel = channel()
628+
self.content_channel.staging_tree = tree()
629+
self.content_channel.version = self.channel_version
630+
self.content_channel.save()
631+
632+
# Incomplete node should be excluded.
633+
new_node = create_node({'kind_id': 'video', 'title': self.incomplete_video_in_staging, 'children': []})
634+
new_node.complete = False
635+
new_node.parent = self.content_channel.staging_tree
636+
new_node.published = False
637+
new_node.save()
638+
639+
# Complete node should be included.
640+
new_video = create_node({'kind_id': 'video', 'title': self.complete_video_in_staging, 'children': []})
641+
new_video.complete = True
642+
new_video.parent = self.content_channel.staging_tree
643+
new_node.published = False
644+
new_video.save()
645+
646+
# Incomplete node in main_tree.
647+
new_node = create_node({'kind_id': 'video', 'title': self.incomplete_video_in_main, 'children': []})
648+
new_node.complete = False
649+
new_node.parent = self.content_channel.main_tree
650+
new_node.published = False
651+
new_node.save()
652+
653+
# Complete node in main_tree.
654+
new_node = create_node({'kind_id': 'video', 'title': self.complete_video_in_main, 'children': []})
655+
new_node.complete = True
656+
new_node.parent = self.content_channel.main_tree
657+
new_node.published = False
658+
new_node.save()
659+
660+
def run_publish_channel(self):
661+
publish_channel(
662+
self.admin_user.id,
663+
self.content_channel.id,
664+
version_notes="",
665+
force=False,
666+
force_exercises=False,
667+
send_email=False,
668+
progress_tracker=None,
669+
language="fr",
670+
use_staging_tree=True
671+
)
672+
673+
def test_none_staging_tree(self):
674+
self.content_channel.staging_tree = None
675+
self.content_channel.save()
676+
with self.assertRaises(NoneContentNodeTreeError):
677+
self.run_publish_channel()
678+
679+
def test_staging_tree_published(self):
680+
self.assertFalse(self.content_channel.staging_tree.published)
681+
self.run_publish_channel()
682+
self.content_channel.refresh_from_db()
683+
self.assertTrue(self.content_channel.staging_tree.published)
684+
685+
def test_next_version_exported(self):
686+
self.run_publish_channel()
687+
self.mock_save_export.assert_called_with(
688+
self.content_channel.id,
689+
"next",
690+
True,
691+
)
692+
693+
def test_main_tree_not_impacted(self):
694+
self.assertFalse(self.content_channel.main_tree.published)
695+
self.run_publish_channel()
696+
self.content_channel.refresh_from_db()
697+
self.assertFalse(self.content_channel.main_tree.published)
698+
699+
def test_channel_version_not_incremented(self):
700+
self.assertEqual(self.content_channel.version, self.channel_version)
701+
self.run_publish_channel()
702+
self.content_channel.refresh_from_db()
703+
self.assertEqual(self.content_channel.version, self.channel_version)
704+
705+
def test_staging_tree_used_for_publish(self):
706+
set_channel_icon_encoding(self.content_channel)
707+
self.tempdb = create_content_database(
708+
self.content_channel,
709+
True,
710+
self.admin_user.id,
711+
True,
712+
progress_tracker=None,
713+
use_staging_tree=True,
714+
)
715+
set_active_content_database(self.tempdb)
716+
717+
nodes = kolibri_models.ContentNode.objects.all()
718+
self.assertEqual(nodes.filter(title=self.incomplete_video_in_staging).count(), 0)
719+
self.assertEqual(nodes.filter(title=self.complete_video_in_staging).count(), 1)
720+
self.assertEqual(nodes.filter(title=self.incomplete_video_in_main).count(), 0)
721+
self.assertEqual(nodes.filter(title=self.complete_video_in_main).count(), 0)
722+
723+
cleanup_content_database_connection(self.tempdb)
724+
set_active_content_database(None)
725+
if os.path.exists(self.tempdb):
726+
os.remove(self.tempdb)

contentcuration/contentcuration/tests/test_sync.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ def setUp(self):
4747

4848
# Put all nodes into a clean state so we can track when syncing
4949
# causes changes in the tree.
50-
mark_all_nodes_as_published(self.channel)
51-
mark_all_nodes_as_published(self.derivative_channel)
50+
mark_all_nodes_as_published(self.channel.main_tree)
51+
mark_all_nodes_as_published(self.derivative_channel.main_tree)
5252

5353
def _add_temp_file_to_content_node(self, node):
5454
new_file = create_temp_file("mybytes")

contentcuration/contentcuration/utils/publish.py

Lines changed: 53 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ class ChannelIncompleteError(Exception):
7272
pass
7373

7474

75+
class NoneContentNodeTreeError(Exception):
76+
pass
77+
78+
7579
class SlowPublishError(Exception):
7680
"""
7781
Used to track slow Publishing operations. We don't raise this error,
@@ -111,17 +115,17 @@ def send_emails(channel, user_id, version_notes=''):
111115
user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL, html_message=message)
112116

113117

114-
def create_content_database(channel, force, user_id, force_exercises, progress_tracker=None):
118+
def create_content_database(channel, force, user_id, force_exercises, progress_tracker=None, use_staging_tree=False):
115119
"""
116120
:type progress_tracker: contentcuration.utils.celery.ProgressTracker|None
117121
"""
118122
# increment the channel version
119-
if not force:
123+
if not use_staging_tree and not force:
120124
raise_if_nodes_are_all_unchanged(channel)
121125
fh, tempdb = tempfile.mkstemp(suffix=".sqlite3")
122126

123127
with using_content_database(tempdb):
124-
if not channel.main_tree.publishing:
128+
if not use_staging_tree and not channel.main_tree.publishing:
125129
channel.mark_publishing(user_id)
126130

127131
call_command("migrate",
@@ -130,8 +134,9 @@ def create_content_database(channel, force, user_id, force_exercises, progress_t
130134
no_input=True)
131135
if progress_tracker:
132136
progress_tracker.track(10)
137+
base_tree = channel.staging_tree if use_staging_tree else channel.main_tree
133138
tree_mapper = TreeMapper(
134-
channel.main_tree,
139+
base_tree,
135140
channel.language,
136141
channel.id,
137142
channel.name,
@@ -141,14 +146,16 @@ def create_content_database(channel, force, user_id, force_exercises, progress_t
141146
inherit_metadata=bool(channel.ricecooker_version),
142147
)
143148
tree_mapper.map_nodes()
144-
kolibri_channel = map_channel_to_kolibri_channel(channel)
149+
kolibri_channel = map_channel_to_kolibri_channel(channel, use_staging_tree)
145150
# It should be at this percent already, but just in case.
146151
if progress_tracker:
147152
progress_tracker.track(90)
148-
map_prerequisites(channel.main_tree)
153+
map_prerequisites(base_tree)
154+
# Need to save as version being published, not current version
155+
version = "next" if use_staging_tree else channel.version + 1
149156
save_export_database(
150-
channel.pk, channel.version + 1
151-
) # Need to save as version being published, not current version
157+
channel.pk, version, use_staging_tree,
158+
)
152159
if channel.public:
153160
mapper = ChannelMapper(kolibri_channel)
154161
mapper.run()
@@ -732,17 +739,18 @@ def map_prerequisites(root_node):
732739
logging.error('Unable to find source node for prerequisite relationship {}'.format(str(e)))
733740

734741

735-
def map_channel_to_kolibri_channel(channel):
742+
def map_channel_to_kolibri_channel(channel, use_staging_tree=False):
736743
logging.debug("Generating the channel metadata.")
744+
base_tree = channel.staging_tree if use_staging_tree else channel.main_tree
737745
kolibri_channel = kolibrimodels.ChannelMetadata.objects.create(
738746
id=channel.id,
739747
name=channel.name,
740748
description=channel.description,
741749
tagline=channel.tagline,
742750
version=channel.version + 1, # Need to save as version being published, not current version
743751
thumbnail=channel.icon_encoding,
744-
root_pk=channel.main_tree.node_id,
745-
root_id=channel.main_tree.node_id,
752+
root_pk=base_tree.node_id,
753+
root_id=base_tree.node_id,
746754
min_schema_version=MIN_SCHEMA_VERSION, # Need to modify Kolibri so we can import this without importing models
747755
)
748756
logging.info("Generated the channel metadata.")
@@ -805,25 +813,27 @@ def raise_if_nodes_are_all_unchanged(channel):
805813
logging.info("Some nodes are changed.")
806814

807815

808-
def mark_all_nodes_as_published(channel):
816+
def mark_all_nodes_as_published(tree):
809817
logging.debug("Marking all nodes as published.")
810818

811-
channel.main_tree.get_family().update(changed=False, published=True)
819+
tree.get_family().update(changed=False, published=True)
812820

813821
logging.info("Marked all nodes as published.")
814822

815823

816-
def save_export_database(channel_id, version):
824+
def save_export_database(channel_id, version, use_staging_tree=False):
817825
logging.debug("Saving export database")
818826
current_export_db_location = get_active_content_database()
819827
target_paths = [
820-
os.path.join(
821-
settings.DB_ROOT, "{id}.sqlite3".format(id=channel_id)
822-
),
823828
os.path.join(
824829
settings.DB_ROOT, "{}-{}.sqlite3".format(channel_id, version)
825-
),
830+
)
826831
]
832+
# Only create non-version path if not using the staging tree
833+
if not use_staging_tree:
834+
target_paths.append(
835+
os.path.join(settings.DB_ROOT, "{id}.sqlite3".format(id=channel_id)
836+
))
827837

828838
for target_export_db_location in target_paths:
829839
with open(current_export_db_location, 'rb') as currentf:
@@ -919,30 +929,43 @@ def publish_channel(
919929
send_email=False,
920930
progress_tracker=None,
921931
language=settings.LANGUAGE_CODE,
932+
use_staging_tree=False,
922933
):
923934
"""
924935
:type progress_tracker: contentcuration.utils.celery.ProgressTracker|None
925936
"""
926937
channel = ccmodels.Channel.objects.get(pk=channel_id)
938+
base_tree = channel.staging_tree if use_staging_tree else channel.main_tree
939+
if base_tree is None:
940+
tree_name = "staging_tree" if use_staging_tree else "main_tree"
941+
raise NoneContentNodeTreeError(f"{tree_name} is None!")
927942
kolibri_temp_db = None
928943
start = time.time()
929944
try:
930945
set_channel_icon_encoding(channel)
931-
kolibri_temp_db = create_content_database(channel, force, user_id, force_exercises, progress_tracker=progress_tracker)
932-
increment_channel_version(channel)
946+
kolibri_temp_db = create_content_database(
947+
channel,
948+
force,
949+
user_id,
950+
force_exercises,
951+
progress_tracker=progress_tracker,
952+
use_staging_tree=use_staging_tree,
953+
)
933954
add_tokens_to_channel(channel)
934-
sync_contentnode_and_channel_tsvectors(channel_id=channel.id)
935-
mark_all_nodes_as_published(channel)
936-
fill_published_fields(channel, version_notes)
955+
if not use_staging_tree:
956+
increment_channel_version(channel)
957+
sync_contentnode_and_channel_tsvectors(channel_id=channel.id)
958+
mark_all_nodes_as_published(base_tree)
959+
fill_published_fields(channel, version_notes)
937960

938961
# Attributes not getting set for some reason, so just save it here
939-
channel.main_tree.publishing = False
940-
channel.main_tree.changed = False
941-
channel.main_tree.published = True
942-
channel.main_tree.save()
962+
base_tree.publishing = False
963+
base_tree.changed = False
964+
base_tree.published = True
965+
base_tree.save()
943966

944967
# Delete public channel cache.
945-
if channel.public:
968+
if not use_staging_tree and channel.public:
946969
delete_public_channel_cache_keys()
947970

948971
if send_email:
@@ -960,8 +983,8 @@ def publish_channel(
960983
finally:
961984
if kolibri_temp_db and os.path.exists(kolibri_temp_db):
962985
os.remove(kolibri_temp_db)
963-
channel.main_tree.publishing = False
964-
channel.main_tree.save()
986+
base_tree.publishing = False
987+
base_tree.save()
965988

966989
elapsed = time.time() - start
967990

0 commit comments

Comments
 (0)