Repository: learningequality/kolibri

Issue:
Make sure we can continue to use direct attachment SQLite channel import
## Observed behavior
* Because the content schema version that is being used on Kolibri is now in advance of the version being published on Studio, Kolibri is falling back to a very slow import

## Expected behavior
We should find a way to still leverage the direct attachment import while still allowing for this backwards compatible import

Options include:
Trying to insert the migration behaviour as part of the attached copy
Running a post-import operation to make the migration


Discussion:

Resolving patch:
diff --git a/kolibri/core/content/utils/channel_import.py b/kolibri/core/content/utils/channel_import.py
--- a/kolibri/core/content/utils/channel_import.py
+++ b/kolibri/core/content/utils/channel_import.py
@@ -841,6 +841,19 @@ def try_detaching_sqlite_database(self):
                 pass
             self._sqlite_db_attached = False
 
+    def execute_post_operations(self, model, post_operations):
+        DestinationTable = self.destination.get_table(model)
+        for operation in post_operations:
+            try:
+                handler = getattr(self, operation)
+                handler(DestinationTable)
+            except AttributeError:
+                raise AttributeError(
+                    "Post operation {} specified for model {} but none found on class".format(
+                        operation, model
+                    )
+                )
+
     def import_channel_data(self):
 
         logger.debug("Beginning channel metadata import")
@@ -859,6 +872,7 @@ def import_channel_data(self):
                     table_mapper = self.generate_table_mapper(mapping.get("per_table"))
                     logger.info("Importing {model} data".format(model=model.__name__))
                     self.table_import(model, row_mapper, table_mapper)
+                    self.execute_post_operations(model, mapping.get("post", []))
                     logger.debug(
                         "{model} data imported after {seconds} seconds".format(
                             model=model.__name__, seconds=time.time() - model_start
@@ -901,20 +915,23 @@ class NoLearningActivitiesChannelImport(ChannelImport):
             "per_row": {
                 "tree_id": "available_tree_id",
                 "available": "default_to_not_available",
-                "learning_activities": "infer_learning_activities_from_kind",
-            }
+            },
+            "post": ["set_learning_activities_from_kind"],
         },
         LocalFile: {"per_row": {"available": "default_to_not_available"}},
         File: {"per_row": {"available": "default_to_not_available"}},
     }
 
-    def infer_learning_activities_from_kind(self, source_object):
-        kind = source_object.kind
-        if kind in kind_activity_map:
-            return kind_activity_map[kind]
+    def set_learning_activities_from_kind(self, ContentNodeTable):
+        for kind, la in kind_activity_map.items():
+            self.destination.execute(
+                ContentNodeTable.update()
+                .where(ContentNodeTable.c.kind == kind)
+                .values(learning_activities=la)
+            )
 
 
-class NoVersionChannelImport(ChannelImport):
+class NoVersionChannelImport(NoLearningActivitiesChannelImport):
     """
     Class defining the schema mapping for importing old content databases (i.e. ones produced before the
     ChannelImport machinery was implemented). The schema mapping below defines how to bring in information
@@ -936,8 +953,8 @@ class NoVersionChannelImport(ChannelImport):
                 "available": "get_none",
                 "license_name": "get_license_name",
                 "license_description": "get_license_description",
-                "learning_activities": "infer_learning_activities_from_kind",
-            }
+            },
+            "post": ["set_learning_activities_from_kind"],
         },
         File: {
             "per_row": {
@@ -973,11 +990,6 @@ class NoVersionChannelImport(ChannelImport):
 
     licenses = {}
 
-    def infer_learning_activities_from_kind(self, source_object):
-        kind = source_object.kind
-        if kind in kind_activity_map:
-            return kind_activity_map[kind]
-
     def infer_channel_id_from_source(self, source_object):
         return self.channel_id
 


Test patch:
diff --git a/kolibri/core/content/test/test_channel_import.py b/kolibri/core/content/test/test_channel_import.py
--- a/kolibri/core/content/test/test_channel_import.py
+++ b/kolibri/core/content/test/test_channel_import.py
@@ -265,6 +265,8 @@ def test_import_channel_methods_called(self, apps_mock, tree_id_mock, BridgeMock
             channel_import, "generate_table_mapper"
         ), patch.object(channel_import, "table_import"), patch.object(
             channel_import, "check_and_delete_existing_channel"
+        ), patch.object(
+            channel_import, "execute_post_operations"
         ):
             channel_import.import_channel_data()
             channel_import.generate_row_mapper.assert_called_once_with(
@@ -275,6 +277,7 @@ def test_import_channel_methods_called(self, apps_mock, tree_id_mock, BridgeMock
             )
             channel_import.table_import.assert_called_once()
             channel_import.check_and_delete_existing_channel.assert_called_once()
+            channel_import.execute_post_operations.assert_called_once()
 
     def test_end(self, apps_mock, tree_id_mock, BridgeMock):
         channel_import = ChannelImport("test")