Repository: openedx/openedx-learning (language: python)

Issue:
Data Model experimentation
Very preliminary data modeling experimentation, as I try out some of the ideas in #1.

This PR created for the sake of transparency. It's very likely to get thrown away entirely by the end.

﻿



Discussion / hints:

Fix patch:
diff --git a/openedx_tagging/core/tagging/import_export/actions.py b/openedx_tagging/core/tagging/import_export/actions.py
index b968a6b0..679f36ae 100644
--- a/openedx_tagging/core/tagging/import_export/actions.py
+++ b/openedx_tagging/core/tagging/import_export/actions.py
@@ -373,8 +373,7 @@ class DeleteTag(ImportAction):
     """
 
     def __str__(self) -> str:
-        taxonomy_tag = self._get_tag()
-        return str(_("Delete tag (external_id={external_id})").format(external_id=taxonomy_tag.external_id))
+        return str(_("Delete tag (external_id={external_id})").format(external_id=self.tag.id))
 
     name = "delete"
 
@@ -397,8 +396,11 @@ def execute(self) -> None:
         """
         Delete a tag
         """
-        taxonomy_tag = self._get_tag()
-        taxonomy_tag.delete()
+        try:
+            taxonomy_tag = self._get_tag()
+            taxonomy_tag.delete()
+        except Tag.DoesNotExist:
+            pass  # The tag may be already cascade deleted if the parent tag was deleted
 
 
 class WithoutChanges(ImportAction):
diff --git a/openedx_tagging/core/tagging/import_export/import_plan.py b/openedx_tagging/core/tagging/import_export/import_plan.py
index f58390df..b5ce4130 100644
--- a/openedx_tagging/core/tagging/import_export/import_plan.py
+++ b/openedx_tagging/core/tagging/import_export/import_plan.py
@@ -93,14 +93,16 @@ def _build_delete_actions(self, tags: dict):
                 # Verify if there is not a parent update before
                 if not self._search_parent_update(child.external_id, tag.external_id):
                     # Change parent to avoid delete childs
-                    self._build_action(
-                        UpdateParentTag,
-                        TagItem(
-                            id=child.external_id,
-                            value=child.value,
-                            parent_id=None,
-                        ),
-                    )
+                    if child.external_id not in tags:
+                        # Only update parent if the child is not going to be deleted
+                        self._build_action(
+                            UpdateParentTag,
+                            TagItem(
+                                id=child.external_id,
+                                value=child.value,
+                                parent_id=None,
+                            ),
+                        )
 
             # Delete action
             self._build_action(


Test patch:
diff --git a/tests/openedx_tagging/core/tagging/import_export/test_api.py b/tests/openedx_tagging/core/tagging/import_export/test_api.py
index 9721fec4..039933d4 100644
--- a/tests/openedx_tagging/core/tagging/import_export/test_api.py
+++ b/tests/openedx_tagging/core/tagging/import_export/test_api.py
@@ -8,7 +8,7 @@
 
 import openedx_tagging.core.tagging.import_export.api as import_export_api
 from openedx_tagging.core.tagging.import_export import ParserFormat
-from openedx_tagging.core.tagging.models import LanguageTaxonomy, TagImportTask, TagImportTaskState, Taxonomy
+from openedx_tagging.core.tagging.models import LanguageTaxonomy, Tag, TagImportTask, TagImportTaskState, Taxonomy
 
 from .mixins import TestImportExportMixin
 
@@ -197,3 +197,42 @@ def test_import_with_export_output(self) -> None:
                 if tag.parent:
                     assert new_tag.parent
                     assert tag.parent.external_id == new_tag.parent.external_id
+
+    def test_import_removing_with_childs(self) -> None:
+        """
+        Test import need to remove childs with parents that will also be removed
+        """
+        new_taxonomy = Taxonomy(name="New taxonomy")
+        new_taxonomy.save()
+        level2 = Tag.objects.create(
+            id=1000,
+            external_id="tag_2",
+            value="Tag 2",
+            taxonomy=new_taxonomy,
+        )
+        level1 = Tag.objects.create(
+            id=1001,
+            external_id="tag_1",
+            value="Tag 1",
+            taxonomy=new_taxonomy,
+        )
+        level3 = Tag.objects.create(
+            id=1002,
+            external_id="tag_3",
+            value="Tag 3",
+            taxonomy=new_taxonomy,
+        )
+        level2.parent = level1
+        level2.save()
+
+        level3.parent = level3
+        level3.save()
+
+        # Import with empty tags, to remove all tags
+        importFile = BytesIO(json.dumps({"tags": []}).encode())
+        assert import_export_api.import_tags(
+            new_taxonomy,
+            importFile,
+            ParserFormat.JSON,
+            replace=True,
+        )
diff --git a/tests/openedx_tagging/core/tagging/import_export/test_import_plan.py b/tests/openedx_tagging/core/tagging/import_export/test_import_plan.py
index 915ffb72..c8dbaa0c 100644
--- a/tests/openedx_tagging/core/tagging/import_export/test_import_plan.py
+++ b/tests/openedx_tagging/core/tagging/import_export/test_import_plan.py
@@ -60,23 +60,15 @@ def test_build_delete_actions(self) -> None:
         assert len(self.import_plan.errors) == 0
 
         # Check actions in order
-        # #1 Update parent of 'tag_2'
-        assert self.import_plan.actions[0].name == 'update_parent'
-        assert self.import_plan.actions[0].tag.id == 'tag_2'
-        assert self.import_plan.actions[0].tag.parent_id is None
-        # #2 Delete 'tag_1'
+        # #1 Delete 'tag_1'
+        assert self.import_plan.actions[0].name == 'delete'
+        assert self.import_plan.actions[0].tag.id == 'tag_1'
+        # #2 Delete 'tag_2'
         assert self.import_plan.actions[1].name == 'delete'
-        assert self.import_plan.actions[1].tag.id == 'tag_1'
-        # #3 Delete 'tag_2'
+        assert self.import_plan.actions[1].tag.id == 'tag_2'
+        # #3 Delete 'tag_3'
         assert self.import_plan.actions[2].name == 'delete'
-        assert self.import_plan.actions[2].tag.id == 'tag_2'
-        # #4 Update parent of 'tag_4'
-        assert self.import_plan.actions[3].name == 'update_parent'
-        assert self.import_plan.actions[3].tag.id == 'tag_4'
-        assert self.import_plan.actions[3].tag.parent_id is None
-        # #5 Delete 'tag_3'
-        assert self.import_plan.actions[4].name == 'delete'
-        assert self.import_plan.actions[4].tag.id == 'tag_3'
+        assert self.import_plan.actions[2].tag.id == 'tag_3'
 
     @ddt.data(
         # Test valid actions
@@ -193,10 +185,6 @@ def test_build_delete_actions(self) -> None:
                     'name': 'without_changes',
                     'id': 'tag_4',
                 },
-                {
-                    'name': 'update_parent',
-                    'id': 'tag_2',
-                },
                 {
                     'name': 'delete',
                     'id': 'tag_1',
@@ -330,13 +318,11 @@ def test_generate_actions(self, tags, replace, expected_errors, expected_actions
             "Import plan for Import Taxonomy Test\n"
             "--------------------------------\n"
             "#1: No changes needed for tag (external_id=tag_4)\n"
-            "#2: Update the parent of tag (external_id=tag_2) from parent (external_id=tag_1) "
-            "to parent (external_id=None).\n"
-            "#3: Delete tag (external_id=tag_1)\n"
-            "#4: Delete tag (external_id=tag_2)\n"
-            "#5: Update the parent of tag (external_id=tag_4) from parent (external_id=tag_3) "
+            "#2: Delete tag (external_id=tag_1)\n"
+            "#3: Delete tag (external_id=tag_2)\n"
+            "#4: Update the parent of tag (external_id=tag_4) from parent (external_id=tag_3) "
             "to parent (external_id=None).\n"
-            "#6: Delete tag (external_id=tag_3)\n"
+            "#5: Delete tag (external_id=tag_3)\n"
         ),
     )
     @ddt.unpack