From 9e14fa4ac335277f67717db5b3a37158016d55c1 Mon Sep 17 00:00:00 2001 From: Yusuf Musleh Date: Mon, 18 Dec 2023 22:28:04 +0300 Subject: [PATCH] feat: Add ability to get unassigned taxonomies (#33945) This adds a query param to fetch unassigned taxonomies, i.e. taxonomies that do not belong to any org. --- .../core/djangoapps/content_tagging/api.py | 18 ++++++++++ .../rest_api/v1/serializers.py | 1 + .../rest_api/v1/tests/test_views.py | 34 +++++++++++++++++-- .../content_tagging/rest_api/v1/views.py | 12 +++++-- .../content_tagging/tests/test_api.py | 7 ++++ 5 files changed, 68 insertions(+), 4 deletions(-) diff --git a/openedx/core/djangoapps/content_tagging/api.py b/openedx/core/djangoapps/content_tagging/api.py index 1f33cc1c84..16f42b2fbf 100644 --- a/openedx/core/djangoapps/content_tagging/api.py +++ b/openedx/core/djangoapps/content_tagging/api.py @@ -109,6 +109,24 @@ def get_taxonomies_for_org( ) +def get_unassigned_taxonomies(enabled=True) -> QuerySet: + """ + Generate a list of the enabled orphaned Taxomonies, i.e. that do not belong to any + organization. We don't use `TaxonomyOrg.get_relationships` as that returns + Taxonomies which are available for all Organizations when no `org` is provided + """ + return oel_tagging.get_taxonomies(enabled=enabled).filter( + ~( + Exists( + TaxonomyOrg.objects.filter( + taxonomy=OuterRef("pk"), + rel_type=TaxonomyOrg.RelType.OWNER, + ) + ) + ) + ) + + def get_content_tags( object_key: ContentKey, taxonomy_id: int | None = None, diff --git a/openedx/core/djangoapps/content_tagging/rest_api/v1/serializers.py b/openedx/core/djangoapps/content_tagging/rest_api/v1/serializers.py index 058022ef75..12433f8a38 100644 --- a/openedx/core/djangoapps/content_tagging/rest_api/v1/serializers.py +++ b/openedx/core/djangoapps/content_tagging/rest_api/v1/serializers.py @@ -46,6 +46,7 @@ class TaxonomyOrgListQueryParamsSerializer(TaxonomyListQueryParamsSerializer): queryset=Organization.objects.all(), required=False, ) + unassigned: fields.Field = serializers.BooleanField(required=False) class TaxonomyUpdateOrgBodySerializer(serializers.Serializer): diff --git a/openedx/core/djangoapps/content_tagging/rest_api/v1/tests/test_views.py b/openedx/core/djangoapps/content_tagging/rest_api/v1/tests/test_views.py index c9026dbdbd..3ff0623eb8 100644 --- a/openedx/core/djangoapps/content_tagging/rest_api/v1/tests/test_views.py +++ b/openedx/core/djangoapps/content_tagging/rest_api/v1/tests/test_views.py @@ -277,7 +277,8 @@ class TestTaxonomyListCreateViewSet(TestTaxonomyObjectsMixin, APITestCase): user_attr: str, expected_taxonomies: list[str], enabled_parameter: bool | None = None, - org_parameter: str | None = None + org_parameter: str | None = None, + unassigned_parameter: bool | None = None ) -> None: """ Helper function to call the list endpoint and check the response @@ -288,7 +289,11 @@ class TestTaxonomyListCreateViewSet(TestTaxonomyObjectsMixin, APITestCase): self.client.force_authenticate(user=user) # Set parameters cleaning empty values - query_params = {k: v for k, v in {"enabled": enabled_parameter, "org": org_parameter}.items() if v is not None} + query_params = {k: v for k, v in { + "enabled": enabled_parameter, + "org": org_parameter, + "unassigned": unassigned_parameter, + }.items() if v is not None} response = self.client.get(url, query_params, format="json") @@ -358,6 +363,31 @@ class TestTaxonomyListCreateViewSet(TestTaxonomyObjectsMixin, APITestCase): expected_taxonomies=expected_taxonomies, ) + def test_list_unassigned_taxonomies(self): + """ + Test that passing in "unassigned" query param returns Taxonomies that + are unassigned. i.e. does not belong to any org + """ + self._test_list_taxonomy( + user_attr="staff", + expected_taxonomies=["ot1", "ot2"], + unassigned_parameter=True, + ) + + def test_list_unassigned_and_org_filter_invalid(self) -> None: + """ + Test that passing "org" and "unassigned" query params should throw an error + """ + url = TAXONOMY_ORG_LIST_URL + + self.client.force_authenticate(user=self.user) + + query_params = {"org": "orgA", "unassigned": "true"} + + response = self.client.get(url, query_params, format="json") + + assert response.status_code == status.HTTP_400_BAD_REQUEST + @ddt.data( ("user", (), None), ("staffA", ["tA2", "tBA1", "tBA2"], None), diff --git a/openedx/core/djangoapps/content_tagging/rest_api/v1/views.py b/openedx/core/djangoapps/content_tagging/rest_api/v1/views.py index a38aab8e01..f04256f4a1 100644 --- a/openedx/core/djangoapps/content_tagging/rest_api/v1/views.py +++ b/openedx/core/djangoapps/content_tagging/rest_api/v1/views.py @@ -5,7 +5,7 @@ from openedx_tagging.core.tagging import rules as oel_tagging_rules from openedx_tagging.core.tagging.rest_api.v1.views import ObjectTagView, TaxonomyView from rest_framework import status from rest_framework.decorators import action -from rest_framework.exceptions import PermissionDenied +from rest_framework.exceptions import PermissionDenied, ValidationError from rest_framework.request import Request from rest_framework.response import Response @@ -14,6 +14,7 @@ from ...api import ( get_taxonomy, get_taxonomies, get_taxonomies_for_org, + get_unassigned_taxonomies, set_taxonomy_orgs, ) from ...rules import get_admin_orgs @@ -55,11 +56,18 @@ class TaxonomyOrgView(TaxonomyView): query_params = TaxonomyOrgListQueryParamsSerializer(data=self.request.query_params.dict()) query_params.is_valid(raise_exception=True) enabled = query_params.validated_data.get("enabled", None) + unassigned = query_params.validated_data.get("unassigned", None) + org = query_params.validated_data.get("org", None) + + # Raise an error if both "org" and "unassigned" query params were provided + if "org" in query_params.validated_data and "unassigned" in query_params.validated_data: + raise ValidationError("'org' and 'unassigned' params cannot be both defined") # If org filtering was requested, then use it, even if the org is invalid/None - org = query_params.validated_data.get("org", None) if "org" in query_params.validated_data: queryset = get_taxonomies_for_org(enabled, org) + elif "unassigned" in query_params.validated_data: + queryset = get_unassigned_taxonomies(enabled) else: queryset = get_taxonomies(enabled) diff --git a/openedx/core/djangoapps/content_tagging/tests/test_api.py b/openedx/core/djangoapps/content_tagging/tests/test_api.py index 8dc6117376..9a297be968 100644 --- a/openedx/core/djangoapps/content_tagging/tests/test_api.py +++ b/openedx/core/djangoapps/content_tagging/tests/test_api.py @@ -156,6 +156,13 @@ class TestAPITaxonomy(TestTaxonomyMixin, TestCase): getattr(self, taxonomy_attr) for taxonomy_attr in expected ] + def test_get_unassigned_taxonomies(self): + expected = ["taxonomy_no_orgs"] + taxonomies = list(api.get_unassigned_taxonomies()) + assert taxonomies == [ + getattr(self, taxonomy_attr) for taxonomy_attr in expected + ] + @ddt.data( ("taxonomy_all_orgs", "all_orgs_course_tag"), ("taxonomy_all_orgs", "all_orgs_block_tag"),