From 3129f55fddc65c6543604da4b925d54f6625d181 Mon Sep 17 00:00:00 2001 From: Yusuf Musleh Date: Thu, 12 Oct 2023 17:14:57 +0300 Subject: [PATCH] feat: Add update taxonomy tag api/rest + tests Also fixed a few things in the add taxonomy tag rest api --- openedx_tagging/core/tagging/api.py | 18 ++- openedx_tagging/core/tagging/models/base.py | 44 +++++- .../core/tagging/rest_api/v1/serializers.py | 11 ++ .../core/tagging/rest_api/v1/views.py | 33 +++- .../core/tagging/test_views.py | 147 ++++++++++++++++++ 5 files changed, 242 insertions(+), 11 deletions(-) diff --git a/openedx_tagging/core/tagging/api.py b/openedx_tagging/core/tagging/api.py index 2c78b10c..a8a4e7bc 100644 --- a/openedx_tagging/core/tagging/api.py +++ b/openedx_tagging/core/tagging/api.py @@ -129,13 +129,7 @@ def resync_object_tags(object_tags: QuerySet | None = None) -> int: if not object_tags: object_tags = ObjectTag.objects.select_related("tag", "taxonomy") - num_changed = 0 - for object_tag in object_tags: - changed = object_tag.resync() - if changed: - object_tag.save() - num_changed += 1 - return num_changed + return ObjectTag.resync_object_tags(object_tags) def get_object_tags( @@ -329,3 +323,13 @@ def add_tag_to_taxonomy( Tag is returned """ return taxonomy.cast().add_tag(tag, parent_tag_id, external_id) + + +def update_tag_in_taxonomy(taxonomy: Taxonomy, tag: int, tag_value: str): + """ + Update a Tag that belongs to a Taxonomy. The related ObjectTags are + updated accordingly. + + Currently only support updates the Tag value. + """ + return taxonomy.cast().update_tag(tag, tag_value) diff --git a/openedx_tagging/core/tagging/models/base.py b/openedx_tagging/core/tagging/models/base.py index ffcef13e..8035c6ee 100644 --- a/openedx_tagging/core/tagging/models/base.py +++ b/openedx_tagging/core/tagging/models/base.py @@ -366,8 +366,6 @@ def add_tag( "add_tag() doesn't work for system defined taxonomies. They cannot be modified." ) - current_tags = self.get_tags() - if self.tag_set.filter(value__iexact=tag_value).exists(): raise ValueError(f"Tag with value '{tag_value}' already exists for taxonomy.") @@ -381,6 +379,33 @@ def add_tag( return tag + def update_tag(self, tag_id: int, tag_value: str) -> Tag: + """ + Update an existing Tag in Taxonomy and return it. Currently only + supports updating the Tag's value. + """ + self.check_casted() + + if self.allow_free_text: + raise ValueError( + "update_tag() doesn't work for free text taxonomies. They don't use Tag instances." + ) + + if self.system_defined: + raise ValueError( + "update_tag() doesn't work for system defined taxonomies. They cannot be modified." + ) + + # Update Tag instance with new value + tag = self.tag_set.get(id=tag_id) + tag.value = tag_value + tag.save() + + # Resync all related ObjectTags to update to the new Tag value + object_tags = self.objecttag_set.all() + ObjectTag.resync_object_tags(object_tags) + return tag + def validate_value(self, value: str) -> bool: """ Check if 'value' is part of this Taxonomy. @@ -648,3 +673,18 @@ def copy(self, object_tag: ObjectTag) -> Self: self._value = object_tag._value # pylint: disable=protected-access self._name = object_tag._name # pylint: disable=protected-access return self + + @classmethod + def resync_object_tags(cls, object_tags: models.QuerySet[ObjectTag]) -> int: + """ + Reconciles ObjectTag entries with any changes made to their associated + taxonomies and tags. Return the number of changes made. + """ + num_changed = 0 + for object_tag in object_tags: + changed = object_tag.resync() + if changed: + object_tag.save() + num_changed += 1 + + return num_changed diff --git a/openedx_tagging/core/tagging/rest_api/v1/serializers.py b/openedx_tagging/core/tagging/rest_api/v1/serializers.py index 8cb3f5f1..a926bae8 100644 --- a/openedx_tagging/core/tagging/rest_api/v1/serializers.py +++ b/openedx_tagging/core/tagging/rest_api/v1/serializers.py @@ -187,3 +187,14 @@ class TaxonomyTagCreateBodySerializer(serializers.Serializer): # pylint: disabl queryset=Tag.objects.all(), required=False ) external_id = serializers.CharField(required=False) + + +class TaxonomyTagUpdateBodySerializer(serializers.Serializer): # pylint: disable=abstract-method + """ + Serializer of the body for the Taxonomy Tags UPDATE view + """ + + tag = serializers.PrimaryKeyRelatedField( + queryset=Tag.objects.all(), required=True + ) + tag_value = serializers.CharField(required=True) diff --git a/openedx_tagging/core/tagging/rest_api/v1/views.py b/openedx_tagging/core/tagging/rest_api/v1/views.py index 0ca851c5..7cd15f12 100644 --- a/openedx_tagging/core/tagging/rest_api/v1/views.py +++ b/openedx_tagging/core/tagging/rest_api/v1/views.py @@ -14,6 +14,7 @@ from openedx_tagging.core.tagging.models.base import Tag from ...api import ( + add_tag_to_taxonomy, create_taxonomy, get_children_tags, get_object_tags, @@ -22,7 +23,7 @@ get_taxonomy, search_tags, tag_object, - add_tag_to_taxonomy, + update_tag_in_taxonomy, ) from ...models import Taxonomy from ...rules import ChangeObjectTagPermissionItem @@ -39,6 +40,7 @@ TaxonomyListQueryParamsSerializer, TaxonomySerializer, TaxonomyTagCreateBodySerializer, + TaxonomyTagUpdateBodySerializer, ) @@ -547,6 +549,33 @@ def post(self, request, *args, **kwargs): except ValueError as e: raise ValidationError from e + serializer_context = self.get_serializer_context() + return Response( + self.serializer_class(new_tag, context=serializer_context).data, + status=status.HTTP_201_CREATED + ) + + def update(self, request, *args, **kwargs): + """ + Updates a Tag that belongs to the Taxonomy and returns it. + Currently only updating the Tag value is supported. + """ + pk = self.kwargs.get("pk") + taxonomy = self.get_taxonomy(pk) + + body = TaxonomyTagUpdateBodySerializer(data=request.data) + body.is_valid(raise_exception=True) + + tag = body.data.get("tag") + tag_value = body.data.get("tag_value") + + try: + updated_tag = update_tag_in_taxonomy(taxonomy, tag, tag_value) + except ValueError as e: + raise ValidationError from e + + serializer_context = self.get_serializer_context() return Response( - TagsSerializer(new_tag).data, status=status.HTTP_201_CREATED + self.serializer_class(updated_tag, context=serializer_context).data, + status=status.HTTP_200_OK ) diff --git a/tests/openedx_tagging/core/tagging/test_views.py b/tests/openedx_tagging/core/tagging/test_views.py index 107ee76a..9f21a60b 100644 --- a/tests/openedx_tagging/core/tagging/test_views.py +++ b/tests/openedx_tagging/core/tagging/test_views.py @@ -1228,3 +1228,150 @@ def test_create_tag_in_taxonomy_with_already_existing_value(self): ) assert response.status_code == status.HTTP_400_BAD_REQUEST + + def test_update_tag_in_taxonomy_with_different_methods(self): + self.client.force_authenticate(user=self.user) + updated_tag_value = "Updated Tag" + updated_tag_value_2 = "Updated Tag 2" + + # Existing Tag that will be updated + existing_tag = self.small_taxonomy.tag_set.filter(parent=None).first() + + update_data = { + "tag": existing_tag.id, + "tag_value": updated_tag_value + } + + # Test updating using the PUT method + 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 + self.assertEqual(data.get("id"), existing_tag.id) + self.assertEqual(data.get("value"), updated_tag_value) + self.assertEqual(data.get("taxonomy_id"), self.small_taxonomy.pk) + self.assertEqual(data.get("parent_id"), existing_tag.parent) + self.assertEqual(data.get("external_id"), existing_tag.external_id) + + # Test updating using the PATCH method + update_data["tag_value"] = updated_tag_value_2 + response = self.client.patch( + self.small_taxonomy_url, update_data, format="json" + ) + + assert response.status_code == status.HTTP_200_OK + + data = response.data + + # Check the Tag value got updated again + self.assertEqual(data.get("id"), existing_tag.id) + self.assertEqual(data.get("value"), updated_tag_value_2) + self.assertEqual(data.get("taxonomy_id"), self.small_taxonomy.pk) + self.assertEqual(data.get("parent_id"), existing_tag.parent) + self.assertEqual(data.get("external_id"), existing_tag.external_id) + + def test_update_tag_in_taxonomy_reflects_changes_in_object_tags(self): + self.client.force_authenticate(user=self.user) + + existing_tag = self.small_taxonomy.tag_set.filter(parent=None).first() + + # Setup ObjectTags + # _value=existing_tag.value + object_tag_1 = ObjectTag.objects.create( + object_id="abc", taxonomy=self.small_taxonomy, tag=existing_tag + ) + object_tag_2 = ObjectTag.objects.create( + object_id="def", taxonomy=self.small_taxonomy, tag=existing_tag + ) + object_tag_3 = ObjectTag.objects.create( + object_id="ghi", taxonomy=self.small_taxonomy, tag=existing_tag + ) + + assert object_tag_1.value == existing_tag.value + assert object_tag_2.value == existing_tag.value + assert object_tag_3.value == existing_tag.value + + updated_tag_value = "Updated Tag" + update_data = { + "tag": existing_tag.id, + "tag_value": updated_tag_value + } + + # Test updating using the PUT method + 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 + self.assertEqual(data.get("id"), existing_tag.id) + self.assertEqual(data.get("value"), updated_tag_value) + self.assertEqual(data.get("taxonomy_id"), self.small_taxonomy.pk) + self.assertEqual(data.get("parent_id"), existing_tag.parent) + self.assertEqual(data.get("external_id"), existing_tag.external_id) + + # Check that the ObjectTags got updated as well + object_tag_1.refresh_from_db() + self.assertEqual(object_tag_1.value, updated_tag_value) + object_tag_2.refresh_from_db() + self.assertEqual(object_tag_2.value, updated_tag_value) + object_tag_3.refresh_from_db() + self.assertEqual(object_tag_3.value, updated_tag_value) + + def test_update_tag_in_taxonomy_with_invalid_tag_id(self): + self.client.force_authenticate(user=self.user) + updated_tag_value = "Updated Tag" + + update_data = { + "tag": 919191, + "tag_value": updated_tag_value + } + + response = self.client.put( + self.small_taxonomy_url, update_data, format="json" + ) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + + def test_update_tag_in_taxonomy_with_no_tag_value_provided(self): + self.client.force_authenticate(user=self.user) + + # Existing Tag that will be updated + existing_tag = self.small_taxonomy.tag_set.filter(parent=None).first() + + update_data = { + "tag": existing_tag.id + } + + response = self.client.put( + self.small_taxonomy_url, update_data, format="json" + ) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + + def test_update_tag_in_invalid_taxonomy(self): + self.client.force_authenticate(user=self.user) + + # Existing Tag that will be updated + existing_tag = self.small_taxonomy.tag_set.filter(parent=None).first() + + updated_tag_value = "Updated Tag" + update_data = { + "tag": existing_tag.id, + "tag_value": updated_tag_value + } + + invalid_taxonomy_url = TAXONOMY_TAGS_URL.format(pk=919191) + response = self.client.put( + invalid_taxonomy_url, update_data, format="json" + ) + + assert response.status_code == status.HTTP_404_NOT_FOUND