Repository: learningequality/kolibri

Issue:
Remove use of MPTT in Collection hierarchy
## Observed behavior
Having to write MPTT fields has caused issues in the past including:
* Slow writes when creating AdHoc groups for large collection hierarchies
* Corrupted MPTT trees when doing data synchronization

Once https://github.com/learningequality/kolibri/pull/7928 is merged, we will no longer need MPTT fields to describe our collection hierarchy, as all role permissions are explicitly granted, and the collection hierarchy made concrete.

## Expected behavior
We should maintain a `parent` `ForeignKey` field in order to continue to encode the parent relationships between collections, but can drop all the other MPTT specific fields from the model. Simplest solution would be to change the base class from MPTTModel to a Django Model.

We should also ensure that we sort collections by name in the frontend to ensure that we have a consistent ordering, now that it will no longer be ordered by 'lft' (which roughly correlates to the order of creation).

## User-facing consequences
More scalable data.



Discussion:

Resolving patch:
diff --git a/kolibri/core/auth/migrations/0019_collection_no_mptt.py b/kolibri/core/auth/migrations/0019_collection_no_mptt.py
new file mode 100644
--- /dev/null
+++ b/kolibri/core/auth/migrations/0019_collection_no_mptt.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.29 on 2021-05-01 16:46
+from __future__ import unicode_literals
+
+import django.db.models.deletion
+from django.db import migrations
+from django.db import models
+
+
+class MPTTRemoveField(migrations.RemoveField):
+    def database_backwards(self, app_label, schema_editor, from_state, to_state):
+        to_model = to_state.apps.get_model(app_label, self.model_name)
+        if self.allow_migrate_model(schema_editor.connection.alias, to_model):
+            from_model = from_state.apps.get_model(app_label, self.model_name)
+            to_field = to_model._meta.get_field(self.name)
+            to_field.default = 0
+            schema_editor.add_field(from_model, to_field)
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("kolibriauth", "0018_no_i18n_collection_kinds"),
+    ]
+
+    operations = [
+        MPTTRemoveField(
+            model_name="collection",
+            name="level",
+        ),
+        MPTTRemoveField(
+            model_name="collection",
+            name="lft",
+        ),
+        MPTTRemoveField(
+            model_name="collection",
+            name="rght",
+        ),
+        MPTTRemoveField(
+            model_name="collection",
+            name="tree_id",
+        ),
+        migrations.AlterField(
+            model_name="collection",
+            name="parent",
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="children",
+                to="kolibriauth.Collection",
+            ),
+        ),
+    ]
diff --git a/kolibri/core/auth/models.py b/kolibri/core/auth/models.py
--- a/kolibri/core/auth/models.py
+++ b/kolibri/core/auth/models.py
@@ -38,8 +38,6 @@
 from django.db.utils import IntegrityError
 from django.utils.encoding import python_2_unicode_compatible
 from morango.models import Certificate
-from morango.models import MorangoMPTTModel
-from morango.models import MorangoMPTTTreeManager
 from morango.models import SyncableModel
 from morango.models import SyncableModelManager
 from mptt.models import TreeForeignKey
@@ -907,7 +905,7 @@ def has_module_perms(self, app_label):
 
 
 @python_2_unicode_compatible
-class Collection(MorangoMPTTModel, AbstractFacilityDataModel):
+class Collection(AbstractFacilityDataModel):
     """
     ``Collections`` are hierarchical groups of ``FacilityUsers``, used for grouping users and making decisions about permissions.
     ``FacilityUsers`` can have roles for one or more ``Collections``, by way of obtaining ``Roles`` associated with those ``Collections``.
@@ -931,7 +929,7 @@ class Collection(MorangoMPTTModel, AbstractFacilityDataModel):
     _KIND = None  # Should be overridden in subclasses to specify what "kind" they are
 
     name = models.CharField(max_length=100)
-    parent = TreeForeignKey(
+    parent = models.ForeignKey(
         "self", null=True, blank=True, related_name="children", db_index=True
     )
     kind = models.CharField(max_length=20, choices=collection_kinds.choices)
@@ -1083,10 +1081,12 @@ def remove_member(self, user):
         Membership.objects.filter(user=user, collection=self).delete()
 
     def infer_dataset(self, *args, **kwargs):
+        if self.dataset_id:
+            return self.dataset_id
         if self.parent:
             # subcollections inherit dataset from root of their tree
             # (we can't call `get_root` directly on self, as it won't work if self hasn't yet been saved)
-            return self.parent.get_root().dataset_id
+            return self.parent.dataset_id
         else:
             # the root node (i.e. Facility) must be explicitly tied to a dataset
             return None
@@ -1292,7 +1292,7 @@ def delete(self, **kwargs):
             return super(Role, self).delete(**kwargs)
 
 
-class CollectionProxyManager(MorangoMPTTTreeManager):
+class CollectionProxyManager(SyncableModelManager):
     def get_queryset(self):
         return (
             super(CollectionProxyManager, self)


Test patch: