Skip to content

Commit

Permalink
feat: Add update taxonomy tag api/rest + tests
Browse files Browse the repository at this point in the history
Also fixed a few things in the add taxonomy tag rest api
  • Loading branch information
yusuf-musleh committed Oct 15, 2023
1 parent 199df54 commit 3c2f198
Show file tree
Hide file tree
Showing 5 changed files with 279 additions and 13 deletions.
18 changes: 11 additions & 7 deletions openedx_tagging/core/tagging/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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)
46 changes: 43 additions & 3 deletions openedx_tagging/core/tagging/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,21 +366,46 @@ 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.")

parent = None
if parent_tag_id:
parent = Tag.objects.get(id=parent_tag_id)
parent = self.tag_set.get(id=parent_tag_id)

tag = Tag.objects.create(
taxonomy=self, value=tag_value, parent=parent, external_id=external_id
)

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.
Expand Down Expand Up @@ -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
11 changes: 11 additions & 0 deletions openedx_tagging/core/tagging/rest_api/v1/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,3 +205,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)
39 changes: 36 additions & 3 deletions openedx_tagging/core/tagging/rest_api/v1/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"""
from __future__ import annotations

from django.db import model
from django.db import models
from django.http import Http404, HttpResponse
from rest_framework import mixins, status
from rest_framework.decorators import action
Expand All @@ -15,6 +15,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,
Expand All @@ -23,7 +24,7 @@
get_taxonomy,
search_tags,
tag_object,
add_tag_to_taxonomy,
update_tag_in_taxonomy,
)
from ...import_export.api import export_tags
from ...import_export.parsers import ParserFormat
Expand All @@ -43,6 +44,7 @@
TaxonomyListQueryParamsSerializer,
TaxonomySerializer,
TaxonomyTagCreateBodySerializer,
TaxonomyTagUpdateBodySerializer,
)
from .utils import view_auth_classes

Expand Down Expand Up @@ -602,9 +604,40 @@ def post(self, request, *args, **kwargs):
new_tag = add_tag_to_taxonomy(
taxonomy, tag, parent_tag_id, external_id
)
except Tag.DoesNotExist as e:
raise Http404("Parent Tag not found") from e
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 Tag.DoesNotExist as e:
raise Http404("Tag not found") from e
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
)
178 changes: 178 additions & 0 deletions tests/openedx_tagging/core/tagging/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1310,6 +1310,22 @@ def test_create_tag_in_taxonomy_with_invalid_parent_tag_id(self):

assert response.status_code == status.HTTP_400_BAD_REQUEST

def test_create_tag_in_taxonomy_with_parent_tag_id_in_other_taxonomy(self):
self.client.force_authenticate(user=self.user)
invalid_parent_tag_id = 1
new_tag_value = "New Child Tag"

create_data = {
"tag": new_tag_value,
"parent_tag_id": invalid_parent_tag_id,
}

response = self.client.post(
self.large_taxonomy_url, create_data, format="json"
)

assert response.status_code == status.HTTP_404_NOT_FOUND

def test_create_tag_in_taxonomy_with_already_existing_value(self):
self.client.force_authenticate(user=self.user)
new_tag_value = "New Tag"
Expand All @@ -1330,3 +1346,165 @@ 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_tag_id_in_other_taxonomy(self):
self.client.force_authenticate(user=self.user)
updated_tag_value = "Updated Tag"

update_data = {
"tag": 1,
"tag_value": updated_tag_value
}

response = self.client.put(
self.large_taxonomy_url, update_data, format="json"
)

assert response.status_code == status.HTTP_404_NOT_FOUND

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

0 comments on commit 3c2f198

Please sign in to comment.