From 36e284e065548c912f5f28d6d64071f156090700 Mon Sep 17 00:00:00 2001 From: Ivan Savov Date: Wed, 9 Dec 2020 00:43:42 -0500 Subject: [PATCH 1/6] Will it serialize? Yes it will! --- .../contentcuration/utils/archive.py | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 contentcuration/contentcuration/utils/archive.py diff --git a/contentcuration/contentcuration/utils/archive.py b/contentcuration/contentcuration/utils/archive.py new file mode 100644 index 0000000000..34e8f2627d --- /dev/null +++ b/contentcuration/contentcuration/utils/archive.py @@ -0,0 +1,153 @@ +from datetime import datetime +import json +import os + +from rest_framework import serializers + +from contentcuration.models import Channel +from contentcuration.models import ContentNode + + +# TASKS +################################################################################ + +def archive_channel(channel_id, tree='main'): + # channel_id = '79137650868d58a89f12f28aaceec690' + channel = Channel.objects.get(id=channel_id) + root = getattr(channel, tree + '_tree') + serializer = ContentNodeArchiveSerializer(root) + channel_data = serializer.data + channel_data_json_str = json.dumps(serializer.data, indent=4, ensure_ascii=False) + archive_time = datetime.now().strftime("%Y-%m-%d__%H%M") + filename_ext = channel_id + '_' + tree + '_' + archive_time + '.json' + tmpcontent_write(filename_ext, channel_data_json_str) + + + + + + +# ARCHIVAL SERIALIZERS +################################################################################ + +NODE_ATTRIBUTES = [ + # ids + 'kind_id', + 'id', + 'source_domain', + 'source_id', + 'content_id', + 'node_id', + # data + 'title', + 'description', + 'language', + 'author', + 'aggregator', + 'provider', + 'thumbnail_encoding', + # licensing metadata + 'license_id', + 'license_description', + 'copyright_holder', + # domain-specific metadata + 'role_visibility', + # content provenance + 'original_node_id', + 'cloned_source_id', + 'original_channel_id', + 'source_channel_id', + 'original_source_node_id', + 'source_node_id', + # workflows + 'publishing', + 'published', + 'complete', + 'changed', + 'freeze_authoring_data', + # structural + 'parent_id', + 'sort_order', + # via MPTTModel + 'tree_id', + 'level', + 'lft', 'rght', + # timestamps + 'created', + 'modified', + # kind-specific extended attributes + 'extra_fields', +] + + +API_NODE_ATTRIBUTES = [ + "id", + "title", + "description", + "kind", + "language", + "license", + "license_description", + "copyright_holder", + "author", + "role_visibility", + "aggregator", + "provider", + "extra_fields", + "thumbnail_encoding", + "parent", + "complete", + "changed", + "tags", +] + + + +class ContentNodeArchiveSerializer(serializers.ModelSerializer): + """ + This is a write only serializer - we leverage it to do create and update + operations, but read operations are handled by the Viewset. + """ + # extra_fields = ExtraFieldsSerializer(required=False) + # tags = TagField(required=False) + + class Meta: + model = ContentNode + fields = NODE_ATTRIBUTES + + + + + + + + +# SAVE +################################################################################ + +settings_ARCHIVES_ROOT = 'tmpcontent/archives' # TODO: move me to GCP bucket + # i'm thinking archives could be + # a sibling of content/databases + # and content/storage +if not os.path.exists(settings_ARCHIVES_ROOT): + os.makedirs(settings_ARCHIVES_ROOT, exist_ok=True) + + +def tmpcontent_write(filename_ext, jsondata): + save_to_path = os.path.join(settings_ARCHIVES_ROOT, filename_ext) + with open(save_to_path, 'w') as outf: + outf.write(jsondata) + +# TODO (continued): replace tmpcontent_write; sample code below +# def write(self, *args, **kwargs): +# try: +# filepath = self.get_write_to_path() +# self._write_details(filepath) +# saved_filename = "{}.{}".format(self.filename, self.ext) +# save_to_path = os.path.sep.join([settings_settings_ARCHIVES_ROOT, saved_filename]) +# # Write file to default storage +# with open(filepath, 'rb') as fobj: +# default_storage.save(save_to_path, fobj) +# return save_to_path +# finally: +# self.delete_tempfiles() From 1aaaab5dd1f50e4637825358420be2437604e10e Mon Sep 17 00:00:00 2001 From: Ivan Savov Date: Wed, 9 Dec 2020 01:11:56 -0500 Subject: [PATCH 2/6] Will it recurse? Yes it will! --- .../contentcuration/utils/archive.py | 36 +++++-------------- 1 file changed, 8 insertions(+), 28 deletions(-) diff --git a/contentcuration/contentcuration/utils/archive.py b/contentcuration/contentcuration/utils/archive.py index 34e8f2627d..1848b4e5de 100644 --- a/contentcuration/contentcuration/utils/archive.py +++ b/contentcuration/contentcuration/utils/archive.py @@ -79,45 +79,25 @@ def archive_channel(channel_id, tree='main'): 'extra_fields', ] - -API_NODE_ATTRIBUTES = [ - "id", - "title", - "description", - "kind", - "language", - "license", - "license_description", - "copyright_holder", - "author", - "role_visibility", - "aggregator", - "provider", - "extra_fields", - "thumbnail_encoding", - "parent", - "complete", - "changed", - "tags", +NODE_RELATIONS = [ + 'children', ] class ContentNodeArchiveSerializer(serializers.ModelSerializer): """ - This is a write only serializer - we leverage it to do create and update - operations, but read operations are handled by the Viewset. + This is a read-only serializer used for channel archiving. """ - # extra_fields = ExtraFieldsSerializer(required=False) - # tags = TagField(required=False) class Meta: model = ContentNode - fields = NODE_ATTRIBUTES - - - + fields = NODE_ATTRIBUTES + NODE_RELATIONS + def get_fields(self): + fields = super(ContentNodeArchiveSerializer, self).get_fields() + fields['children'] = ContentNodeArchiveSerializer(many=True) + return fields From dbca1ea598d39309c69c4d35a75e7d032597f917 Mon Sep 17 00:00:00 2001 From: Ivan Savov Date: Wed, 9 Dec 2020 09:42:52 -0500 Subject: [PATCH 3/6] Include channel metadata in archive json --- .gitignore | 3 + .../contentcuration/utils/archive.py | 98 +++++++++++++++++-- 2 files changed, 91 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 041eaf3fbb..e2c5afe719 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# TMP archival dir for local dev +tmpcontent/archives + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/contentcuration/contentcuration/utils/archive.py b/contentcuration/contentcuration/utils/archive.py index 1848b4e5de..42f0d3f762 100644 --- a/contentcuration/contentcuration/utils/archive.py +++ b/contentcuration/contentcuration/utils/archive.py @@ -11,17 +11,41 @@ # TASKS ################################################################################ -def archive_channel(channel_id, tree='main'): - # channel_id = '79137650868d58a89f12f28aaceec690' +def archive_channel_tree(channel_id, tree='main'): + """ + Convert the `tree`_tree of `channel_id` to JSON and save it to archives dir. + """ channel = Channel.objects.get(id=channel_id) + # 1. serialize tree root = getattr(channel, tree + '_tree') - serializer = ContentNodeArchiveSerializer(root) - channel_data = serializer.data - channel_data_json_str = json.dumps(serializer.data, indent=4, ensure_ascii=False) + tree_serializer = ContentNodeArchiveSerializer(root) + tree_data = tree_serializer.data + + # 2. get channel attributes + channel_serializer = ChannelMetadataArchiveSerializer(channel) + channel_data = channel_serializer.data + + # 3. manually transplant attributes from tree root node onto channel node + # TODO: review if all these are necessay and archive-worthy + channel_data['children'] = tree_data['children'] + channel_data['tree_name'] = tree + '_tree' # to know what we're archiving + channel_data['tree_id'] = tree_data['tree_id'] # to know what we're archiving + channel_data['created'] = tree_data['created'] + channel_data['modified'] = tree_data['modified'] + channel_data['extra_fields'] = tree_data['extra_fields'] + channel_data['publishing'] = tree_data['publishing'] + channel_data['published'] = tree_data['published'] + channel_data['complete'] = tree_data['complete'] + channel_data['changed'] = tree_data['changed'] + channel_data['freeze_authoring_data'] = tree_data['freeze_authoring_data'] + + # 4. dict -> json + tree_data_json_str = json.dumps(channel_data, indent=4, ensure_ascii=False) + + # 5. save dat archive_time = datetime.now().strftime("%Y-%m-%d__%H%M") filename_ext = channel_id + '_' + tree + '_' + archive_time + '.json' - tmpcontent_write(filename_ext, channel_data_json_str) - + tmpcontent_write(filename_ext, tree_data_json_str) @@ -83,12 +107,16 @@ def archive_channel(channel_id, tree='main'): 'children', ] - - class ContentNodeArchiveSerializer(serializers.ModelSerializer): """ - This is a read-only serializer used for channel archiving. + This is a read-only content node serializer used for channel archiving. """ + # TODO: finish all fields (reuse existing serializers?) + # files + # asssessmet_item + # tags + # license + # prerequisites? class Meta: model = ContentNode @@ -101,6 +129,56 @@ def get_fields(self): +CHANNEL_ATTRIBUTES = [ + 'id', + 'name', + 'description', + 'tagline', + 'version', + 'thumbnail', + 'thumbnail_encoding', + 'language_id', + 'trash_tree_id', + 'clipboard_tree_id', + 'main_tree_id', + 'staging_tree_id', + 'chef_tree_id', + 'previous_tree_id', + 'deleted', + 'public', + 'preferences', + 'content_defaults', + 'priority', + 'last_published', + 'source_url', + 'demo_server_url', + 'source_id', + 'source_domain', + 'ricecooker_version', + 'published_data', + 'icon_encoding', + 'total_resource_count', + 'published_kind_count', + 'published_size', +] + +CHANNEL_RELATIONS = [] + +class ChannelMetadataArchiveSerializer(serializers.ModelSerializer): + """ + This is a read-only channel metadata serializer used for channel archiving. + """ + # TODO: finish all fields + # editors? + # viewers? + # secret_tokens + + class Meta: + model = Channel + fields = CHANNEL_ATTRIBUTES + CHANNEL_RELATIONS + + + # SAVE ################################################################################ From ab75cff4423825148034686d39d1e2b151476832 Mon Sep 17 00:00:00 2001 From: Ivan Savov Date: Wed, 9 Dec 2020 10:29:01 -0500 Subject: [PATCH 4/6] Added archivechanneltree command --- .../management/commands/archivechanneltree.py | 27 +++++++++++++++++++ .../contentcuration/utils/archive.py | 4 ++- 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 contentcuration/contentcuration/management/commands/archivechanneltree.py diff --git a/contentcuration/contentcuration/management/commands/archivechanneltree.py b/contentcuration/contentcuration/management/commands/archivechanneltree.py new file mode 100644 index 0000000000..86857d49c3 --- /dev/null +++ b/contentcuration/contentcuration/management/commands/archivechanneltree.py @@ -0,0 +1,27 @@ +import os +import logging as logmodule + +from django.core.management.base import BaseCommand + +from contentcuration.utils.archive import archive_channel_tree + +logmodule.basicConfig() +logging = logmodule.getLogger(__name__) + + +class Command(BaseCommand): + def add_arguments(self, parser): + parser.add_argument("channel_id", type=str) + parser.add_argument("--tree", default='main') + + def handle(self, *args, **options): + channel_id = options["channel_id"] + tree = options["tree"] + + archive_path = archive_channel_tree(channel_id, tree=tree) + + if os.path.exists(archive_path): + self.stdout.write("Archive saved to {}".format(archive_path)) + else: + logging.warning("Channel archive failed.") + diff --git a/contentcuration/contentcuration/utils/archive.py b/contentcuration/contentcuration/utils/archive.py index 42f0d3f762..f10519a4e2 100644 --- a/contentcuration/contentcuration/utils/archive.py +++ b/contentcuration/contentcuration/utils/archive.py @@ -45,7 +45,8 @@ def archive_channel_tree(channel_id, tree='main'): # 5. save dat archive_time = datetime.now().strftime("%Y-%m-%d__%H%M") filename_ext = channel_id + '_' + tree + '_' + archive_time + '.json' - tmpcontent_write(filename_ext, tree_data_json_str) + save_to_path = tmpcontent_write(filename_ext, tree_data_json_str) + return save_to_path @@ -195,6 +196,7 @@ def tmpcontent_write(filename_ext, jsondata): save_to_path = os.path.join(settings_ARCHIVES_ROOT, filename_ext) with open(save_to_path, 'w') as outf: outf.write(jsondata) + return save_to_path # TODO (continued): replace tmpcontent_write; sample code below # def write(self, *args, **kwargs): From d3484704fb15099cfc9e7b925277fd2a73104524 Mon Sep 17 00:00:00 2001 From: Ivan Savov Date: Wed, 9 Dec 2020 12:13:12 -0500 Subject: [PATCH 5/6] Added files and assessment_items that look like API This is minimal additions to make sure JSON archive format really works with treediffer preset="studio" defined in https://github.com/learningequality/treediffer/blob/master/src/treediffer/presets.py#L39-L80 --- .../contentcuration/utils/archive.py | 87 ++++++++++++++++--- 1 file changed, 77 insertions(+), 10 deletions(-) diff --git a/contentcuration/contentcuration/utils/archive.py b/contentcuration/contentcuration/utils/archive.py index f10519a4e2..e73f53296a 100644 --- a/contentcuration/contentcuration/utils/archive.py +++ b/contentcuration/contentcuration/utils/archive.py @@ -4,9 +4,13 @@ from rest_framework import serializers +from contentcuration.models import AssessmentItem from contentcuration.models import Channel from contentcuration.models import ContentNode - +from contentcuration.models import File +from contentcuration.viewsets.assessmentitem import AssessmentItemSerializer +from contentcuration.viewsets.file import FileSerializer +from contentcuration.viewsets.file import retrieve_storage_url # TASKS ################################################################################ @@ -55,6 +59,14 @@ def archive_channel_tree(channel_id, tree='main'): # ARCHIVAL SERIALIZERS ################################################################################ +# NOTE: the NODE_ATTRIBUTES was obtained from node.__dict__.keys() and is a complete +# picture of a content node (maybe too complete!). This is because ContentNodeSerializer +# only ouputs a subset of fields +# https://github.com/learningequality/studio/blob/develop/contentcuration/contentcuration/viewsets/contentnode.py#L219-L238 +# while for archival purposes we want ot have the complete picture. +# TODO: do not reinvent the wheel: try to ContentNodeSerializer if possible so as +# not to create a "custom json" format but reuse same structure as API json. + NODE_ATTRIBUTES = [ # ids 'kind_id', @@ -89,14 +101,14 @@ def archive_channel_tree(channel_id, tree='main'): 'published', 'complete', 'changed', - 'freeze_authoring_data', + 'freeze_authoring_data', # needed? # structural 'parent_id', 'sort_order', # via MPTTModel 'tree_id', - 'level', - 'lft', 'rght', + 'level', # TODO: remove me (info not neeeded) + 'lft', 'rght', # TODO: remove me (info not neeeded) # timestamps 'created', 'modified', @@ -106,18 +118,73 @@ def archive_channel_tree(channel_id, tree='main'): NODE_RELATIONS = [ 'children', + 'files', + 'assessment_items', ] + +# copied from +# https://github.com/learningequality/studio/blob/develop/contentcuration/contentcuration/viewsets/file.py#L74-L95 + +class FileArchiveSerializer(FileSerializer): + class Meta: + model = File + fields = ( + "id", + "checksum", + "file_size", + "language", + "file_format", + "contentnode_id", + "assessment_item_id", + "file_on_disk", + "preset_id", + "language_id", + "original_filename", + "uploaded_by", + ) + field_map = { + "url": retrieve_storage_url, + "preset": "preset_id", + "language": "language_id", + "contentnode": "contentnode_id", + "assessment_item": "assessment_item_id", + } + + +# copied from +# https://github.com/learningequality/studio/blob/develop/contentcuration/contentcuration/viewsets/assessmentitem.py#L202-L218 + +class AssessmentItemArchiveSerializer(AssessmentItemSerializer): + class Meta: + model = AssessmentItem + fields = ( + "question", + "type", + "answers", + "contentnode_id", + "assessment_id", + "hints", + "raw_data", + "order", + "source_url", + "randomize", + "deleted", + ) + field_map = { + "contentnode": "contentnode_id", + } + class ContentNodeArchiveSerializer(serializers.ModelSerializer): """ This is a read-only content node serializer used for channel archiving. """ - # TODO: finish all fields (reuse existing serializers?) - # files - # asssessmet_item - # tags - # license - # prerequisites? + files = FileArchiveSerializer(many=True) + assessment_items = AssessmentItemArchiveSerializer(many=True) + # TODO: finish all fields (reusing existing serializers as much as possible) + # tags = TagField(required=False)? + # license as nested obj? + # prerequisites? class Meta: model = ContentNode From a3f3cdb7f7ae92c121fb31356e4e5556b648096c Mon Sep 17 00:00:00 2001 From: Ivan Savov Date: Wed, 9 Dec 2020 13:11:22 -0500 Subject: [PATCH 6/6] Black strings --- .../contentcuration/utils/archive.py | 193 +++++++++--------- 1 file changed, 98 insertions(+), 95 deletions(-) diff --git a/contentcuration/contentcuration/utils/archive.py b/contentcuration/contentcuration/utils/archive.py index e73f53296a..dbe4c58a47 100644 --- a/contentcuration/contentcuration/utils/archive.py +++ b/contentcuration/contentcuration/utils/archive.py @@ -15,13 +15,14 @@ # TASKS ################################################################################ -def archive_channel_tree(channel_id, tree='main'): + +def archive_channel_tree(channel_id, tree="main"): """ Convert the `tree`_tree of `channel_id` to JSON and save it to archives dir. """ channel = Channel.objects.get(id=channel_id) # 1. serialize tree - root = getattr(channel, tree + '_tree') + root = getattr(channel, tree + "_tree") tree_serializer = ContentNodeArchiveSerializer(root) tree_data = tree_serializer.data @@ -31,31 +32,28 @@ def archive_channel_tree(channel_id, tree='main'): # 3. manually transplant attributes from tree root node onto channel node # TODO: review if all these are necessay and archive-worthy - channel_data['children'] = tree_data['children'] - channel_data['tree_name'] = tree + '_tree' # to know what we're archiving - channel_data['tree_id'] = tree_data['tree_id'] # to know what we're archiving - channel_data['created'] = tree_data['created'] - channel_data['modified'] = tree_data['modified'] - channel_data['extra_fields'] = tree_data['extra_fields'] - channel_data['publishing'] = tree_data['publishing'] - channel_data['published'] = tree_data['published'] - channel_data['complete'] = tree_data['complete'] - channel_data['changed'] = tree_data['changed'] - channel_data['freeze_authoring_data'] = tree_data['freeze_authoring_data'] + channel_data["children"] = tree_data["children"] + channel_data["tree_name"] = tree + "_tree" # to know what we're archiving + channel_data["tree_id"] = tree_data["tree_id"] # to know what we're archiving + channel_data["created"] = tree_data["created"] + channel_data["modified"] = tree_data["modified"] + channel_data["extra_fields"] = tree_data["extra_fields"] + channel_data["publishing"] = tree_data["publishing"] + channel_data["published"] = tree_data["published"] + channel_data["complete"] = tree_data["complete"] + channel_data["changed"] = tree_data["changed"] + channel_data["freeze_authoring_data"] = tree_data["freeze_authoring_data"] # 4. dict -> json tree_data_json_str = json.dumps(channel_data, indent=4, ensure_ascii=False) # 5. save dat archive_time = datetime.now().strftime("%Y-%m-%d__%H%M") - filename_ext = channel_id + '_' + tree + '_' + archive_time + '.json' + filename_ext = channel_id + "_" + tree + "_" + archive_time + ".json" save_to_path = tmpcontent_write(filename_ext, tree_data_json_str) return save_to_path - - - # ARCHIVAL SERIALIZERS ################################################################################ @@ -69,63 +67,65 @@ def archive_channel_tree(channel_id, tree='main'): NODE_ATTRIBUTES = [ # ids - 'kind_id', - 'id', - 'source_domain', - 'source_id', - 'content_id', - 'node_id', + "kind_id", + "id", + "source_domain", + "source_id", + "content_id", + "node_id", # data - 'title', - 'description', - 'language', - 'author', - 'aggregator', - 'provider', - 'thumbnail_encoding', + "title", + "description", + "language", + "author", + "aggregator", + "provider", + "thumbnail_encoding", # licensing metadata - 'license_id', - 'license_description', - 'copyright_holder', + "license_id", + "license_description", + "copyright_holder", # domain-specific metadata - 'role_visibility', + "role_visibility", # content provenance - 'original_node_id', - 'cloned_source_id', - 'original_channel_id', - 'source_channel_id', - 'original_source_node_id', - 'source_node_id', + "original_node_id", + "cloned_source_id", + "original_channel_id", + "source_channel_id", + "original_source_node_id", + "source_node_id", # workflows - 'publishing', - 'published', - 'complete', - 'changed', - 'freeze_authoring_data', # needed? + "publishing", + "published", + "complete", + "changed", + "freeze_authoring_data", # needed? # structural - 'parent_id', - 'sort_order', + "parent_id", + "sort_order", # via MPTTModel - 'tree_id', - 'level', # TODO: remove me (info not neeeded) - 'lft', 'rght', # TODO: remove me (info not neeeded) + "tree_id", + "level", # TODO: remove me (info not neeeded) + "lft", + "rght", # TODO: remove me (info not neeeded) # timestamps - 'created', - 'modified', + "created", + "modified", # kind-specific extended attributes - 'extra_fields', + "extra_fields", ] NODE_RELATIONS = [ - 'children', - 'files', - 'assessment_items', + "children", + "files", + "assessment_items", ] # copied from # https://github.com/learningequality/studio/blob/develop/contentcuration/contentcuration/viewsets/file.py#L74-L95 + class FileArchiveSerializer(FileSerializer): class Meta: model = File @@ -155,6 +155,7 @@ class Meta: # copied from # https://github.com/learningequality/studio/blob/develop/contentcuration/contentcuration/viewsets/assessmentitem.py#L202-L218 + class AssessmentItemArchiveSerializer(AssessmentItemSerializer): class Meta: model = AssessmentItem @@ -175,10 +176,12 @@ class Meta: "contentnode": "contentnode_id", } + class ContentNodeArchiveSerializer(serializers.ModelSerializer): """ This is a read-only content node serializer used for channel archiving. """ + files = FileArchiveSerializer(many=True) assessment_items = AssessmentItemArchiveSerializer(many=True) # TODO: finish all fields (reusing existing serializers as much as possible) @@ -192,50 +195,51 @@ class Meta: def get_fields(self): fields = super(ContentNodeArchiveSerializer, self).get_fields() - fields['children'] = ContentNodeArchiveSerializer(many=True) + fields["children"] = ContentNodeArchiveSerializer(many=True) return fields - CHANNEL_ATTRIBUTES = [ - 'id', - 'name', - 'description', - 'tagline', - 'version', - 'thumbnail', - 'thumbnail_encoding', - 'language_id', - 'trash_tree_id', - 'clipboard_tree_id', - 'main_tree_id', - 'staging_tree_id', - 'chef_tree_id', - 'previous_tree_id', - 'deleted', - 'public', - 'preferences', - 'content_defaults', - 'priority', - 'last_published', - 'source_url', - 'demo_server_url', - 'source_id', - 'source_domain', - 'ricecooker_version', - 'published_data', - 'icon_encoding', - 'total_resource_count', - 'published_kind_count', - 'published_size', + "id", + "name", + "description", + "tagline", + "version", + "thumbnail", + "thumbnail_encoding", + "language_id", + "trash_tree_id", + "clipboard_tree_id", + "main_tree_id", + "staging_tree_id", + "chef_tree_id", + "previous_tree_id", + "deleted", + "public", + "preferences", + "content_defaults", + "priority", + "last_published", + "source_url", + "demo_server_url", + "source_id", + "source_domain", + "ricecooker_version", + "published_data", + "icon_encoding", + "total_resource_count", + "published_kind_count", + "published_size", ] CHANNEL_RELATIONS = [] + class ChannelMetadataArchiveSerializer(serializers.ModelSerializer): """ This is a read-only channel metadata serializer used for channel archiving. """ + # TODO: finish all fields # editors? # viewers? @@ -246,25 +250,24 @@ class Meta: fields = CHANNEL_ATTRIBUTES + CHANNEL_RELATIONS - - # SAVE ################################################################################ -settings_ARCHIVES_ROOT = 'tmpcontent/archives' # TODO: move me to GCP bucket - # i'm thinking archives could be - # a sibling of content/databases - # and content/storage +settings_ARCHIVES_ROOT = "tmpcontent/archives" +# TODO: move me to GCP bucket. This dir could be a sibling of content/databases +# and content/storage, like content/archives/jsontrees/{channel_id}/?/?.json + if not os.path.exists(settings_ARCHIVES_ROOT): os.makedirs(settings_ARCHIVES_ROOT, exist_ok=True) def tmpcontent_write(filename_ext, jsondata): save_to_path = os.path.join(settings_ARCHIVES_ROOT, filename_ext) - with open(save_to_path, 'w') as outf: + with open(save_to_path, "w") as outf: outf.write(jsondata) return save_to_path + # TODO (continued): replace tmpcontent_write; sample code below # def write(self, *args, **kwargs): # try: