Skip to content

Commit

Permalink
feat: Implement ObjectTag retrieve REST API (#68)
Browse files Browse the repository at this point in the history
* chore: Remove is_valid checks from get_object_tags

* fix: Rename ObjectTag perms to match model name

* feat: Implement ObjectTag retrieve REST API
  Retrieve ObjectTags for given Object IDs, and optionally filter by taxonomy.

* chore: bumped version
  • Loading branch information
yusuf-musleh authored Aug 18, 2023
1 parent 79c16a0 commit 593d63c
Show file tree
Hide file tree
Showing 10 changed files with 431 additions and 64 deletions.
2 changes: 1 addition & 1 deletion openedx_learning/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.1.4"
__version__ = "0.1.5"
14 changes: 5 additions & 9 deletions openedx_tagging/core/tagging/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,16 +96,14 @@ def resync_object_tags(object_tags: QuerySet = None) -> int:


def get_object_tags(
object_id: str, taxonomy: Taxonomy = None, valid_only=True
) -> Iterator[ObjectTag]:
object_id: str, taxonomy_id: str = None
) -> QuerySet:
"""
Generates a list of object tags for a given object.
Returns a Queryset of object tags for a given object.
Pass taxonomy to limit the returned object_tags to a specific taxonomy.
Pass valid_only=False when displaying tags to content authors, so they can see invalid tags too.
Invalid tags will (probably) be hidden from learners.
"""
taxonomy = get_taxonomy(taxonomy_id)
ObjectTagClass = taxonomy.object_tag_class if taxonomy else ObjectTag
tags = (
ObjectTagClass.objects.filter(
Expand All @@ -117,9 +115,7 @@ def get_object_tags(
if taxonomy:
tags = tags.filter(taxonomy=taxonomy)

for object_tag in tags:
if not valid_only or object_tag.is_valid():
yield object_tag
return tags


def delete_object_tags(object_id: str):
Expand Down
14 changes: 13 additions & 1 deletion openedx_tagging/core/tagging/rest_api/v1/permissions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
Taxonomy permissions
Tagging permissions
"""

from rest_framework.permissions import DjangoObjectPermissions
Expand All @@ -15,3 +15,15 @@ class TaxonomyObjectPermissions(DjangoObjectPermissions):
"PATCH": ["%(app_label)s.change_%(model_name)s"],
"DELETE": ["%(app_label)s.delete_%(model_name)s"],
}


class ObjectTagObjectPermissions(DjangoObjectPermissions):
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"],
}
28 changes: 27 additions & 1 deletion openedx_tagging/core/tagging/rest_api/v1/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from rest_framework import serializers

from openedx_tagging.core.tagging.models import Taxonomy
from openedx_tagging.core.tagging.models import Taxonomy, ObjectTag


class TaxonomyListQueryParamsSerializer(serializers.Serializer):
Expand All @@ -29,3 +29,29 @@ class Meta:
"system_defined",
"visible_to_authors",
]


class ObjectTagListQueryParamsSerializer(serializers.Serializer):
"""
Serializer for the query params for the ObjectTag GET view
"""

taxonomy = serializers.PrimaryKeyRelatedField(
queryset=Taxonomy.objects.all(), required=False
)


class ObjectTagSerializer(serializers.ModelSerializer):
"""
Serializer for the ObjectTag model.
"""

class Meta:
model = ObjectTag
fields = [
"name",
"value",
"taxonomy_id",
"tag_ref",
"is_valid",
]
1 change: 1 addition & 0 deletions openedx_tagging/core/tagging/rest_api/v1/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@

router = DefaultRouter()
router.register("taxonomies", views.TaxonomyView, basename="taxonomy")
router.register("object_tags", views.ObjectTagView, basename="object_tag")

urlpatterns = [path("", include(router.urls))]
87 changes: 84 additions & 3 deletions openedx_tagging/core/tagging/rest_api/v1/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,23 @@
Tagging API Views
"""
from django.http import Http404
from rest_framework.viewsets import ModelViewSet
from rest_framework import status
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from rest_framework.response import Response

from ...api import (
create_taxonomy,
get_taxonomy,
get_taxonomies,
get_object_tags,
)
from .permissions import TaxonomyObjectPermissions, ObjectTagObjectPermissions
from .serializers import (
TaxonomyListQueryParamsSerializer,
TaxonomySerializer,
ObjectTagListQueryParamsSerializer,
ObjectTagSerializer,
)
from .permissions import TaxonomyObjectPermissions
from .serializers import TaxonomyListQueryParamsSerializer, TaxonomySerializer


class TaxonomyView(ModelViewSet):
Expand Down Expand Up @@ -145,3 +153,76 @@ def perform_create(self, serializer):
Create a new taxonomy.
"""
serializer.instance = create_taxonomy(**serializer.validated_data)


class ObjectTagView(ReadOnlyModelViewSet):
"""
View to retrieve paginated ObjectTags for an Object, given its Object ID.
(What tags does this object have?)
**Retrieve Parameters**
* object_id (required): - The Object ID to retrieve ObjectTags for.
**Retrieve Query Parameters**
* taxonomy (optional) - PK of taxonomy to filter ObjectTags for.
* page (optional) - Page number of paginated results.
* page_size (optional) - Number of results included in each page.
**Retrieve Example Requests**
GET api/tagging/v1/object_tags/:object_id
GET api/tagging/v1/object_tags/:object_id?taxonomy=1
GET api/tagging/v1/object_tags/:object_id?taxonomy=1&page=2
GET api/tagging/v1/object_tags/:object_id?taxonomy=1&page=2&page_size=10
**Retrieve Query Returns**
* 200 - Success
* 400 - Invalid query parameter
* 403 - Permission denied
**Create Query Returns**
* 403 - Permission denied
* 405 - Method not allowed
**Update Query Returns**
* 403 - Permission denied
* 405 - Method not allowed
**Delete Query Returns**
* 403 - Permission denied
* 405 - Method not allowed
"""

serializer_class = ObjectTagSerializer
permission_classes = [ObjectTagObjectPermissions]
lookup_field = "object_id"

def get_queryset(self):
"""
Return a queryset of object tags for a given object.
If a taxonomy is passed in, object tags are limited to that taxonomy.
"""
object_id = self.kwargs.get("object_id")
query_params = ObjectTagListQueryParamsSerializer(
data=self.request.query_params.dict()
)
query_params.is_valid(raise_exception=True)
taxonomy_id = query_params.data.get("taxonomy", None)
return get_object_tags(object_id, taxonomy_id)

def retrieve(self, request, object_id=None):
"""
Retrieve ObjectTags that belong to a given Object given its
object_id and return paginated results.
Note: We override `retrieve` here instead of `list` because we are
passing in the Object ID (object_id) in the path (as opposed to passing
it in as a query_param) to retrieve the related ObjectTags.
By default retrieve would expect an ObjectTag ID to be passed in the
path and returns a it as a single result however that is not
behavior we want.
"""
object_tags = self.get_queryset()
paginated_object_tags = self.paginate_queryset(object_tags)
serializer = ObjectTagSerializer(paginated_object_tags, many=True)
return self.get_paginated_response(serializer.data)
8 changes: 4 additions & 4 deletions openedx_tagging/core/tagging/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def can_change_object_tag(user: User, object_tag: ObjectTag = None) -> bool:
rules.add_perm("oel_tagging.view_tag", rules.always_allow)

# ObjectTag
rules.add_perm("oel_tagging.add_object_tag", can_change_object_tag)
rules.add_perm("oel_tagging.change_object_tag", can_change_object_tag)
rules.add_perm("oel_tagging.delete_object_tag", is_taxonomy_admin)
rules.add_perm("oel_tagging.view_object_tag", rules.always_allow)
rules.add_perm("oel_tagging.add_objecttag", can_change_object_tag)
rules.add_perm("oel_tagging.change_objecttag", can_change_object_tag)
rules.add_perm("oel_tagging.delete_objecttag", is_taxonomy_admin)
rules.add_perm("oel_tagging.view_objecttag", rules.always_allow)
28 changes: 4 additions & 24 deletions tests/openedx_tagging/core/tagging/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ def test_tag_object(self):
assert (
list(
tagging_api.get_object_tags(
taxonomy=self.taxonomy,
taxonomy_id=self.taxonomy.pk,
object_id="biology101",
)
)
Expand Down Expand Up @@ -355,7 +355,7 @@ def test_tag_object_language_taxonomy(self):
assert (
list(
tagging_api.get_object_tags(
taxonomy=self.language_taxonomy,
taxonomy_id=self.language_taxonomy.pk,
object_id="biology101",
)
)
Expand Down Expand Up @@ -401,7 +401,7 @@ def test_tag_object_model_system_taxonomy(self):
assert (
list(
tagging_api.get_object_tags(
taxonomy=self.user_taxonomy,
taxonomy_id=self.user_taxonomy.pk,
object_id="biology101",
)
)
Expand Down Expand Up @@ -445,37 +445,17 @@ def test_get_object_tags(self):
assert list(
tagging_api.get_object_tags(
object_id="abc",
valid_only=False,
)
) == [
alpha,
beta,
]

# No valid tags for this object yet..
assert not list(
tagging_api.get_object_tags(
object_id="abc",
valid_only=True,
)
)
beta.tag = self.mammalia
beta.save()
assert list(
tagging_api.get_object_tags(
object_id="abc",
valid_only=True,
)
) == [
beta,
]

# Fetch all the tags for a given object ID + taxonomy
assert list(
tagging_api.get_object_tags(
object_id="abc",
taxonomy=self.taxonomy,
valid_only=False,
taxonomy_id=self.taxonomy.pk,
)
) == [
beta,
Expand Down
40 changes: 20 additions & 20 deletions tests/openedx_tagging/core/tagging/test_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,8 @@ def test_view_tag(self):
# ObjectTag

@ddt.data(
"oel_tagging.add_object_tag",
"oel_tagging.change_object_tag",
"oel_tagging.add_objecttag",
"oel_tagging.change_objecttag",
)
def test_add_change_object_tag(self, perm):
"""Taxonomy administrators can create/edit an ObjectTag with an enabled Taxonomy"""
Expand All @@ -174,8 +174,8 @@ def test_add_change_object_tag(self, perm):
assert not self.learner.has_perm(perm, self.object_tag)

@ddt.data(
"oel_tagging.add_object_tag",
"oel_tagging.change_object_tag",
"oel_tagging.add_objecttag",
"oel_tagging.change_objecttag",
)
def test_object_tag_disabled_taxonomy(self, perm):
"""Taxonomy administrators cannot create/edit an ObjectTag with a disabled Taxonomy"""
Expand All @@ -189,23 +189,23 @@ def test_object_tag_disabled_taxonomy(self, perm):
True,
False,
)
def test_delete_object_tag(self, enabled):
def test_delete_objecttag(self, enabled):
"""Taxonomy administrators can delete any ObjectTag, even those associated with a disabled Taxonomy."""
self.taxonomy.enabled = enabled
self.taxonomy.save()
assert self.superuser.has_perm("oel_tagging.delete_object_tag")
assert self.superuser.has_perm("oel_tagging.delete_object_tag", self.object_tag)
assert self.staff.has_perm("oel_tagging.delete_object_tag")
assert self.staff.has_perm("oel_tagging.delete_object_tag", self.object_tag)
assert not self.learner.has_perm("oel_tagging.delete_object_tag")
assert self.superuser.has_perm("oel_tagging.delete_objecttag")
assert self.superuser.has_perm("oel_tagging.delete_objecttag", self.object_tag)
assert self.staff.has_perm("oel_tagging.delete_objecttag")
assert self.staff.has_perm("oel_tagging.delete_objecttag", self.object_tag)
assert not self.learner.has_perm("oel_tagging.delete_objecttag")
assert not self.learner.has_perm(
"oel_tagging.delete_object_tag", self.object_tag
"oel_tagging.delete_objecttag", self.object_tag
)

@ddt.data(
"oel_tagging.add_object_tag",
"oel_tagging.change_object_tag",
"oel_tagging.delete_object_tag",
"oel_tagging.add_objecttag",
"oel_tagging.change_objecttag",
"oel_tagging.delete_objecttag",
)
def test_object_tag_no_taxonomy(self, perm):
"""Taxonomy administrators can modify an ObjectTag with no Taxonomy"""
Expand All @@ -216,9 +216,9 @@ def test_object_tag_no_taxonomy(self, perm):

def test_view_object_tag(self):
"""Anyone can view any ObjectTag"""
assert self.superuser.has_perm("oel_tagging.view_object_tag")
assert self.superuser.has_perm("oel_tagging.view_object_tag", self.object_tag)
assert self.staff.has_perm("oel_tagging.view_object_tag")
assert self.staff.has_perm("oel_tagging.view_object_tag", self.object_tag)
assert self.learner.has_perm("oel_tagging.view_object_tag")
assert self.learner.has_perm("oel_tagging.view_object_tag", self.object_tag)
assert self.superuser.has_perm("oel_tagging.view_objecttag")
assert self.superuser.has_perm("oel_tagging.view_objecttag", self.object_tag)
assert self.staff.has_perm("oel_tagging.view_objecttag")
assert self.staff.has_perm("oel_tagging.view_objecttag", self.object_tag)
assert self.learner.has_perm("oel_tagging.view_objecttag")
assert self.learner.has_perm("oel_tagging.view_objecttag", self.object_tag)
Loading

0 comments on commit 593d63c

Please sign in to comment.