🐛(back) check title unicity when renaming an existing one

An item title can be updated. Check title unicity constraint on this
case.
This commit is contained in:
Manuel Raynaud
2025-04-01 13:43:36 +02:00
parent 4558e0aad5
commit 85d7ebb2d3
3 changed files with 103 additions and 10 deletions
+17
View File
@@ -255,6 +255,23 @@ class ItemSerializer(ListItemSerializer):
def create(self, validated_data):
raise NotImplementedError("Create method can not be used.")
def update(self, instance, validated_data):
"""Validate that the title is unique in the current path."""
if (
instance.title != validated_data.get("title")
and instance.depth > 1
and instance.is_item_title_existing(validated_data.get("title"))
):
raise serializers.ValidationError(
{
"title": _(
"An item with this title already exists in the current path."
)
}
)
return super().update(instance, validated_data)
class CreateItemSerializer(ItemSerializer):
"""Serializer used to create a new item"""
+24 -10
View File
@@ -417,6 +417,20 @@ class ItemQuerySet(TreeQuerySet):
return self.filter(models.Q(link_reach=LinkReachChoices.PUBLIC))
def _is_item_title_existing(queryset, title):
"""Check if the title is unique in the same path."""
return (
queryset.filter(title=title)
.filter(
models.Q(
models.Q(deleted_at__isnull=True)
| models.Q(ancestors_deleted_at__isnull=True)
)
)
.exists()
)
class ItemManager(TreeManager):
"""Custom manager for Item model overriding create_child method."""
@@ -440,16 +454,9 @@ class ItemManager(TreeManager):
if parent.type != ItemTypeChoices.FOLDER:
raise ValidationError({"type": _("Only folders can have children.")})
if (
self.children(parent.path)
.filter(title=kwargs.get("title"))
.filter(
models.Q(
models.Q(deleted_at__isnull=True)
| models.Q(ancestors_deleted_at__isnull=True)
)
)
.exists()
if _is_item_title_existing(
self.children(parent.path),
kwargs.get("title"),
):
raise ValidationError(
{"title": _("title already exists in this folder.")}
@@ -603,6 +610,13 @@ class Item(TreeModel, BaseModel):
"""Generate a unique cache key for each item."""
return f"item_{self.id!s}_nb_accesses"
def is_item_title_existing(self, title):
"""Check if the title is unique in the same path."""
return _is_item_title_existing(
self.siblings(),
title,
)
@property
def nb_accesses(self):
"""Calculate the number of accesses."""
@@ -351,3 +351,65 @@ def test_api_items_update_administrator_or_owner_of_another(via, mock_user_teams
other_item.refresh_from_db()
other_item_values = serializers.ItemSerializer(instance=other_item).data
assert other_item_values == old_item_values
def test_api_items_update_title_unique_in_current_path():
"""
The title of an item should be unique in the current path.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
root = factories.ItemFactory(
title="item1", type=models.ItemTypeChoices.FOLDER, users=[user]
)
child = factories.ItemFactory(
title="child1", type=models.ItemTypeChoices.FOLDER, parent=root, users=[user]
)
factories.ItemFactory(
title="child2", type=models.ItemTypeChoices.FOLDER, parent=root, users=[user]
)
# update child1 to rename it to child2 should fails
response = client.put(
f"/api/v1.0/items/{child.id!s}/",
{"title": "child2"},
format="json",
)
assert response.status_code == 400
assert response.json() == {
"title": "An item with this title already exists in the current path."
}
def test_api_items_update_title_unique_in_current_path_soft_deleted():
"""
Reusing a title of a soft-deleted item should be allowed.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
root = factories.ItemFactory(
title="item1", type=models.ItemTypeChoices.FOLDER, users=[user]
)
child = factories.ItemFactory(
title="child1", type=models.ItemTypeChoices.FOLDER, parent=root, users=[user]
)
child.soft_delete()
child2 = factories.ItemFactory(
title="child2", type=models.ItemTypeChoices.FOLDER, parent=root, users=[user]
)
# update child 2 title using child 1 title is allowed
response = client.put(
f"/api/v1.0/items/{child2.id!s}/",
{"title": "child1"},
format="json",
)
assert response.status_code == 200