Skip to content

Commit

Permalink
feat: Implement add taxonomy tag 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 ca6bc85 commit 199df54
Show file tree
Hide file tree
Showing 5 changed files with 264 additions and 6 deletions.
14 changes: 14 additions & 0 deletions openedx_tagging/core/tagging/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,3 +315,17 @@ def autocomplete_tags(
# remove repeats
.distinct()
)


def add_tag_to_taxonomy(
taxonomy: Taxonomy,
tag: str,
parent_tag_id: int | None = None,
external_id: str | None = None
) -> Tag:
"""
Adds a new Tag to provided Taxonomy. If a Tag already exists in the
Taxonomy, an exception is raised, otherwise the newly created
Tag is returned
"""
return taxonomy.cast().add_tag(tag, parent_tag_id, external_id)
39 changes: 39 additions & 0 deletions openedx_tagging/core/tagging/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,45 @@ def get_filtered_tags(

return tag_set.order_by("value", "id")

def add_tag(
self,
tag_value: str,
parent_tag_id: int | None = None,
external_id: str | None = None

) -> Tag:
"""
Add new Tag to Taxonomy. If an existing Tag with the `tag_value` already
exists in the Taxonomy, an exception is raised, otherwise the newly
created Tag is returned
"""
self.check_casted()

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

if self.system_defined:
raise ValueError(
"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)

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

return tag

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 @@ -110,6 +110,7 @@ class Meta:
"value",
"taxonomy_id",
"parent_id",
"external_id",
"sub_tags_link",
"children_count",
)
Expand Down Expand Up @@ -192,3 +193,15 @@ def get_children_count(self, obj):
Returns the number of child tags of the given tag.
"""
return len(obj.sub_tags)


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

tag = serializers.CharField(required=True)
parent_tag_id = serializers.PrimaryKeyRelatedField(
queryset=Tag.objects.all(), required=False
)
external_id = serializers.CharField(required=False)
62 changes: 57 additions & 5 deletions openedx_tagging/core/tagging/rest_api/v1/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
"""
from __future__ import annotations

from django.db import models
from django.db import model
from django.http import Http404, HttpResponse
from rest_framework import mixins
from rest_framework import mixins, status
from rest_framework.decorators import action
from rest_framework.exceptions import MethodNotAllowed, PermissionDenied, ValidationError
from rest_framework.generics import ListAPIView
from rest_framework.generics import ListAPIView, RetrieveUpdateDestroyAPIView
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet, ModelViewSet

Expand All @@ -23,6 +23,7 @@
get_taxonomy,
search_tags,
tag_object,
add_tag_to_taxonomy,
)
from ...import_export.api import export_tags
from ...import_export.parsers import ParserFormat
Expand All @@ -41,6 +42,7 @@
TaxonomyExportQueryParamsSerializer,
TaxonomyListQueryParamsSerializer,
TaxonomySerializer,
TaxonomyTagCreateBodySerializer,
)
from .utils import view_auth_classes

Expand Down Expand Up @@ -380,9 +382,9 @@ def update(self, request, *args, **kwargs):


@view_auth_classes
class TaxonomyTagsView(ListAPIView):
class TaxonomyTagsView(ListAPIView, RetrieveUpdateDestroyAPIView):
"""
View to list tags of a taxonomy.
View to list/create/update/delete tags of a taxonomy.
**List Query Parameters**
* pk (required) - The pk of the taxonomy to retrieve tags.
Expand All @@ -399,6 +401,31 @@ class TaxonomyTagsView(ListAPIView):
* 400 - Invalid query parameter
* 403 - Permission denied
* 404 - Taxonomy not found
**Create Query Parameters**
* pk (required) - The pk of the taxonomy to create a Tag for
**Create Request Body**
* tag (required): The value of the Tag that should be added to
the Taxonomy
* parent_tag_id (optional): The id of the parent tag that the new
Tag should fall under
* extenal_id (optional): The external id for the new Tag
**Create Example Requests**
POST api/tagging/v1/taxonomy/:pk/tags - Create a Tag in taxonomy
{
"value": "New Tag",
"parent_tag_id": 123
"external_id": "abc123",
}
**Create Query Returns**
* 201 - Success
* 400 - Invalid parameters provided
* 403 - Permission denied
* 404 - Taxonomy not found
"""

permission_classes = [TagListPermissions]
Expand Down Expand Up @@ -556,3 +583,28 @@ def get_queryset(self) -> list[Tag]: # type: ignore[override]
self.pagination_class = self.get_pagination_class()

return result

def post(self, request, *args, **kwargs):
"""
Creates new Tag in Taxonomy and returns the newly created Tag.
"""
pk = self.kwargs.get("pk")
taxonomy = self.get_taxonomy(pk)

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

tag = body.data.get("tag")
parent_tag_id = body.data.get("parent_tag_id", None)
external_id = body.data.get("external_id", None)

try:
new_tag = add_tag_to_taxonomy(
taxonomy, tag, parent_tag_id, external_id
)
except ValueError as e:
raise ValidationError from e

return Response(
TagsSerializer(new_tag).data, status=status.HTTP_201_CREATED
)
142 changes: 141 additions & 1 deletion tests/openedx_tagging/core/tagging/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -878,7 +878,7 @@ def test_tag_object_count_limit(self):

class TestTaxonomyTagsView(TestTaxonomyViewMixin):
"""
Tests the list tags of taxonomy view
Tests the list/create/update/delete tags of taxonomy view
"""

fixtures = ["tests/openedx_tagging/core/fixtures/tagging.yaml"]
Expand Down Expand Up @@ -1190,3 +1190,143 @@ def test_next_children(self):
assert data.get("count") == self.children_tags_count[0]
assert data.get("num_pages") == 2
assert data.get("current_page") == 2

def test_create_tag_in_taxonomy(self):
self.client.force_authenticate(user=self.user)
new_tag_value = "New Tag"

create_data = {
"tag": new_tag_value
}

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

assert response.status_code == status.HTTP_201_CREATED

data = response.data

self.assertIsNotNone(data.get("id"))
self.assertEqual(data.get("value"), new_tag_value)
self.assertEqual(data.get("taxonomy_id"), self.small_taxonomy.pk)
self.assertIsNone(data.get("parent_id"))
self.assertIsNone(data.get("external_id"))
self.assertIsNone(data.get("sub_tags_link"))
self.assertEqual(data.get("children_count"), 0)

def test_create_tag_in_taxonomy_with_parent_id(self):
self.client.force_authenticate(user=self.user)
parent_tag = self.small_taxonomy.tag_set.filter(parent=None).first()
new_tag_value = "New Child Tag"
new_external_id = "extId"

create_data = {
"tag": new_tag_value,
"parent_tag_id": parent_tag.id,
"external_id": new_external_id
}

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

assert response.status_code == status.HTTP_201_CREATED

data = response.data

self.assertIsNotNone(data.get("id"))
self.assertEqual(data.get("value"), new_tag_value)
self.assertEqual(data.get("taxonomy_id"), self.small_taxonomy.pk)
self.assertEqual(data.get("parent_id"), parent_tag.id)
self.assertEqual(data.get("external_id"), new_external_id)
self.assertIsNone(data.get("sub_tags_link"))
self.assertEqual(data.get("children_count"), 0)

def test_create_tag_in_invalid_taxonomy(self):
self.client.force_authenticate(user=self.user)
new_tag_value = "New Tag"

create_data = {
"tag": new_tag_value
}

invalid_taxonomy_url = TAXONOMY_TAGS_URL.format(pk=919191)
response = self.client.post(
invalid_taxonomy_url, create_data, format="json"
)

assert response.status_code == status.HTTP_404_NOT_FOUND

def test_create_tag_in_free_text_taxonomy(self):
self.client.force_authenticate(user=self.user)
new_tag_value = "New Tag"

create_data = {
"tag": new_tag_value
}

# Setting free text flag on taxonomy
self.small_taxonomy.allow_free_text = True
self.small_taxonomy.save()

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

assert response.status_code == status.HTTP_400_BAD_REQUEST

def test_create_tag_in_system_defined_taxonomy(self):
self.client.force_authenticate(user=self.user)
new_tag_value = "New Tag"

create_data = {
"tag": new_tag_value
}

# Setting taxonomy to be system defined
self.small_taxonomy.taxonomy_class = SystemDefinedTaxonomy
self.small_taxonomy.save()

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

assert response.status_code == status.HTTP_400_BAD_REQUEST

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

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

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

assert response.status_code == status.HTTP_400_BAD_REQUEST

def test_create_tag_in_taxonomy_with_already_existing_value(self):
self.client.force_authenticate(user=self.user)
new_tag_value = "New Tag"

create_data = {
"tag": new_tag_value
}

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

assert response.status_code == status.HTTP_201_CREATED

# Make request again with the same Tag value after it was created
response = self.client.post(
self.small_taxonomy_url, create_data, format="json"
)

assert response.status_code == status.HTTP_400_BAD_REQUEST

0 comments on commit 199df54

Please sign in to comment.