Skip to content
Open
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
17 changes: 17 additions & 0 deletions openedx/core/djangoapps/content_tagging/rest_api/v1/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

from openedx_authz import api as authz_api
from openedx_authz.constants.permissions import COURSES_MANAGE_TAGS
from openedx_learning.api import is_competency_taxonomy
from openedx_tagging.api import TaxonomyType
from openedx_tagging.rest_api.v1.serializers import (
ObjectTagMinimalSerializer,
ObjectTagsByTaxonomySerializer,
Expand Down Expand Up @@ -98,6 +100,21 @@ class Meta:
fields = TaxonomySerializer.Meta.fields + ["orgs", "all_orgs"]
read_only_fields = ["orgs", "all_orgs"]

def to_representation(self, instance) -> dict:
"""
Serialize the taxonomy, adding the computed ``taxonomy_type`` (read side).

``taxonomy_type`` is also a write-only field inherited from ``TaxonomySerializer``,
used only as create-time input, so DRF excludes it from the base representation.
This adds it back for reads, computed from whether the taxonomy is backed by a
CompetencyTaxonomy, via the CBE app's own public API.
"""
data = super().to_representation(instance)
data["taxonomy_type"] = (
TaxonomyType.COMPETENCY.value if is_competency_taxonomy(instance) else TaxonomyType.TAGS.value
)
return data


class ObjectTagOrgByTaxonomySerializer(ObjectTagsByTaxonomySerializer):
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator, LibraryCollectionLocator, LibraryContainerLocator
from openedx_authz.constants import permissions as authz_permissions
from openedx_authz.constants.roles import COURSE_AUDITOR, COURSE_EDITOR, COURSE_STAFF
from openedx_learning.api import create_competency_taxonomy
from openedx_learning.models_api import CompetencyTaxonomy
from openedx_tagging.models import Tag, Taxonomy
from openedx_tagging.rest_api.v1.serializers import TaxonomySerializer
Expand Down Expand Up @@ -611,6 +612,24 @@ def test_list_taxonomy_query_count(self, user_attr: str, expected_queries: int):
assert taxonomy["can_delete_taxonomy"] == user.is_staff # not the metadata about the taxonomy.
assert taxonomy["can_tag_object"]

def test_list_taxonomy_type_mixed(self) -> None:
"""
The list endpoint reports "taxonomy_type" per row for a mixed set of taxonomies:
plain taxonomies as "tags", a competency taxonomy as "competency", and never null.
"""
competency_taxonomy = create_competency_taxonomy(name="Nursing Competencies", export_id="nursing-competencies")
set_taxonomy_orgs(taxonomy=competency_taxonomy, all_orgs=False, orgs=[self.orgA])

self.client.force_authenticate(user=self.staff)
response = self.client.get(TAXONOMY_ORG_LIST_URL, {"org": self.orgA.short_name, "page_size": 20})
assert response.status_code == status.HTTP_200_OK

taxonomy_types_by_name = {t["name"]: t["taxonomy_type"] for t in response.data["results"]}
assert taxonomy_types_by_name["t1"] == "tags"
assert taxonomy_types_by_name["ro1"] == "tags"
assert taxonomy_types_by_name["Nursing Competencies"] == "competency"
assert None not in taxonomy_types_by_name.values()


@ddt.ddt
class TestTaxonomyDetailExportMixin(TestTaxonomyObjectsMixin):
Expand Down Expand Up @@ -931,6 +950,39 @@ def _test_api_call(self, **kwargs) -> None:
**(TaxonomySerializer(taxonomy, context=context)).data,
)

def test_detail_taxonomy_type_tags_for_plain_taxonomy(self) -> None:
"""
A plain taxonomy's detail response reports taxonomy_type "tags".
"""
self.client.force_authenticate(user=self.staff)
response = self.client.get(TAXONOMY_ORG_DETAIL_URL.format(pk=self.t1.pk))
assert response.status_code == status.HTTP_200_OK
assert response.data["taxonomy_type"] == "tags"

def test_detail_taxonomy_type_tags_for_read_only_taxonomy(self) -> None:
"""
A read-only taxonomy (maintained by the system or an external integration, per
Taxonomy.read_only's docstring) still reports taxonomy_type "tags": is_competency_taxonomy()
keys off the presence of a CompetencyTaxonomy row, not off read_only.
"""
self.client.force_authenticate(user=self.staff)
response = self.client.get(TAXONOMY_ORG_DETAIL_URL.format(pk=self.ro1.pk))
assert response.status_code == status.HTTP_200_OK
assert response.data["taxonomy_type"] == "tags"
assert response.data["read_only"] is True

def test_detail_taxonomy_type_competency(self) -> None:
"""
A competency taxonomy's detail response reports taxonomy_type "competency".
"""
competency_taxonomy = create_competency_taxonomy(name="Nursing Competencies", export_id="nursing-competencies")
set_taxonomy_orgs(taxonomy=competency_taxonomy, all_orgs=True)

self.client.force_authenticate(user=self.staff)
response = self.client.get(TAXONOMY_ORG_DETAIL_URL.format(pk=competency_taxonomy.pk))
assert response.status_code == status.HTTP_200_OK
assert response.data["taxonomy_type"] == "competency"


@skip_unless_cms
class TestTaxonomyExportViewSet(TestTaxonomyDetailExportMixin, APITestCase):
Expand Down
6 changes: 5 additions & 1 deletion openedx/core/djangoapps/content_tagging/rest_api/v1/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from django.http import StreamingHttpResponse
from openedx_authz import api as authz_api
from openedx_authz.constants.permissions import COURSES_MANAGE_TAGS, COURSES_VIEW_COURSE
from openedx_learning.api import create_competency_taxonomy
from openedx_learning.api import create_competency_taxonomy, select_competency_taxonomies
from openedx_tagging import rules as oel_tagging_rules
from openedx_tagging.api import TagDoesNotExist, TaxonomyType
from openedx_tagging.models import Taxonomy
Expand Down Expand Up @@ -106,6 +106,10 @@ def get_queryset(self):
# Annotate with tags_count to avoid selecting all the tags
queryset = queryset.annotate(tags_count=Count("tag", distinct=True))

# Select the competency taxonomy relation (if any) so is_competency_taxonomy()
# costs no extra query per row when serializing taxonomy_type.
queryset = select_competency_taxonomies(queryset)

return queryset

def perform_create(self, serializer) -> None:
Expand Down
Loading