Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement ObjectTag retrieve REST API #68

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
pomegranited marked this conversation as resolved.
Show resolved Hide resolved
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