diff --git a/src/backend/core/migrations/0020_remove_document_is_public_and_more.py b/src/backend/core/migrations/0020_remove_document_is_public_and_more.py new file mode 100644 index 00000000..9663131a --- /dev/null +++ b/src/backend/core/migrations/0020_remove_document_is_public_and_more.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.7 on 2025-03-14 14:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0019_alter_user_language_default_to_null"), + ] + + operations = [ + migrations.AddField( + model_name="document", + name="has_deleted_children", + field=models.BooleanField(default=False), + ), + ] diff --git a/src/backend/core/models.py b/src/backend/core/models.py index 4edd40e8..4d6e55fc 100644 --- a/src/backend/core/models.py +++ b/src/backend/core/models.py @@ -486,6 +486,7 @@ class Document(MP_Node, BaseModel): ) deleted_at = models.DateTimeField(null=True, blank=True) ancestors_deleted_at = models.DateTimeField(null=True, blank=True) + has_deleted_children = models.BooleanField(default=False) _content = None @@ -546,6 +547,12 @@ class Document(MP_Node, BaseModel): content_file = ContentFile(bytes_content) default_storage.save(file_key, content_file) + def is_leaf(self): + """ + :returns: True if the node is has no children + """ + return not self.has_deleted_children and self.numchild == 0 + @property def key_base(self): """Key base of the location where the document is stored in object storage.""" @@ -903,7 +910,8 @@ class Document(MP_Node, BaseModel): if self.depth > 1: self._meta.model.objects.filter(pk=self.get_parent().pk).update( - numchild=models.F("numchild") - 1 + numchild=models.F("numchild") - 1, + has_deleted_children=True, ) # Mark all descendants as soft deleted diff --git a/src/backend/core/tests/test_models_documents.py b/src/backend/core/tests/test_models_documents.py index f51857ee..1c9c6227 100644 --- a/src/backend/core/tests/test_models_documents.py +++ b/src/backend/core/tests/test_models_documents.py @@ -1297,3 +1297,47 @@ def test_models_documents_restore_complex_bis(django_assert_num_queries): def test_models_documents_get_select_options(ancestors_links, select_options): """Validate that the "get_select_options" method operates as expected.""" assert models.LinkReachChoices.get_select_options(ancestors_links) == select_options + + +def test_models_documents_children_create_after_sibling_deletion(): + """ + It should be possible to create a new child after all children have been deleted. + """ + + root = factories.DocumentFactory() + assert root.numchild == 0 + assert root.has_deleted_children is False + assert root.is_leaf() is True + child1 = factories.DocumentFactory(parent=root) + child2 = factories.DocumentFactory(parent=root) + + root.refresh_from_db() + assert root.numchild == 2 + assert root.has_deleted_children is False + assert root.is_leaf() is False + + child1.soft_delete() + child2.soft_delete() + root.refresh_from_db() + assert root.numchild == 0 + assert root.has_deleted_children is True + assert root.is_leaf() is False + + factories.DocumentFactory(parent=root) + root.refresh_from_db() + assert root.numchild == 1 + assert root.has_deleted_children is True + assert root.is_leaf() is False + + +def test_models_documents_has_deleted_children(): + """ + A document should have its has_deleted_children attribute set to True if one of its children + has been solf deleted no matter if numchild is 0 or not. + """ + root = factories.DocumentFactory() + child = factories.DocumentFactory(parent=root) + assert root.has_deleted_children is False + child.soft_delete() + root.refresh_from_db() + assert root.has_deleted_children is True