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
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.models_api import CompetencyTaxonomy
from openedx_tagging.models import Tag, Taxonomy
from openedx_tagging.rest_api.v1.serializers import TaxonomySerializer
from organizations.models import Organization
Expand Down Expand Up @@ -520,6 +521,68 @@ def test_create_taxonomy(self, user_attr: str, expected_status: int) -> None:
if user_attr == "staffA":
assert response.data["orgs"] == [self.orgA.short_name]

def test_create_competency_taxonomy(self) -> None:
"""
Posting taxonomy_type="competency" creates a CompetencyTaxonomy linked to
the new Taxonomy.
"""
self.client.force_authenticate(user=self.staff)
response = self.client.post(
TAXONOMY_ORG_LIST_URL,
{"name": "Nursing Competencies", "export_id": "nursing-competencies", "taxonomy_type": "competency"},
format="json",
)
assert response.status_code == status.HTTP_201_CREATED
assert CompetencyTaxonomy.objects.filter(pk=response.data["id"]).exists()

def test_create_competency_taxonomy_duplicate_export_id_returns_400(self) -> None:
"""
A validation failure in the competency branch (duplicate export_id, via
full_clean()) returns a 400, like the "tags" branch, not an unhandled 500.
"""
self.client.force_authenticate(user=self.staff)
self.client.post(
TAXONOMY_ORG_LIST_URL,
{"name": "Existing", "export_id": "duplicate-export-id"},
format="json",
)
response = self.client.post(
TAXONOMY_ORG_LIST_URL,
{"name": "Nursing Competencies", "export_id": "duplicate-export-id", "taxonomy_type": "competency"},
format="json",
)
assert response.status_code == status.HTTP_400_BAD_REQUEST

@ddt.data("tags", None)
def test_create_taxonomy_tags_or_omitted_creates_no_competency_row(self, taxonomy_type: str | None) -> None:
"""
Posting taxonomy_type="tags", or omitting it, creates a plain Taxonomy and
no CompetencyTaxonomy row.
"""
create_data = {"name": "Plain Taxonomy", "export_id": "plain-taxonomy"}
if taxonomy_type is not None:
create_data["taxonomy_type"] = taxonomy_type

self.client.force_authenticate(user=self.staff)
response = self.client.post(TAXONOMY_ORG_LIST_URL, create_data, format="json")

assert response.status_code == status.HTTP_201_CREATED
assert not CompetencyTaxonomy.objects.filter(pk=response.data["id"]).exists()

def test_create_taxonomy_rejects_invalid_type(self) -> None:
"""
An unsupported taxonomy_type (e.g. "system") 400s and names taxonomy_type
as the invalid field.
"""
self.client.force_authenticate(user=self.staff)
response = self.client.post(
TAXONOMY_ORG_LIST_URL,
{"name": "Rejected", "export_id": "rejected", "taxonomy_type": "system"},
format="json",
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert "taxonomy_type" in response.data

@ddt.data(
('staff', 10),
("content_creatorA", 22),
Expand Down Expand Up @@ -2502,6 +2565,68 @@ def test_import_no_file(self) -> None:
# Check if the taxonomy was not created
assert not Taxonomy.objects.filter(name="Imported Taxonomy name").exists()

def test_import_competency_taxonomy(self) -> None:
"""
Importing with taxonomy_type="competency" creates a CompetencyTaxonomy
linked to the new Taxonomy.
"""
file = self._get_file([{"id": "tag_1", "value": "Tag 1"}], "json")
self.client.force_authenticate(user=self.staff)
response = self.client.post(
TAXONOMY_CREATE_IMPORT_URL,
{
"taxonomy_name": "Imported Competency",
"taxonomy_description": "Imported Competency description",
"taxonomy_export_id": "imported-competency",
"taxonomy_type": "competency",
"file": file,
},
format="multipart",
)
assert response.status_code == status.HTTP_201_CREATED
assert CompetencyTaxonomy.objects.filter(pk=response.data["id"]).exists()

def test_import_without_type_creates_no_competency_row(self) -> None:
"""
Importing without taxonomy_type creates a plain Taxonomy and no
CompetencyTaxonomy row.
"""
file = self._get_file([{"id": "tag_1", "value": "Tag 1"}], "json")
self.client.force_authenticate(user=self.staff)
response = self.client.post(
TAXONOMY_CREATE_IMPORT_URL,
{
"taxonomy_name": "Imported Plain",
"taxonomy_description": "Imported Plain description",
"taxonomy_export_id": "imported-plain",
"file": file,
},
format="multipart",
)
assert response.status_code == status.HTTP_201_CREATED
assert not CompetencyTaxonomy.objects.filter(pk=response.data["id"]).exists()

def test_import_rejects_invalid_type(self) -> None:
"""
An unsupported taxonomy_type (e.g. "system") 400s on import and names
taxonomy_type as the invalid field.
"""
file = self._get_file([{"id": "tag_1", "value": "Tag 1"}], "json")
self.client.force_authenticate(user=self.staff)
response = self.client.post(
TAXONOMY_CREATE_IMPORT_URL,
{
"taxonomy_name": "Imported Rejected",
"taxonomy_description": "Imported Rejected description",
"taxonomy_export_id": "imported-rejected",
"taxonomy_type": "system",
"file": file,
},
format="multipart",
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert "taxonomy_type" in response.data

@ddt.data(
"csv",
"json",
Expand Down
36 changes: 31 additions & 5 deletions openedx/core/djangoapps/content_tagging/rest_api/v1/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@
import functools
from typing import TYPE_CHECKING

from django.core import exceptions
from django.db.models import Count
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_tagging import rules as oel_tagging_rules
from openedx_tagging.api import TagDoesNotExist
from openedx_tagging.api import TagDoesNotExist, TaxonomyType
from openedx_tagging.models import Taxonomy
from openedx_tagging.rest_api.v1.views import ObjectTagView, TaxonomyView
from rest_framework import status
from rest_framework.decorators import action
Expand All @@ -24,7 +27,6 @@

from ...api import (
InvalidOrgException,
create_taxonomy,
generate_csv_rows,
get_taxonomies,
get_taxonomies_for_org,
Expand Down Expand Up @@ -100,12 +102,36 @@ def get_queryset(self):

return queryset

def perform_create(self, serializer):
def perform_create(self, serializer) -> None:
"""
Create a new taxonomy.
Create a new taxonomy (competency or tags).
"""
taxonomy_type = serializer.validated_data.pop("taxonomy_type", TaxonomyType.TAGS.value)
if taxonomy_type == TaxonomyType.COMPETENCY.value:
try:
serializer.instance = create_competency_taxonomy(**serializer.validated_data)
except exceptions.ValidationError as e:
raise ValidationError() from e
else:
super().perform_create(serializer)
user_admin_orgs = get_admin_orgs(self.request.user)
serializer.instance = create_taxonomy(**serializer.validated_data, orgs=user_admin_orgs)
set_taxonomy_orgs(taxonomy=serializer.instance, all_orgs=False, orgs=user_admin_orgs)

def _create_taxonomy_for_import(self, validated_data: dict) -> Taxonomy:
"""
Create a competency taxonomy if requested, otherwise defer to the base implementation.
"""
taxonomy_type = validated_data.get("taxonomy_type", TaxonomyType.TAGS.value)
if taxonomy_type == TaxonomyType.COMPETENCY.value:
try:
return create_competency_taxonomy(
name=validated_data["taxonomy_name"],
description=validated_data["taxonomy_description"],
export_id=validated_data.get("taxonomy_export_id"),
)
except exceptions.ValidationError as e:
raise ValidationError() from e
return super()._create_taxonomy_for_import(validated_data)

@action(detail=False, url_path="import", methods=["post"])
def create_import(self, request: RestRequest, **kwargs) -> Response: # type: ignore
Expand Down
2 changes: 1 addition & 1 deletion requirements/edx/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -826,7 +826,7 @@ openedx-calc==5.0.0
# via
# openedx-platform
# xblocks-contrib
openedx-core==1.3.0
openedx-core==1.4.0
# via openedx-platform
openedx-django-pyfs==4.0.0
# via xblock
Expand Down
2 changes: 1 addition & 1 deletion requirements/edx/development.txt
Original file line number Diff line number Diff line change
Expand Up @@ -928,7 +928,7 @@ openedx-calc==5.0.0
# via
# openedx-platform
# xblocks-contrib
openedx-core==1.3.0
openedx-core==1.4.0
# via openedx-platform
openedx-django-pyfs==4.0.0
# via xblock
Expand Down
6 changes: 3 additions & 3 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading