Skip to content

Commit

Permalink
feat: Add an API to retrieve the count of tags applied to each object
Browse files Browse the repository at this point in the history
  • Loading branch information
bradenmacdonald committed Nov 1, 2023
1 parent 9966402 commit 58afaff
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 5 deletions.
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,6 +10,7 @@
router = DefaultRouter()
router.register("taxonomies", views.TaxonomyView, basename="taxonomy")
router.register("object_tags", views.ObjectTagView, basename="object_tag")
router.register("object_tag_counts", views.ObjectTagCountsView, basename="object_tag_counts")

urlpatterns = [
path("", include(router.urls)),
Expand Down
52 changes: 47 additions & 5 deletions openedx_tagging/core/tagging/rest_api/v1/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from ...data import TagDataQuerySet
from ...import_export.api import export_tags
from ...import_export.parsers import ParserFormat
from ...models import Taxonomy
from ...models import ObjectTag, Taxonomy
from ...rules import ObjectTagPermissionItem
from ..paginators import TAGS_THRESHOLD, DisabledTagsPagination, TagsPagination
from .permissions import ObjectTagObjectPermissions, TagObjectPermissions, TaxonomyObjectPermissions
Expand Down Expand Up @@ -266,10 +266,6 @@ class ObjectTagView(
* 400 - Invalid query parameter
* 403 - Permission denied
**Create Query Returns**
* 403 - Permission denied
* 405 - Method not allowed
**Update Parameters**
* object_id (required): - The Object ID to add ObjectTags for.
Expand Down Expand Up @@ -411,6 +407,52 @@ def update(self, request, *args, **kwargs) -> Response:
return self.retrieve(request, object_id)


@view_auth_classes
class ObjectTagCountsView(
mixins.RetrieveModelMixin,
GenericViewSet,
):
"""
View to retrieve the count of ObjectTags for all matching object IDs.
This API does NOT bother doing any permission checks as the "# of tags" is not considered sensitive information.
**Retrieve Parameters**
* object_id_pattern (required): - The Object ID to retrieve ObjectTags for. Can contain '*' at the end
for wildcard matching, or use ',' to separate multiple object IDs.
**Retrieve Example Requests**
GET api/tagging/v1/object_tag_counts/:object_id_pattern
**Retrieve Query Returns**
* 200 - Success
"""

serializer_class = ObjectTagSerializer
lookup_field = "object_id_pattern"

def retrieve(self, request, *args, **kwargs) -> Response:
"""
Retrieve the counts of object tags that belong to a given object_id pattern
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 ObjectTag counts.
"""
# This API does NOT bother doing any permission checks as the # of tags is not considered sensitive information.
object_id_pattern = self.kwargs["object_id_pattern"]
qs = ObjectTag.objects
if object_id_pattern.endswith("*"):
qs = qs.filter(object_id__startswith=object_id_pattern[0:len(object_id_pattern) - 1])
elif "*" in object_id_pattern:
raise ValidationError("Wildcard matches are only supported if the * is at the end.")
else:
qs = qs.filter(object_id__in=object_id_pattern.split(","))

qs = qs.values("object_id").annotate(num_tags=models.Count("id")).order_by("object_id")
return Response({row["object_id"]: row["num_tags"] for row in qs})


@view_auth_classes
class TaxonomyTagsView(ListAPIView, RetrieveUpdateDestroyAPIView):
"""
Expand Down
49 changes: 49 additions & 0 deletions tests/openedx_tagging/core/tagging/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@


OBJECT_TAGS_RETRIEVE_URL = "/tagging/rest_api/v1/object_tags/{object_id}/"
OBJECT_TAG_COUNTS_URL = "/tagging/rest_api/v1/object_tag_counts/{object_id_pattern}/"
OBJECT_TAGS_UPDATE_URL = "/tagging/rest_api/v1/object_tags/{object_id}/?taxonomy={taxonomy_id}"

LANGUAGE_TAXONOMY_ID = -1
Expand Down Expand Up @@ -916,6 +917,54 @@ def test_tag_object_count_limit(self):
assert response.status_code == status.HTTP_400_BAD_REQUEST


class TestObjectTagCountsViewSet(TestTagTaxonomyMixin, APITestCase):
"""
Testing various cases for counting how many tags are applied to several objects.
"""

def test_get_counts(self):
"""
Test retrieving the counts of tags applied to various content objects.
This API does NOT bother doing any permission checks as the "# of tags" is not considered sensitive information.
"""
# Course 2
api.tag_object(object_id="course02-unit01-problem01", taxonomy=self.free_text_taxonomy, tags=["other"])
# Course 7 Unit 1
api.tag_object(object_id="course07-unit01-problem01", taxonomy=self.free_text_taxonomy, tags=["a", "b", "c"])
api.tag_object(object_id="course07-unit01-problem02", taxonomy=self.free_text_taxonomy, tags=["a", "b"])
# Course 7 Unit 2
api.tag_object(object_id="course07-unit02-problem01", taxonomy=self.free_text_taxonomy, tags=["b"])
api.tag_object(object_id="course07-unit02-problem02", taxonomy=self.free_text_taxonomy, tags=["c", "d"])
api.tag_object(object_id="course07-unit02-problem03", taxonomy=self.free_text_taxonomy, tags=["N", "M", "x"])

def check(object_id_pattern: str):
result = self.client.get(OBJECT_TAG_COUNTS_URL.format(object_id_pattern=object_id_pattern))
assert result.status_code == status.HTTP_200_OK
return result.data

assert check(object_id_pattern="course02-*") == {
"course02-unit01-problem01": 1,
}
assert check(object_id_pattern="course07-unit01-*") == {
"course07-unit01-problem01": 3,
"course07-unit01-problem02": 2,
}
assert check(object_id_pattern="course07-unit*") == {
"course07-unit01-problem01": 3,
"course07-unit01-problem02": 2,
"course07-unit02-problem01": 1,
"course07-unit02-problem02": 2,
"course07-unit02-problem03": 3,
}
# Can also use a comma to separate explicit object IDs:
assert check(object_id_pattern="course07-unit01-problem01") == {"course07-unit01-problem01": 3}
assert check(object_id_pattern="course07-unit01-problem01,course07-unit02-problem02") == {
"course07-unit01-problem01": 3,
"course07-unit02-problem02": 2,
}


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

0 comments on commit 58afaff

Please sign in to comment.