Repository: learningequality/kolibri

Issue:
Feature Request: Add --manifest-only option to exportcontent
My understanding is that 0.16 will generate a channel manifest during 

`kolibri manage exportcontent`

My request is that you add an option that will not do the export of content but only generate the manifest. This manifest could then be used on another remote install to import from network the same set of content.


```[tasklist]
### Tasks
- [ ] Add --manifest-only command line option to the exportcontent management command
- [ ] If this option is selected, generate the manifest, but skip copying any files (channel database files, and content files)
- [ ] Write tests to confirm the --manifest-only behaviour
```



Discussion:
Thanks @tim-moody - I think this should be relatively feasible, the merging of the manifest files with sequential exports only happens within the manifest files themselves, and doesn't need access to the resources being on disk if I recall. I'll add this to our patch release milestone for 0.16.x.
@rtibbles any idea if Kolibri 0.16 is expected in Q2 or Q3 or Q4?

If you happen to know anyway, _No Obligation!_

([Internet-in-a-Box](https://internet-in-a-box.org) is looking into stronger support for Kolibri, just a rough idea would be great if available!)

- PR iiab/iiab#3504
0.16 will be released in Q2. It is nearly feature complete, and we will start cutting betas rather than alphas in the very near future. 
> 0.16 will be released in Q2. It is nearly feature complete, and we will start cutting betas rather than alphas in the very near future.

@rtibbles any chance we ([Internet-in-a-Box](https://internet-in-a-box.org)) can help beta-test this channel manifest functionality in coming-weeks-or-so ?

If possible anyway, ideally !!

Thank you for considering 📚
Looks interesting! I would love to implement this feature. Can you assign me this ticket? 
Just a little confused about exportcontent command that how is it working, I don't see it in the doc. Any relevant code for this? 
It is a custom Django management command, implemented here: https://github.com/learningequality/kolibri/blob/develop/kolibri/core/content/management/commands/exportcontent.py

This would involve adding an option to the arg parser for the management command and then using that option to conditionalize the behaviour. It should default to `False` :)

As we've targeted this for the 2nd planned patch of the 0.16.x series, please branch from and target any work to the release-v0.16.x branch.

And thank you! I've assigned you!
Alright, Thank you! 

Resolving patch:
diff --git a/kolibri/core/content/management/commands/exportcontent.py b/kolibri/core/content/management/commands/exportcontent.py
--- a/kolibri/core/content/management/commands/exportcontent.py
+++ b/kolibri/core/content/management/commands/exportcontent.py
@@ -60,6 +60,12 @@ def add_arguments(self, parser):
 
         parser.add_argument("channel_id", type=str)
         parser.add_argument("destination", type=str)
+        parser.add_argument(
+            "--manifest-only",
+            action="store_true",
+            default=False,
+            help="Generate only the manifest.json file",
+        )
 
     def update_job_metadata(self, total_bytes_to_transfer, total_resource_count):
         job = get_current_job()
@@ -75,9 +81,6 @@ def handle_async(self, *args, **options):
         data_dir = os.path.realpath(options["destination"])
         node_ids = options["node_ids"]
         exclude_node_ids = options["exclude_node_ids"]
-        logger.info(
-            "Exporting content for channel id {} to {}".format(channel_id, data_dir)
-        )
 
         channel_metadata = ChannelMetadata.objects.get(id=channel_id)
 
@@ -91,19 +94,11 @@ def handle_async(self, *args, **options):
 
         self.update_job_metadata(total_bytes_to_transfer, total_resource_count)
 
-        exported_files = []
-
-        with self.start_progress(
-            total=total_bytes_to_transfer
-        ) as overall_progress_update:
-            for f in files:
-
-                if self.is_cancelled():
-                    break
-
-                dest = self.export_file(f, data_dir, overall_progress_update)
-                if dest:
-                    exported_files.append(dest)
+        # dont copy files if we are only exporting the manifest
+        if not options["manifest_only"]:
+            self.copy_content_files(
+                channel_id, data_dir, files, total_bytes_to_transfer
+            )
 
         # Reraise any cancellation
         self.check_for_cancel()
@@ -120,6 +115,18 @@ def handle_async(self, *args, **options):
         )
         content_manifest.write(manifest_path)
 
+    def copy_content_files(self, channel_id, data_dir, files, total_bytes_to_transfer):
+        logger.info(
+            "Exporting content for channel id {} to {}".format(channel_id, data_dir)
+        )
+        with self.start_progress(
+            total=total_bytes_to_transfer
+        ) as overall_progress_update:
+            for f in files:
+                if self.is_cancelled():
+                    break
+                self.export_file(f, data_dir, overall_progress_update)
+
     def export_file(self, f, data_dir, overall_progress_update):
         filename = get_content_file_name(f)
         try:


Test patch:
diff --git a/kolibri/core/content/test/test_import_export.py b/kolibri/core/content/test/test_import_export.py
--- a/kolibri/core/content/test/test_import_export.py
+++ b/kolibri/core/content/test/test_import_export.py
@@ -2254,6 +2254,33 @@ def test_local_cancel_during_transfer(
         )
         cancel_mock.assert_called_with()
 
+    @patch(
+        "kolibri.core.content.management.commands.exportcontent.Command.copy_content_files"
+    )
+    def test_manifest_only(
+        self,
+        copy_content_files_mock,
+        ContentManifestMock,
+        get_content_nodes_data_mock,
+        get_import_export_nodes_mock,
+    ):
+        get_content_nodes_data_mock.return_value = (
+            1,
+            [LocalFile.objects.values("id", "file_size", "extension").first()],
+            10,
+        )
+
+        # run with manifest-only option
+        call_command(
+            "exportcontent", self.the_channel_id, tempfile.mkdtemp(), manifest_only=True
+        )
+
+        copy_content_files_mock.assert_not_called()
+
+        ContentManifestMock.return_value.write.assert_called_once()
+
+        # Shall be enough mock assertions for now ?
+
 
 class TestFilesToTransfer(TestCase):