diff --git a/src/openedx_tagging/rest_api/v1/serializers.py b/src/openedx_tagging/rest_api/v1/serializers.py index 9c5b62a06..92720cd3f 100644 --- a/src/openedx_tagging/rest_api/v1/serializers.py +++ b/src/openedx_tagging/rest_api/v1/serializers.py @@ -218,15 +218,25 @@ class ObjectTagUpdateBodySerializer(serializers.Serializer): # pylint: disable= tagsData = serializers.ListField(child=ObjectTagUpdateByTaxonomySerializer(), required=True) -def validate_tag_value(value, context): +def validate_tag_value(value, context, original_value=None): """ - Validate this tag value is unique within the current taxonomy context and - does not contain forbidden characters. + Validates the incoming request early: + - This tag is unique, not a duplicate. (The model only validates this if you call `full_clean()`.) + - There are no forbidden / reserved characters present. There is an additional + model-side validation for this as well, but we are keeping this so we can validate + the incoming request immediately. """ taxonomy_id = context.get("taxonomy_id") + original_tag = Tag.objects.filter(taxonomy_id=taxonomy_id, value=original_value).first() if original_value else None + tag_id = original_tag.pk if original_tag else None if taxonomy_id is not None: - # Check if tag value already exists within this taxonomy. If so, raise a validation error. queryset = Tag.objects.filter(taxonomy_id=taxonomy_id, value=value) + + # Don't compare tag against itself when validating its updated value. + if tag_id: + queryset = queryset.exclude(pk=tag_id) + + # Check if tag value already exists within this taxonomy. If so, raise a validation error. if queryset.exists(): raise serializers.ValidationError( f'Tag value "{value}" already exists in this taxonomy.', code='unique' @@ -350,7 +360,7 @@ def validate_updated_tag_value(self, value): """ Run validations for the updated tag value. """ - return validate_tag_value(value, self.context) + return validate_tag_value(value, self.context, original_value=self.initial_data.get("tag")) class TaxonomyTagDeleteBodySerializer(serializers.Serializer): # pylint: disable=abstract-method diff --git a/tests/openedx_tagging/test_views.py b/tests/openedx_tagging/test_views.py index a7510d6ac..9851ce0d3 100644 --- a/tests/openedx_tagging/test_views.py +++ b/tests/openedx_tagging/test_views.py @@ -2211,6 +2211,35 @@ def test_update_tag_with_duplicate_value(self): # Check that the error message indicates the duplicate value issue assert "Tag value \"Updated Tag\" already exists in this taxonomy" in str(response.data) + def test_update_tag_change_capitalization(self): + """ + Test that renaming a tag by only changing its capitalization is allowed. + """ + self.client.force_authenticate(user=self.staff) + existing_tag = self.small_taxonomy.tag_set.filter(parent=None).first() + + # Avoiding false positives + assert existing_tag.value is not existing_tag.value.upper() + + update_data = { + "tag": existing_tag.value, + "updated_tag_value": existing_tag.value.upper() + } + + response = self.client.put( + self.small_taxonomy_url, update_data, format="json" + ) + + assert response.status_code == status.HTTP_200_OK + + data = response.data + + # Check that Tag value got updated with the new capitalization + self.assertEqual(data.get("_id"), existing_tag.id) + self.assertEqual(data.get("value"), existing_tag.value.upper()) + self.assertEqual(data.get("parent_value"), existing_tag.parent) + self.assertEqual(data.get("external_id"), existing_tag.external_id) + def test_should_handle_unexpected_errors_gracefully(self): """ Test that if any unexpected error occurs during the processing of the request,