Skip to content

Commit

Permalink
chore: update with latest master
Browse files Browse the repository at this point in the history
  • Loading branch information
bradenmacdonald committed Oct 25, 2023
2 parents 68f6f19 + be5d527 commit 72e782b
Show file tree
Hide file tree
Showing 16 changed files with 1,010 additions and 104 deletions.
2 changes: 1 addition & 1 deletion openedx_learning/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""
Open edX Learning ("Learning Core").
"""
__version__ = "0.2.5"
__version__ = "0.2.6"
2 changes: 1 addition & 1 deletion openedx_learning/core/components/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ class ComponentVersion(PublishableEntityVersionMixin):

# The raw_contents hold the actual interesting data associated with this
# ComponentVersion.
raw_contents = models.ManyToManyField(
raw_contents: models.ManyToManyField[RawContent, ComponentVersionRawContent] = models.ManyToManyField(
RawContent,
through="ComponentVersionRawContent",
related_name="component_versions",
Expand Down
55 changes: 55 additions & 0 deletions openedx_tagging/core/tagging/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ def resync_object_tags(object_tags: QuerySet | None = None) -> int:
if changed:
object_tag.save()
num_changed += 1

return num_changed


Expand Down Expand Up @@ -263,3 +264,57 @@ def _check_new_tag_count(new_tag_count: int) -> None:
for object_tag in updated_tags:
object_tag.full_clean() # Run validation
object_tag.save()


def add_tag_to_taxonomy(
taxonomy: Taxonomy,
tag: str,
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
"""
taxonomy = taxonomy.cast()
new_tag = taxonomy.add_tag(tag, parent_tag_value, external_id)

# Resync all related ObjectTags after creating new Tag to
# to ensure any existing ObjectTags with the same value will
# be linked to the new Tag
object_tags = taxonomy.objecttag_set.all()
resync_object_tags(object_tags)

return new_tag


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 supports updating the Tag value.
"""
taxonomy = taxonomy.cast()
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()
resync_object_tags(object_tags)

return updated_tag


def delete_tags_from_taxonomy(
taxonomy: Taxonomy,
tags: list[str],
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.
"""
taxonomy = taxonomy.cast()
taxonomy.delete_tags(tags, with_subtags)
99 changes: 99 additions & 0 deletions openedx_tagging/core/tagging/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,105 @@ def _get_filtered_tags_deep(
qs = qs.annotate(usage_count=models.Subquery(obj_tags.values('count')))
return qs

def add_tag(
self,
tag_value: str,
parent_tag_value: str | 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."
)

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_value:
# Get parent tag from taxonomy, raises Tag.DoesNotExist if doesn't
# belong to taxonomy
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: str, new_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, raises Tag.DoesNotExist if
# tag doesn't belong to taxonomy
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, 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
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_to_delete = self.tag_set.filter(value__in=tags)

if tags_to_delete.count() != len(tags):
# If they do not match that means there is one or more Tag ID(s)
# provided that do 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_to_delete.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_to_delete.delete()

def validate_value(self, value: str) -> bool:
"""
Check if 'value' is part of this Taxonomy.
Expand Down
26 changes: 15 additions & 11 deletions openedx_tagging/core/tagging/rest_api/v1/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import rules # type: ignore[import]
from rest_framework.permissions import DjangoObjectPermissions

from ...models import Tag


class TaxonomyObjectPermissions(DjangoObjectPermissions):
"""
Expand Down Expand Up @@ -35,22 +37,24 @@ class ObjectTagObjectPermissions(DjangoObjectPermissions):
}


class TagListPermissions(DjangoObjectPermissions):
class TagObjectPermissions(DjangoObjectPermissions):
"""
Permissions for Tag object views.
Maps each REST API methods to its corresponding Tag permission.
"""
def has_permission(self, request, view):
"""
Returns True if the user on the given request is allowed the given view.
"""
if not request.user or (
not request.user.is_authenticated and self.authenticated_users_only
):
return False
return True
perms_map = {
"GET": ["%(app_label)s.view_%(model_name)s"],
"OPTIONS": [],
"HEAD": ["%(app_label)s.view_%(model_name)s"],
"POST": ["%(app_label)s.add_%(model_name)s"],
"PUT": ["%(app_label)s.change_%(model_name)s"],
"PATCH": ["%(app_label)s.change_%(model_name)s"],
"DELETE": ["%(app_label)s.delete_%(model_name)s"],
}

# This is to handle the special case for GET list of Taxonomy Tags
def has_object_permission(self, request, view, obj):
"""
Returns True if the user on the given request is allowed the given view for the given object.
"""
obj = obj.taxonomy if isinstance(obj, Tag) else obj
return rules.has_perm("oel_tagging.list_tag", request.user, obj)
31 changes: 30 additions & 1 deletion openedx_tagging/core/tagging/rest_api/v1/serializers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""
API Serializers for taxonomies
"""

from rest_framework import serializers
from rest_framework.reverse import reverse

Expand Down Expand Up @@ -132,3 +131,33 @@ def update(self, instance, validated_data):

def create(self, validated_data):
raise RuntimeError('`create()` is not supported by the TagData serializer.')


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

tag = serializers.CharField(required=True)
parent_tag_value = serializers.CharField(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 request
"""

tag = serializers.CharField(required=True)
updated_tag_value = serializers.CharField(required=True)


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

tags = serializers.ListField(
child=serializers.CharField(), required=True
)
with_subtags = serializers.BooleanField(required=False)
Loading

0 comments on commit 72e782b

Please sign in to comment.