Skip to content

Commit

Permalink
feat: Add delete taxonomy tags api/rest + tests
Browse files Browse the repository at this point in the history
  • Loading branch information
yusuf-musleh committed Oct 15, 2023
1 parent 3c2f198 commit 9c9cfe5
Show file tree
Hide file tree
Showing 5 changed files with 261 additions and 2 deletions.
13 changes: 13 additions & 0 deletions openedx_tagging/core/tagging/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,3 +333,16 @@ def update_tag_in_taxonomy(taxonomy: Taxonomy, tag: int, tag_value: str):
Currently only support updates the Tag value.
"""
return taxonomy.cast().update_tag(tag, tag_value)


def delete_tags_from_taxonomy(
taxonomy: Taxonomy,
tag_ids: list[Tag],
with_subtags: bool
):
"""
Delete Tags that belong to a Taxonomy. If any of the Tags have children and
the `with_subtags` is not set to `True` it will fail, otherwise
the sub-tags will be deleted as well.
"""
return taxonomy.cast().delete_tags(tag_ids, with_subtags)
37 changes: 37 additions & 0 deletions openedx_tagging/core/tagging/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,43 @@ def update_tag(self, tag_id: int, tag_value: str) -> Tag:
ObjectTag.resync_object_tags(object_tags)
return tag

def delete_tags(self, tag_ids: List[int], with_subtags: bool = False):
"""
Delete the Taxonomy Tags provided. If any of them have children and
the `with_subtags` is not set to `True` it will fail, otherwise
the sub-tags will be deleted as well.
"""
self.check_casted()

if self.allow_free_text:
raise ValueError(
"delete_tags() doesn't work for free text taxonomies. They don't use Tag instances."
)

if self.system_defined:
raise ValueError(
"delete_tags() doesn't work for system defined taxonomies. They cannot be modified."
)

tags = self.tag_set.filter(id__in=tag_ids)

if tags.count() != len(tag_ids):
# If they do not match that means there is a Tag ID in the provided
# list that is either invalid or does not belong to this Taxonomy
raise ValueError("Invalid tag id provided or tag id does not belong to taxonomy")

# Check if any Tag contains subtags (children)
contains_children = tags.filter(children__isnull=False).distinct().exists()

if contains_children and not with_subtags:
raise ValueError(
"Tag(s) contain children, `with_subtags` must be `True` for "
"all Tags and their subtags (children) to be deleted."
)

# Delete the Tags with their subtags if any
tags.delete()

def validate_value(self, value: str) -> bool:
"""
Check if 'value' is part of this Taxonomy.
Expand Down
13 changes: 13 additions & 0 deletions openedx_tagging/core/tagging/rest_api/v1/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,3 +216,16 @@ class TaxonomyTagUpdateBodySerializer(serializers.Serializer): # pylint: disabl
queryset=Tag.objects.all(), required=True
)
tag_value = serializers.CharField(required=True)


class TaxonomyTagDeleteBodySerializer(serializers.Serializer): # pylint: disable=abstract-method
"""
Serializer of the body for the Taxonomy Tags DELETE view
"""

tag_ids = serializers.PrimaryKeyRelatedField(
queryset=Tag.objects.all(),
many=True,
required=True
)
with_subtags = serializers.BooleanField(required=False)
75 changes: 73 additions & 2 deletions openedx_tagging/core/tagging/rest_api/v1/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from ...api import (
add_tag_to_taxonomy,
create_taxonomy,
delete_tags_from_taxonomy,
get_children_tags,
get_object_tags,
get_root_tags,
Expand Down Expand Up @@ -44,6 +45,7 @@
TaxonomyListQueryParamsSerializer,
TaxonomySerializer,
TaxonomyTagCreateBodySerializer,
TaxonomyTagDeleteBodySerializer,
TaxonomyTagUpdateBodySerializer,
)
from .utils import view_auth_classes
Expand Down Expand Up @@ -428,6 +430,53 @@ class TaxonomyTagsView(ListAPIView, RetrieveUpdateDestroyAPIView):
* 403 - Permission denied
* 404 - Taxonomy not found
**Update Query Parameters**
* pk (required) - The pk of the taxonomy to update a Tag in
**Update Request Body**
* tag (required): The ID of the Tag that should be updated
* tag_value (required): The updated value of the Tag
**Update Example Requests**
PUT api/tagging/v1/taxonomy/:pk/tags - Update a Tag in Taxonomy
{
"tag": 1,
"tag_value": "Updated Tag Value"
}
PATCH api/tagging/v1/taxonomy/:pk/tags - Update a Tag in Taxonomy
{
"tag": 1,
"tag_value": "Updated Tag Value"
}
**Update Query Returns**
* 200 - Success
* 400 - Invalid parameters provided
* 403 - Permission denied
* 404 - Taxonomy, Tag or Parent Tag not found
**Delete Query Parameters**
* pk (required) - The pk of the taxonomy to Delete Tag(s) in
**Delete Request Body**
* tag_ids (required): The IDs of Tags that should be deleted from Taxonomy
* with_subtags (optional): If a Tag in the provided ids contains
children (subtags), deletion will fail unless
set to `True`. Defaults to `False`.
**Delete Example Requests**
DELETE api/tagging/v1/taxonomy/:pk/tags - Delete Tag(s) in Taxonomy
{
"tag_ids": [1,2,3],
"with_subtags": True
}
**Delete Query Returns**
* 200 - Success
* 400 - Invalid parameters provided
* 403 - Permission denied
* 404 - Taxonomy not found
"""

permission_classes = [TagListPermissions]
Expand Down Expand Up @@ -607,7 +656,7 @@ def post(self, request, *args, **kwargs):
except Tag.DoesNotExist as e:
raise Http404("Parent Tag not found") from e
except ValueError as e:
raise ValidationError from e
raise ValidationError(e) from e

serializer_context = self.get_serializer_context()
return Response(
Expand All @@ -634,10 +683,32 @@ def update(self, request, *args, **kwargs):
except Tag.DoesNotExist as e:
raise Http404("Tag not found") from e
except ValueError as e:
raise ValidationError from e
raise ValidationError(e) from e

serializer_context = self.get_serializer_context()
return Response(
self.serializer_class(updated_tag, context=serializer_context).data,
status=status.HTTP_200_OK
)

def delete(self, request, *args, **kwargs):
"""
Deletes Tag(s) in Taxonomy. If any of the Tags have children and
the `with_subtags` is not set to `True` it will fail, otherwise
the sub-tags will be deleted as well.
"""
pk = self.kwargs.get("pk")
taxonomy = self.get_taxonomy(pk)

body = TaxonomyTagDeleteBodySerializer(data=request.data)
body.is_valid(raise_exception=True)

tag_ids = body.data.get("tag_ids")
with_subtags = body.data.get("with_subtags")

try:
delete_tags_from_taxonomy(taxonomy, tag_ids, with_subtags)
except ValueError as e:
raise ValidationError(e) from e

return Response(status=status.HTTP_200_OK)
125 changes: 125 additions & 0 deletions tests/openedx_tagging/core/tagging/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1508,3 +1508,128 @@ def test_update_tag_in_invalid_taxonomy(self):
)

assert response.status_code == status.HTTP_404_NOT_FOUND

def test_delete_single_tag_from_taxonomy(self):
self.client.force_authenticate(user=self.user)

# Get Tag that will be deleted
existing_tag = self.small_taxonomy.tag_set.filter(parent=None).first()

delete_data = {
"tag_ids": [existing_tag.id],
"with_subtags": True
}

response = self.client.delete(
self.small_taxonomy_url, delete_data, format="json"
)

assert response.status_code == status.HTTP_200_OK

# Check that Tag no longer exists
with self.assertRaises(Tag.DoesNotExist) as exc:
existing_tag.refresh_from_db()

def test_delete_multiple_tags_from_taxonomy(self):
self.client.force_authenticate(user=self.user)

# Get Tags that will be deleted
existing_tags = self.small_taxonomy.tag_set.filter(parent=None)[:3]

delete_data = {
"tag_ids": [existing_tag.id for existing_tag in existing_tags],
"with_subtags": True
}

response = self.client.delete(
self.small_taxonomy_url, delete_data, format="json"
)

assert response.status_code == status.HTTP_200_OK

# Check that Tags no longer exists
for existing_tag in existing_tags:
with self.assertRaises(Tag.DoesNotExist) as exc:
existing_tag.refresh_from_db()

def test_delete_tag_with_subtags_should_fail_without_flag_passed(self):
self.client.force_authenticate(user=self.user)

# Get Tag that will be deleted
existing_tag = self.small_taxonomy.tag_set.filter(parent=None).first()

delete_data = {
"tag_ids": [existing_tag.id]
}

response = self.client.delete(
self.small_taxonomy_url, delete_data, format="json"
)

assert response.status_code == status.HTTP_400_BAD_REQUEST

def test_delete_tag_in_invalid_taxonomy(self):
self.client.force_authenticate(user=self.user)

# Get Tag that will be deleted
existing_tag = self.small_taxonomy.tag_set.filter(parent=None).first()

delete_data = {
"tag_ids": [existing_tag.id]
}

invalid_taxonomy_url = TAXONOMY_TAGS_URL.format(pk=919191)
response = self.client.delete(
invalid_taxonomy_url, delete_data, format="json"
)

assert response.status_code == status.HTTP_404_NOT_FOUND

def test_delete_tag_in_taxonomy_with_invalid_tag_id(self):
self.client.force_authenticate(user=self.user)

delete_data = {
"tag_ids": [91919]
}

response = self.client.delete(
self.small_taxonomy_url, delete_data, format="json"
)

assert response.status_code == status.HTTP_400_BAD_REQUEST

def test_delete_tag_with_tag_id_in_other_taxonomy(self):
self.client.force_authenticate(user=self.user)

# Get Tag in other Taxonomy
tag_in_other_taxonomy = self.small_taxonomy.tag_set.filter(parent=None).first()

delete_data = {
"tag_ids": [tag_in_other_taxonomy.id]
}

response = self.client.delete(
self.large_taxonomy_url, delete_data, format="json"
)

assert response.status_code == status.HTTP_400_BAD_REQUEST

def test_delete_tag_in_taxonomy_without_subtags(self):
self.client.force_authenticate(user=self.user)

# Get Tag that will be deleted
existing_tag = self.small_taxonomy.tag_set.filter(children__isnull=True).first()

delete_data = {
"tag_ids": [existing_tag.id]
}

response = self.client.delete(
self.small_taxonomy_url, delete_data, format="json"
)

assert response.status_code == status.HTTP_200_OK

# Check that Tag no longer exists
with self.assertRaises(Tag.DoesNotExist) as exc:
existing_tag.refresh_from_db()

0 comments on commit 9c9cfe5

Please sign in to comment.