From 54459f2038c4efad8fc1d5e8037dacdcb7d61e03 Mon Sep 17 00:00:00 2001 From: Jesper Hodge Date: Mon, 20 Apr 2026 10:42:26 -0400 Subject: [PATCH 1/6] fix: tag rename capitalization --- .../rest_api/v1/serializers.py | 13 +++++++--- tests/openedx_tagging/test_views.py | 26 +++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/openedx_tagging/rest_api/v1/serializers.py b/src/openedx_tagging/rest_api/v1/serializers.py index 9c5b62a06..f260bbfed 100644 --- a/src/openedx_tagging/rest_api/v1/serializers.py +++ b/src/openedx_tagging/rest_api/v1/serializers.py @@ -218,15 +218,22 @@ class ObjectTagUpdateBodySerializer(serializers.Serializer): # pylint: disable= tagsData = serializers.ListField(child=ObjectTagUpdateByTaxonomySerializer(), required=True) -def validate_tag_value(value, context): +def validate_tag_value(value: str, context: dict, original_value: str | None = None): """ Validate this tag value is unique within the current taxonomy context and does not contain forbidden characters. """ 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 +357,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..ee5299c55 100644 --- a/tests/openedx_tagging/test_views.py +++ b/tests/openedx_tagging/test_views.py @@ -2211,6 +2211,32 @@ 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() + + 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, From 7ed76bada53abccd941c347aba9879b281acd687 Mon Sep 17 00:00:00 2001 From: Jesper Hodge Date: Mon, 20 Apr 2026 10:55:12 -0400 Subject: [PATCH 2/6] fix: types --- src/openedx_tagging/rest_api/v1/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/openedx_tagging/rest_api/v1/serializers.py b/src/openedx_tagging/rest_api/v1/serializers.py index f260bbfed..b749bae9c 100644 --- a/src/openedx_tagging/rest_api/v1/serializers.py +++ b/src/openedx_tagging/rest_api/v1/serializers.py @@ -218,7 +218,7 @@ class ObjectTagUpdateBodySerializer(serializers.Serializer): # pylint: disable= tagsData = serializers.ListField(child=ObjectTagUpdateByTaxonomySerializer(), required=True) -def validate_tag_value(value: str, context: dict, original_value: str | None = None): +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. From 9a5511859ae02d12f55ee816080bd331d4874e2e Mon Sep 17 00:00:00 2001 From: Jesper Hodge Date: Tue, 21 Apr 2026 13:00:50 -0400 Subject: [PATCH 3/6] fix: test --- tests/openedx_tagging/test_views.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/openedx_tagging/test_views.py b/tests/openedx_tagging/test_views.py index ee5299c55..9851ce0d3 100644 --- a/tests/openedx_tagging/test_views.py +++ b/tests/openedx_tagging/test_views.py @@ -2218,6 +2218,9 @@ def test_update_tag_change_capitalization(self): 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() From c9bfe9437314bd04e95056c0072df4244f55b270 Mon Sep 17 00:00:00 2001 From: Jesper Hodge Date: Wed, 22 Apr 2026 18:37:14 -0400 Subject: [PATCH 4/6] chore: explain double validation --- src/openedx_tagging/rest_api/v1/serializers.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/openedx_tagging/rest_api/v1/serializers.py b/src/openedx_tagging/rest_api/v1/serializers.py index b749bae9c..2b9d3ffdd 100644 --- a/src/openedx_tagging/rest_api/v1/serializers.py +++ b/src/openedx_tagging/rest_api/v1/serializers.py @@ -220,8 +220,10 @@ class ObjectTagUpdateBodySerializer(serializers.Serializer): # pylint: disable= 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 does not validate this sufficiently.) + - 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 From d7869faaf181b4e397ef2166a4ed525175482105 Mon Sep 17 00:00:00 2001 From: Jesper Hodge Date: Wed, 22 Apr 2026 18:40:26 -0400 Subject: [PATCH 5/6] fix: lint --- src/openedx_tagging/rest_api/v1/serializers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/openedx_tagging/rest_api/v1/serializers.py b/src/openedx_tagging/rest_api/v1/serializers.py index 2b9d3ffdd..64a452377 100644 --- a/src/openedx_tagging/rest_api/v1/serializers.py +++ b/src/openedx_tagging/rest_api/v1/serializers.py @@ -222,8 +222,9 @@ def validate_tag_value(value, context, original_value=None): """ Validates the incoming request early: - This tag is unique, not a duplicate. (The model does not validate this sufficiently.) - - 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. + - 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 From 80167062bc4f3624fe00ad15b2bfeae3eeb21658 Mon Sep 17 00:00:00 2001 From: Jesper Hodge <19345795+jesperhodge@users.noreply.github.com> Date: Thu, 23 Apr 2026 09:45:41 -0400 Subject: [PATCH 6/6] fix: PR comment --- src/openedx_tagging/rest_api/v1/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/openedx_tagging/rest_api/v1/serializers.py b/src/openedx_tagging/rest_api/v1/serializers.py index 64a452377..92720cd3f 100644 --- a/src/openedx_tagging/rest_api/v1/serializers.py +++ b/src/openedx_tagging/rest_api/v1/serializers.py @@ -221,7 +221,7 @@ class ObjectTagUpdateBodySerializer(serializers.Serializer): # pylint: disable= def validate_tag_value(value, context, original_value=None): """ Validates the incoming request early: - - This tag is unique, not a duplicate. (The model does not validate this sufficiently.) + - 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.