Repository: learningequality/kolibri

Issue:
When upgrading or updating a channel, if they have previously been ordered, the ordering of the channel will be lost
## Observed behavior
Running an upgrade on a channel deletes the ChannelMetadata object, so any previously explicitly set ordering relative to other channels is lost.

It's possible that this ordering is also lost just when adding more resources to an already existing channel without an upgrade, but this needs to be confirmed.

Looking at the code, however, it seems unlikely: https://github.com/learningequality/kolibri/blob/develop/kolibri/core/content/utils/annotation.py#L782 - so this is probably only happening for upgrades.

## Expected behavior
The order of the channel should be preserved across upgrades and updates.

## User-facing consequences
Careful planning of the order of channels is lost.

## Steps to reproduce
1. Import 2 channels.
2. Order the channels so that the currently second one is first.
3. Upgrade the now first channel.
4. See it move to second.

Do a similar procedure, but only import additional resources this time, and see if that bug occurs there.

## Context
 * Kolibri version: 0.15+

## Hints for resolution

To resolve this, we would need to persist any explicitly set ordering across upgrades. This is probably best done in the channel import logic.

In this method: https://github.com/learningequality/kolibri/blob/develop/kolibri/core/content/utils/channel_import.py#L952 the current channel order can be read before the import runs, and then it can be set again on the channel object if the import did indeed run.

Creating a test to ensure this is preserved would also be helpful. This could be added to the channel upgrade tests here: https://github.com/learningequality/kolibri/blob/develop/kolibri/core/content/test/test_channel_upgrade.py


Discussion:
I'm having trouble trying to figure out how to upgrade the channel. I read the docs and thought maybe I could upload my own resource into kolibri and then update _that_ to reproduce this bug but decided to ask first instead 
![image](https://github.com/learningequality/kolibri/assets/75334285/7e05c6a5-cf5b-4104-a70c-312d4780421a)

@im-NL You are on the right track: the easiest way to try to replicate this is to have your own channel(s) on [Kolibri Studio](https://studio.learningequality.org/) (register a new account if you don't have one), update or add some new resources, and publish them to be able to (re)import & update (use the channel token) in your Kolibri installation.

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
@@ -448,7 +448,6 @@ def _sqlite_method(self, model):
         return "INSERT OR REPLACE"
 
     def raw_attached_sqlite_table_import(self, model, table_mapper):
-
         self.check_cancelled()
 
         source_table = self.source.get_table(model)
@@ -595,7 +594,6 @@ def generate_data_with_default(record):
                 yield value if value is not None else default
 
         if not merge:
-
             separator = "\t"
             data_string_iterator = StringIteratorIO(
                 (
@@ -800,7 +798,6 @@ def delete_old_channel_many_to_many_fields(self, channel_id):
             self.destination.execute(query)
 
     def delete_old_channel_tree_data(self, old_tree_id):
-
         # we want to delete all content models, but not "merge models" (ones that might also be used by other channels), and ContentNode last
         models_to_delete = [
             model
@@ -907,10 +904,8 @@ def execute_post_operations(self, model, post_operations):
                 )
 
     def import_channel_data(self):
-
         logger.debug("Beginning channel metadata import")
         start = time.time()
-
         import_ran = False
 
         try:
@@ -950,15 +945,25 @@ def import_channel_data(self):
                 time.time() - start
             )
         )
+
         return import_ran
 
     def run_and_annotate(self):
+        try:
+            old_order = ChannelMetadata.objects.values("order").get(id=self.channel_id)[
+                "order"
+            ]
+        except ChannelMetadata.DoesNotExist:
+            old_order = None
+
         import_ran = self.import_channel_data()
 
         self.end()
 
         if import_ran:
             channel = ChannelMetadata.objects.get(id=self.channel_id)
+            if old_order is not None:
+                channel.order = old_order
             channel.last_updated = local_now()
             channel.partial = self.partial
             try:


Test patch: