Skip to content

Commit

Permalink
refactor: Use Taxonomy Tag values rather than IDs
Browse files Browse the repository at this point in the history
  • Loading branch information
yusuf-musleh committed Oct 17, 2023
1 parent bdaffeb commit 3abce26
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 82 deletions.
14 changes: 7 additions & 7 deletions openedx_tagging/core/tagging/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,26 +321,26 @@ def autocomplete_tags(
def add_tag_to_taxonomy(
taxonomy: Taxonomy,
tag: str,
parent_tag_id: int | None = None,
parent_tag_value: str | 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)
return taxonomy.cast().add_tag(tag, parent_tag_value, external_id)


def update_tag_in_taxonomy(taxonomy: Taxonomy, tag: int, tag_value: str):
def update_tag_in_taxonomy(taxonomy: Taxonomy, tag: str, new_value: str):
"""
Update a Tag that belongs to a Taxonomy. The related ObjectTags are
updated accordingly.
Currently only support updates the Tag value.
Currently only supports updating the Tag value.
"""
taxonomy = taxonomy.cast()
updated_tag = taxonomy.update_tag(tag, tag_value)
updated_tag = taxonomy.update_tag(tag, new_value)

# Resync all related ObjectTags to update to the new Tag value
object_tags = taxonomy.objecttag_set.all()
Expand All @@ -351,7 +351,7 @@ def update_tag_in_taxonomy(taxonomy: Taxonomy, tag: int, tag_value: str):

def delete_tags_from_taxonomy(
taxonomy: Taxonomy,
tag_ids: list[int],
tags: list[str],
with_subtags: bool
):
"""
Expand All @@ -360,7 +360,7 @@ def delete_tags_from_taxonomy(
the sub-tags will be deleted as well.
"""
taxonomy = taxonomy.cast()
taxonomy.delete_tags(tag_ids, with_subtags)
taxonomy.delete_tags(tags, with_subtags)

# Resync all related ObjectTags after deleting the Tag(s)
object_tags = taxonomy.objecttag_set.all()
Expand Down
26 changes: 13 additions & 13 deletions openedx_tagging/core/tagging/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ def get_filtered_tags(
def add_tag(
self,
tag_value: str,
parent_tag_id: int | None = None,
parent_tag_value: str | None = None,
external_id: str | None = None
) -> Tag:
"""
Expand All @@ -369,16 +369,16 @@ def add_tag(
raise ValueError(f"Tag with value '{tag_value}' already exists for taxonomy.")

parent = None
if parent_tag_id:
parent = self.tag_set.get(id=parent_tag_id)
if parent_tag_value:
parent = self.tag_set.get(value__iexact=parent_tag_value)

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:
def update_tag(self, tag: str, new_value: str) -> Tag:
"""
Update an existing Tag in Taxonomy and return it. Currently only
supports updating the Tag's value.
Expand All @@ -396,12 +396,12 @@ def update_tag(self, tag_id: int, tag_value: str) -> Tag:
)

# Update Tag instance with new value
tag = self.tag_set.get(id=tag_id)
tag.value = tag_value
tag.save()
return tag
tag_to_update = self.tag_set.get(value__iexact=tag)
tag_to_update.value = new_value
tag_to_update.save()
return tag_to_update

def delete_tags(self, tag_ids: List[int], with_subtags: bool = False):
def delete_tags(self, tags: List[str], 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
Expand All @@ -419,15 +419,15 @@ def delete_tags(self, tag_ids: List[int], with_subtags: bool = False):
"delete_tags() doesn't work for system defined taxonomies. They cannot be modified."
)

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

if tags.count() != len(tag_ids):
if tags_to_delete.count() != len(tags):
# 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()
contains_children = tags_to_delete.filter(children__isnull=False).distinct().exists()

if contains_children and not with_subtags:
raise ValueError(
Expand All @@ -436,7 +436,7 @@ def delete_tags(self, tag_ids: List[int], with_subtags: bool = False):
)

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

def validate_value(self, value: str) -> bool:
"""
Expand Down
53 changes: 43 additions & 10 deletions openedx_tagging/core/tagging/rest_api/v1/serializers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"""
API Serializers for taxonomies
"""
from functools import reduce

from django.db.models import Q
from rest_framework import serializers
from rest_framework.reverse import reverse

Expand Down Expand Up @@ -201,31 +203,62 @@ class TaxonomyTagCreateBodySerializer(serializers.Serializer): # pylint: disabl
"""

tag = serializers.CharField(required=True)
parent_tag_id = serializers.PrimaryKeyRelatedField(
queryset=Tag.objects.all(), required=False
parent_tag_value = serializers.CharField(
source='parent.value', required=False
)
external_id = serializers.CharField(required=False)

def validate_parent_tag_value(self, value):
"""
Check that the provided parent Tag exists based on the value
"""
valid = Tag.objects.filter(value__iexact=value).exists()
if not valid:
raise serializers.ValidationError("Invalid `parent_tag_value` provided")

return value


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)
tag = serializers.CharField(source="value", required=True)
updated_tag_value = serializers.CharField(required=True)

def validate_tag(self, value):
"""
Check that the provided Tag exists based on the value
"""

valid = Tag.objects.filter(value__iexact=value).exists()
if not valid:
raise serializers.ValidationError("Invalid `tag` provided")

return value


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
tags = serializers.ListField(
child=serializers.CharField(), required=True
)
with_subtags = serializers.BooleanField(required=False)

def validate_tags(self, value):
"""
Check that the provided Tags exists based on the values
"""

# Build query to check if case-insensitive value for Tags list provided
q_list = map(lambda n: Q(value__iexact=n), value)
q_list = reduce(lambda a, b: a | b, q_list)
valid = Tag.objects.filter(q_list).count() == len(value)
if not valid:
raise serializers.ValidationError("One more tag in `tags` is invalid")

return value
33 changes: 17 additions & 16 deletions openedx_tagging/core/tagging/rest_api/v1/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -437,15 +437,15 @@ class TaxonomyTagsView(ListAPIView, RetrieveUpdateDestroyAPIView):
**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
* parent_tag_value (optional): The value 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
"parent_tag_value": "Parent Tag"
"external_id": "abc123",
}
Expand All @@ -459,19 +459,19 @@ class TaxonomyTagsView(ListAPIView, RetrieveUpdateDestroyAPIView):
* 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
* tag (required): The value (identifier) of the Tag to be updated
* 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"
"tag": "Tag 1",
"updated_tag_value": "Updated Tag Value"
}
PATCH api/tagging/v1/taxonomy/:pk/tags - Update a Tag in Taxonomy
{
"tag": 1,
"tag_value": "Updated Tag Value"
"tag": "Tag 1",
"updated_tag_value": "Updated Tag Value"
}
**Update Query Returns**
Expand All @@ -484,15 +484,16 @@ class TaxonomyTagsView(ListAPIView, RetrieveUpdateDestroyAPIView):
* 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
* tags (required): The values (identifiers) 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],
"tags": ["Tag 1", "Tag 2", "Tag 3"],
"with_subtags": True
}
Expand Down Expand Up @@ -687,12 +688,12 @@ def post(self, request, *args, **kwargs):
body.is_valid(raise_exception=True)

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

try:
new_tag = add_tag_to_taxonomy(
taxonomy, tag, parent_tag_id, external_id
taxonomy, tag, parent_tag_value, external_id
)
except TagDoesNotExist as e:
raise Http404("Parent Tag not found") from e
Expand All @@ -718,10 +719,10 @@ def update(self, request, *args, **kwargs):
body.is_valid(raise_exception=True)

tag = body.data.get("tag")
tag_value = body.data.get("tag_value")
updated_tag_value = body.data.get("updated_tag_value")

try:
updated_tag = update_tag_in_taxonomy(taxonomy, tag, tag_value)
updated_tag = update_tag_in_taxonomy(taxonomy, tag, updated_tag_value)
except TagDoesNotExist as e:
raise Http404("Tag not found") from e
except ValueError as e:
Expand All @@ -746,11 +747,11 @@ def delete(self, request, *args, **kwargs):
body = TaxonomyTagDeleteBodySerializer(data=request.data)
body.is_valid(raise_exception=True)

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

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

Expand Down
Loading

0 comments on commit 3abce26

Please sign in to comment.