From af1955907d0683905750595bda9b548d2208c122 Mon Sep 17 00:00:00 2001 From: alezconsultant Date: Thu, 3 Sep 2026 16:42:24 +0400 Subject: [PATCH 1/4] feat: accept taxonomy_type on taxonomy create and import endpoints Add taxonomy_type as a write-only ChoiceField on TaxonomySerializer ("tags" default, "competency" via a new TaxonomyType enum). TaxonomyView.perform_create() and create_import() discard it after validation: openedx_tagging must never import openedx_learning, so it can't act on it. create_import()'s taxonomy-creation step is extracted into an overridable _create_taxonomy_for_import() hook for subclasses. Add create_competency_taxonomy() to the CBE applet: creates the Taxonomy and linked CompetencyTaxonomy row in one transaction, via save_base(raw=True) since save() would re-save Taxonomy with unpopulated field values. Add CompetencyTaxonomyView(TaxonomyView), overriding perform_create() and _create_taxonomy_for_import() to dispatch to create_competency_taxonomy() when taxonomy_type="competency". Lives here since this is the layer that can see both openedx_tagging and openedx_learning. See openedx/openedx-core#628 Co-Authored-By: Claude Sonnet 5 --- src/openedx_learning/api.py | 1 + src/openedx_learning/applets/cbe/api.py | 39 ++++++++ src/openedx_learning/applets/cbe/views.py | 39 ++++++++ src/openedx_tagging/api.py | 10 ++ .../rest_api/v1/serializers.py | 12 +++ src/openedx_tagging/rest_api/v1/views.py | 23 +++-- .../openedx_learning/applets/cbe/test_api.py | 52 +++++++++- tests/openedx_tagging/test_views.py | 95 +++++++++++++++++++ 8 files changed, 260 insertions(+), 11 deletions(-) create mode 100644 src/openedx_learning/applets/cbe/views.py diff --git a/src/openedx_learning/api.py b/src/openedx_learning/api.py index 789a315f3..9c2740f6d 100644 --- a/src/openedx_learning/api.py +++ b/src/openedx_learning/api.py @@ -4,3 +4,4 @@ # This wildcard import is okay because the applet api module declares __all__. # pylint: disable=wildcard-import from .applets.cbe.api import * +from .applets.cbe.views import CompetencyTaxonomyView # pylint: disable=unused-import diff --git a/src/openedx_learning/applets/cbe/api.py b/src/openedx_learning/applets/cbe/api.py index 8fc4861dc..08e2c534d 100644 --- a/src/openedx_learning/applets/cbe/api.py +++ b/src/openedx_learning/applets/cbe/api.py @@ -3,16 +3,55 @@ """ from __future__ import annotations +from django.db import transaction from django.db.models import QuerySet +from openedx_tagging.api import create_taxonomy from openedx_tagging.models import Taxonomy +from .models import CompetencyTaxonomy + __all__ = [ + "create_competency_taxonomy", "is_competency_taxonomy", "select_competency_taxonomies", ] +def create_competency_taxonomy( # pylint: disable=too-many-positional-arguments + name: str, + description: str | None = None, + enabled=True, + allow_multiple=True, + allow_free_text=False, + read_only=False, + export_id: str | None = None, +) -> CompetencyTaxonomy: + """ + Create, save, and return a new CompetencyTaxonomy with the given attributes. + """ + with transaction.atomic(): + taxonomy = create_taxonomy( + name=name, + description=description, + enabled=enabled, + allow_multiple=allow_multiple, + allow_free_text=allow_free_text, + read_only=read_only, + export_id=export_id, + ) + competency_taxonomy = CompetencyTaxonomy(taxonomy_ptr=taxonomy) + # Copy the parent's fields onto the child instance: save_base(raw=True) below writes + # only the child's own row and skips Taxonomy entirely, so it never reads these back + # off the DB itself the way a normal save() of the MTI chain would. + for field in Taxonomy._meta.fields: + setattr(competency_taxonomy, field.attname, getattr(taxonomy, field.attname)) + competency_taxonomy.save_base(raw=True) + # competency_taxonomy carries every Taxonomy field too (copied above), so it's usable + # anywhere a Taxonomy is expected, without a second query to re-fetch the parent row. + return competency_taxonomy + + def is_competency_taxonomy(taxonomy: Taxonomy) -> bool: """ Return True if ``taxonomy`` is competency-enabled, i.e. has a CompetencyTaxonomy row. diff --git a/src/openedx_learning/applets/cbe/views.py b/src/openedx_learning/applets/cbe/views.py new file mode 100644 index 000000000..bc433000a --- /dev/null +++ b/src/openedx_learning/applets/cbe/views.py @@ -0,0 +1,39 @@ +""" +REST API views for Competency-Based Education (CBE). +""" +from __future__ import annotations + +from openedx_tagging.api import TaxonomyType +from openedx_tagging.models import Taxonomy +from openedx_tagging.rest_api.v1.views import TaxonomyView + +from .api import create_competency_taxonomy + + +class CompetencyTaxonomyView(TaxonomyView): + """ + TaxonomyView that also supports taxonomy_type="competency". + """ + + def perform_create(self, serializer) -> None: + """ + Create a new taxonomy (competency or tags). + """ + taxonomy_type = serializer.validated_data.pop("taxonomy_type", TaxonomyType.TAGS.value) + if taxonomy_type == TaxonomyType.COMPETENCY.value: + serializer.instance = create_competency_taxonomy(**serializer.validated_data) + else: + super().perform_create(serializer) + + 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: + return create_competency_taxonomy( + name=validated_data["taxonomy_name"], + description=validated_data["taxonomy_description"], + export_id=validated_data.get("taxonomy_export_id"), + ) + return super()._create_taxonomy_for_import(validated_data) diff --git a/src/openedx_tagging/api.py b/src/openedx_tagging/api.py index 48ea82a2a..163571cf2 100644 --- a/src/openedx_tagging/api.py +++ b/src/openedx_tagging/api.py @@ -13,6 +13,7 @@ from __future__ import annotations from collections import defaultdict +from enum import Enum from typing import Any, Counter, cast from django.db import models, transaction @@ -31,6 +32,15 @@ OBJECT_MAX_TAGS = 100 +class TaxonomyType(Enum): + """ + Valid values for a taxonomy's type on create. + """ + + TAGS = "tags" + COMPETENCY = "competency" + + def create_taxonomy( # pylint: disable=too-many-positional-arguments name: str, description: str | None = None, diff --git a/src/openedx_tagging/rest_api/v1/serializers.py b/src/openedx_tagging/rest_api/v1/serializers.py index fcab256a3..18dd34e59 100644 --- a/src/openedx_tagging/rest_api/v1/serializers.py +++ b/src/openedx_tagging/rest_api/v1/serializers.py @@ -10,6 +10,7 @@ from rest_framework.request import Request from rest_framework.reverse import reverse +from openedx_tagging.api import TaxonomyType from openedx_tagging.data import TagData from openedx_tagging.import_export.parsers import ParserFormat from openedx_tagging.models import ObjectTag, Tag, TagImportTask, Taxonomy @@ -74,6 +75,11 @@ class TaxonomySerializer(UserPermissionsSerializerMixin, serializers.ModelSerial can_delete_taxonomy = serializers.SerializerMethodField(method_name='get_can_delete') can_tag_object = serializers.SerializerMethodField() export_id = serializers.CharField(required=False) + taxonomy_type = serializers.ChoiceField( + choices=[TaxonomyType.TAGS.value, TaxonomyType.COMPETENCY.value], + default=TaxonomyType.TAGS.value, + write_only=True, + ) class Meta: model = Taxonomy @@ -91,6 +97,7 @@ class Meta: "can_delete_taxonomy", "can_tag_object", "export_id", + "taxonomy_type", ] def get_tags_count(self, instance): @@ -429,6 +436,11 @@ class TaxonomyImportNewBodySerializer(TaxonomyImportBodySerializer): # pylint: taxonomy_name = serializers.CharField(required=True) taxonomy_description = serializers.CharField(default="") taxonomy_export_id = serializers.CharField(required=False) + taxonomy_type = serializers.ChoiceField( + choices=[TaxonomyType.TAGS.value, TaxonomyType.COMPETENCY.value], + default=TaxonomyType.TAGS.value, + write_only=True, + ) class TagImportTaskSerializer(serializers.ModelSerializer): diff --git a/src/openedx_tagging/rest_api/v1/views.py b/src/openedx_tagging/rest_api/v1/views.py index f079a048d..9bf57b1bb 100644 --- a/src/openedx_tagging/rest_api/v1/views.py +++ b/src/openedx_tagging/rest_api/v1/views.py @@ -263,6 +263,7 @@ def perform_create(self, serializer) -> None: """ Create a new taxonomy. """ + serializer.validated_data.pop("taxonomy_type", None) try: serializer.instance = create_taxonomy(**serializer.validated_data) except exceptions.ValidationError as e: @@ -298,6 +299,17 @@ def export(self, request, **_kwargs) -> HttpResponse: return HttpResponse(tags, content_type=content_type) + def _create_taxonomy_for_import(self, validated_data: dict) -> Taxonomy: + """ + Create the taxonomy for create_import(). Override to support other taxonomy_type values. + """ + validated_data.pop("taxonomy_type", None) + return create_taxonomy( + validated_data["taxonomy_name"], + validated_data["taxonomy_description"], + export_id=validated_data.get("taxonomy_export_id"), + ) + @action(detail=False, url_path="import", methods=["post"]) def create_import(self, request: Request, **_kwargs) -> Response: """ @@ -306,18 +318,9 @@ def create_import(self, request: Request, **_kwargs) -> Response: body = TaxonomyImportNewBodySerializer(data=request.data) body.is_valid(raise_exception=True) - taxonomy_name = body.validated_data["taxonomy_name"] - taxonomy_export_id = body.validated_data.get("taxonomy_export_id") - taxonomy_description = body.validated_data["taxonomy_description"] file = body.validated_data["file"].file parser_format = body.validated_data["parser_format"] - - # If no taxonomy_export_id provided, a unique export id will be generated - taxonomy = create_taxonomy( - taxonomy_name, - taxonomy_description, - export_id=taxonomy_export_id, - ) + taxonomy = self._create_taxonomy_for_import(body.validated_data) try: import_success, task, _plan = import_tags(taxonomy, file, parser_format) diff --git a/tests/openedx_learning/applets/cbe/test_api.py b/tests/openedx_learning/applets/cbe/test_api.py index 8ebe712bf..cd20630b9 100644 --- a/tests/openedx_learning/applets/cbe/test_api.py +++ b/tests/openedx_learning/applets/cbe/test_api.py @@ -3,13 +3,63 @@ """ import pytest -from openedx_learning.api import is_competency_taxonomy, select_competency_taxonomies +from openedx_learning.api import create_competency_taxonomy, is_competency_taxonomy, select_competency_taxonomies from openedx_learning.models import CompetencyTaxonomy from openedx_tagging.models import Taxonomy pytestmark = pytest.mark.django_db +def test_create_competency_taxonomy_saves_both_rows() -> None: + """ + create_competency_taxonomy() saves a CompetencyTaxonomy and Taxonomy row that both + carry the given field values. + + Re-fetching from Taxonomy.objects (not just CompetencyTaxonomy.objects) is the + regression check for using save_base(raw=True): a plain save() on the child + instance would re-save the parent Taxonomy row with blank/default field values, + which this assertion would catch. + """ + result = create_competency_taxonomy( + name="Nursing", + description="Nursing competencies", + enabled=False, + allow_multiple=False, + allow_free_text=True, + read_only=True, + export_id="nursing-v1", + ) + + assert isinstance(result, CompetencyTaxonomy) + assert is_competency_taxonomy(result) is True + + for taxonomy in ( + CompetencyTaxonomy.objects.get(pk=result.pk), + Taxonomy.objects.get(pk=result.pk), + ): + assert taxonomy.name == "Nursing" + assert taxonomy.description == "Nursing competencies" + assert taxonomy.enabled is False + assert taxonomy.allow_multiple is False + assert taxonomy.allow_free_text is True + assert taxonomy.read_only is True + assert taxonomy.export_id == "nursing-v1" + + +def test_create_competency_taxonomy_defaults() -> None: + """ + create_competency_taxonomy() applies the same defaults as create_taxonomy() when + only name is given, including an auto-generated export_id. + """ + result = create_competency_taxonomy(name="Welding") + + assert result.enabled is True + assert result.allow_multiple is True + assert result.allow_free_text is False + assert result.read_only is False + assert result.export_id + + def test_is_competency_taxonomy() -> None: """ is_competency_taxonomy() is True for a competency taxonomy, False for a plain one. diff --git a/tests/openedx_tagging/test_views.py b/tests/openedx_tagging/test_views.py index 99322dfb8..b0ad505c8 100644 --- a/tests/openedx_tagging/test_views.py +++ b/tests/openedx_tagging/test_views.py @@ -396,6 +396,47 @@ def test_create_taxonomy_read_only(self, create_data): assert response.status_code == status.HTTP_201_CREATED assert response.data["read_only"] is True + @ddt.data( + ("tags", status.HTTP_201_CREATED), + ("competency", status.HTTP_201_CREATED), + (None, status.HTTP_201_CREATED), + ) + @ddt.unpack + def test_create_taxonomy_type_tags_or_omitted(self, taxonomy_type, expected_status): + """ + Posting any accepted taxonomy_type (or omitting it) to the raw create endpoint + (this raw TaxonomyView) creates a plain Taxonomy: this endpoint must + never import openedx_learning, so it has no way to act on "competency" beyond + accepting it as a valid choice and discarding it -- creating a CompetencyTaxonomy + row for it is a downstream consumer's job. + """ + url = TAXONOMY_LIST_URL + create_data = {"name": "Taxonomy Type Test", "export_id": "taxonomy-type-test"} + if taxonomy_type is not None: + create_data["taxonomy_type"] = taxonomy_type + + self.client.force_authenticate(user=self.staff) + response = self.client.post(url, create_data, format="json") + assert response.status_code == expected_status + assert Taxonomy.objects.filter(name="Taxonomy Type Test").exists() + + @ddt.data("system", "bogus") + def test_create_taxonomy_type_invalid_value_rejected(self, taxonomy_type): + """ + An unsupported taxonomy_type value still 400s via the ChoiceField itself. + """ + url = TAXONOMY_LIST_URL + + self.client.force_authenticate(user=self.staff) + response = self.client.post( + url, + {"name": "Rejected Invalid", "export_id": "rejected-invalid", "taxonomy_type": taxonomy_type}, + format="json", + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert "taxonomy_type" in response.data + assert not Taxonomy.objects.filter(name="Rejected Invalid").exists() + @ddt.data( (None, status.HTTP_401_UNAUTHORIZED), ("user", status.HTTP_403_FORBIDDEN), @@ -3237,6 +3278,60 @@ def test_import_no_export_id(self, file_format) -> None: for i, tag in enumerate(tags): assert tag["value"] == new_tags[i]["value"] + @ddt.data( + ("tags", status.HTTP_201_CREATED), + ("competency", status.HTTP_201_CREATED), + (None, status.HTTP_201_CREATED), + ) + @ddt.unpack + def test_import_taxonomy_type_tags_or_omitted(self, taxonomy_type, expected_status) -> None: + """ + Posting any accepted taxonomy_type (or omitting it) to the raw create/import + endpoint (this raw TaxonomyView) creates a plain Taxonomy: this + endpoint must never import openedx_learning, so it has no way to act on + "competency" beyond accepting it as a valid choice and discarding it -- creating + a CompetencyTaxonomy row for it is a downstream consumer's job. + """ + url = TAXONOMY_CREATE_IMPORT_URL + new_tags = [{"id": "tag_1", "value": "Tag 1"}] + file = self._get_file(new_tags, "json") + data = { + "taxonomy_name": "Taxonomy Type Import Test", + "taxonomy_description": "Imported Taxonomy description", + "file": file, + } + if taxonomy_type is not None: + data["taxonomy_type"] = taxonomy_type + + self.client.force_authenticate(user=self.staff) + response = self.client.post(url, data, format="multipart") + assert response.status_code == expected_status + assert Taxonomy.objects.filter(name="Taxonomy Type Import Test").exists() + + @ddt.data("system", "bogus") + def test_import_taxonomy_type_invalid_value_rejected(self, taxonomy_type) -> None: + """ + An unsupported taxonomy_type value still 400s via the ChoiceField itself. + """ + url = TAXONOMY_CREATE_IMPORT_URL + new_tags = [{"id": "tag_1", "value": "Tag 1"}] + file = self._get_file(new_tags, "json") + + self.client.force_authenticate(user=self.staff) + response = self.client.post( + url, + { + "taxonomy_name": "Rejected Invalid Import", + "taxonomy_description": "Imported Taxonomy description", + "taxonomy_type": taxonomy_type, + "file": file, + }, + format="multipart", + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert "taxonomy_type" in response.data + assert not Taxonomy.objects.filter(name="Rejected Invalid Import").exists() + @ddt.ddt class TestImportTagsView(ImportTaxonomyMixin, APITestCase): From 2882c30173e84fbdb1ec0f49d7d179d76980f784 Mon Sep 17 00:00:00 2001 From: alezconsultant Date: Fri, 4 Sep 2026 03:08:44 +0400 Subject: [PATCH 2/4] fix: return 400 on competency taxonomy validation failure and new tests --- src/openedx_learning/applets/cbe/views.py | 8 +- src/openedx_tagging/api.py | 3 + .../applets/cbe/test_views.py | 144 ++++++++++++++++++ tests/openedx_tagging/test_views.py | 14 +- 4 files changed, 158 insertions(+), 11 deletions(-) create mode 100644 tests/openedx_learning/applets/cbe/test_views.py diff --git a/src/openedx_learning/applets/cbe/views.py b/src/openedx_learning/applets/cbe/views.py index bc433000a..2def6f754 100644 --- a/src/openedx_learning/applets/cbe/views.py +++ b/src/openedx_learning/applets/cbe/views.py @@ -3,6 +3,9 @@ """ from __future__ import annotations +from django.core import exceptions +from rest_framework.exceptions import ValidationError + from openedx_tagging.api import TaxonomyType from openedx_tagging.models import Taxonomy from openedx_tagging.rest_api.v1.views import TaxonomyView @@ -21,7 +24,10 @@ def perform_create(self, serializer) -> None: """ taxonomy_type = serializer.validated_data.pop("taxonomy_type", TaxonomyType.TAGS.value) if taxonomy_type == TaxonomyType.COMPETENCY.value: - serializer.instance = create_competency_taxonomy(**serializer.validated_data) + try: + serializer.instance = create_competency_taxonomy(**serializer.validated_data) + except exceptions.ValidationError as e: + raise ValidationError() from e else: super().perform_create(serializer) diff --git a/src/openedx_tagging/api.py b/src/openedx_tagging/api.py index 163571cf2..307b8175f 100644 --- a/src/openedx_tagging/api.py +++ b/src/openedx_tagging/api.py @@ -52,6 +52,9 @@ def create_taxonomy( # pylint: disable=too-many-positional-arguments ) -> Taxonomy: """ Creates, saves, and returns a new Taxonomy with the given attributes. + + If `export_id` is not given, one is auto-generated from the current + Taxonomy count and a slug of `name`. """ if not export_id: export_id = f"{Taxonomy.objects.count() + 1}-{slugify(name, allow_unicode=True)}" diff --git a/tests/openedx_learning/applets/cbe/test_views.py b/tests/openedx_learning/applets/cbe/test_views.py new file mode 100644 index 000000000..6c261e57d --- /dev/null +++ b/tests/openedx_learning/applets/cbe/test_views.py @@ -0,0 +1,144 @@ +""" +Tests for CompetencyTaxonomyView's taxonomy_type dispatch, plus a TaxonomyView +assertion tests/openedx_tagging can't make for itself. + +Neither view is registered on a URL in openedx-core yet, so these tests call +as_view() directly via APIRequestFactory instead of self.client against a URL. +""" +from __future__ import annotations + +import json + +import pytest +from django.contrib.auth import get_user_model +from django.core.files.uploadedfile import SimpleUploadedFile +from rest_framework import status +from rest_framework.test import APIRequestFactory, force_authenticate + +from openedx_learning.applets.cbe.views import CompetencyTaxonomyView +from openedx_learning.models import CompetencyTaxonomy +from openedx_tagging.api import create_taxonomy +from openedx_tagging.models import Taxonomy +from openedx_tagging.rest_api.v1.views import TaxonomyView + +pytestmark = pytest.mark.django_db + +User = get_user_model() + +create_view = CompetencyTaxonomyView.as_view({"post": "create"}) +create_import_view = CompetencyTaxonomyView.as_view({"post": "create_import"}) +base_taxonomy_create_view = TaxonomyView.as_view({"post": "create"}) + + +def _staff_user(): + return User.objects.create(username="staff", email="staff@example.com", is_staff=True) + + +def test_perform_create_competency_creates_competency_taxonomy() -> None: + """ + Posting taxonomy_type="competency" through CompetencyTaxonomyView creates a + CompetencyTaxonomy row, not just a plain Taxonomy. + """ + request = APIRequestFactory().post( + "/fake-competency-taxonomies/", + {"name": "Nursing Competencies", "export_id": "nursing-competencies", "taxonomy_type": "competency"}, + format="json", + ) + force_authenticate(request, user=_staff_user()) + + response = create_view(request) + + assert response.status_code == status.HTTP_201_CREATED + assert CompetencyTaxonomy.objects.filter(pk=response.data["id"]).exists() + + +def test_perform_create_tags_creates_no_competency_row() -> None: + """ + Posting taxonomy_type="tags" through CompetencyTaxonomyView creates a plain + Taxonomy, not a CompetencyTaxonomy row. + """ + request = APIRequestFactory().post( + "/fake-competency-taxonomies/", + {"name": "Tags Type Test", "export_id": "tags-type-test", "taxonomy_type": "tags"}, + format="json", + ) + force_authenticate(request, user=_staff_user()) + + response = create_view(request) + + assert response.status_code == status.HTTP_201_CREATED + assert Taxonomy.objects.filter(name="Tags Type Test").exists() + assert not CompetencyTaxonomy.objects.filter(name="Tags Type Test").exists() + + +def test_perform_create_competency_duplicate_export_id_returns_400() -> 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. + """ + create_taxonomy(name="Existing Taxonomy", export_id="duplicate-export-id") + request = APIRequestFactory().post( + "/fake-competency-taxonomies/", + {"name": "Nursing Competencies", "export_id": "duplicate-export-id", "taxonomy_type": "competency"}, + format="json", + ) + force_authenticate(request, user=_staff_user()) + + response = create_view(request) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + + +def test_create_import_tags_creates_no_competency_row() -> None: + """ + Importing with taxonomy_type="tags" through CompetencyTaxonomyView creates a + plain Taxonomy, not a CompetencyTaxonomy row. + """ + file = SimpleUploadedFile( + "taxonomy.json", + json.dumps({"tags": [{"id": "tag_1", "value": "Tag 1"}]}).encode(), + content_type="application/json", + ) + request = APIRequestFactory().post( + "/fake-competency-taxonomies/import/", + { + "taxonomy_name": "Imported Tags Type Test", + "taxonomy_export_id": "imported-tags-type-test", + "taxonomy_type": "tags", + "file": file, + }, + format="multipart", + ) + force_authenticate(request, user=_staff_user()) + + response = create_import_view(request) + + assert response.status_code == status.HTTP_201_CREATED + assert Taxonomy.objects.filter(name="Imported Tags Type Test").exists() + assert not CompetencyTaxonomy.objects.filter(name="Imported Tags Type Test").exists() + + +def test_base_taxonomy_view_competency_type_creates_no_competency_row() -> None: + """ + Posting taxonomy_type="competency" through the base TaxonomyView creates a + plain Taxonomy and no CompetencyTaxonomy row. + + tests/openedx_tagging/test_views.py can't check this directly: it would have + to import CompetencyTaxonomy, breaking the layering rule it's demonstrating. + """ + request = APIRequestFactory().post( + "/fake-taxonomies/", + { + "name": "Base View Competency Type", + "export_id": "base-view-competency-type", + "taxonomy_type": "competency", + }, + format="json", + ) + force_authenticate(request, user=_staff_user()) + + response = base_taxonomy_create_view(request) + + assert response.status_code == status.HTTP_201_CREATED + assert Taxonomy.objects.filter(name="Base View Competency Type").exists() + assert not CompetencyTaxonomy.objects.filter(name="Base View Competency Type").exists() diff --git a/tests/openedx_tagging/test_views.py b/tests/openedx_tagging/test_views.py index b0ad505c8..f37c5456f 100644 --- a/tests/openedx_tagging/test_views.py +++ b/tests/openedx_tagging/test_views.py @@ -404,11 +404,8 @@ def test_create_taxonomy_read_only(self, create_data): @ddt.unpack def test_create_taxonomy_type_tags_or_omitted(self, taxonomy_type, expected_status): """ - Posting any accepted taxonomy_type (or omitting it) to the raw create endpoint - (this raw TaxonomyView) creates a plain Taxonomy: this endpoint must - never import openedx_learning, so it has no way to act on "competency" beyond - accepting it as a valid choice and discarding it -- creating a CompetencyTaxonomy - row for it is a downstream consumer's job. + Posting any accepted taxonomy_type (or omitting it) succeeds and creates a + Taxonomy row. """ url = TAXONOMY_LIST_URL create_data = {"name": "Taxonomy Type Test", "export_id": "taxonomy-type-test"} @@ -3286,11 +3283,8 @@ def test_import_no_export_id(self, file_format) -> None: @ddt.unpack def test_import_taxonomy_type_tags_or_omitted(self, taxonomy_type, expected_status) -> None: """ - Posting any accepted taxonomy_type (or omitting it) to the raw create/import - endpoint (this raw TaxonomyView) creates a plain Taxonomy: this - endpoint must never import openedx_learning, so it has no way to act on - "competency" beyond accepting it as a valid choice and discarding it -- creating - a CompetencyTaxonomy row for it is a downstream consumer's job. + Posting any accepted taxonomy_type (or omitting it) succeeds and creates a + Taxonomy row. """ url = TAXONOMY_CREATE_IMPORT_URL new_tags = [{"id": "tag_1", "value": "Tag 1"}] From d2ba55f910c078b519a09555f90aa240f9210dd7 Mon Sep 17 00:00:00 2001 From: alezconsultant Date: Fri, 4 Sep 2026 03:26:01 +0400 Subject: [PATCH 3/4] fix: change tagging docstrings --- tests/openedx_tagging/test_views.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/openedx_tagging/test_views.py b/tests/openedx_tagging/test_views.py index f37c5456f..a6743440d 100644 --- a/tests/openedx_tagging/test_views.py +++ b/tests/openedx_tagging/test_views.py @@ -404,8 +404,12 @@ def test_create_taxonomy_read_only(self, create_data): @ddt.unpack def test_create_taxonomy_type_tags_or_omitted(self, taxonomy_type, expected_status): """ - Posting any accepted taxonomy_type (or omitting it) succeeds and creates a - Taxonomy row. + Posting any accepted taxonomy_type (or omitting it) to this raw TaxonomyView + succeeds and creates a Taxonomy row. + + Doesn't check whether a CompetencyTaxonomy row also gets created for + "competency". See tests/openedx_learning/applets/cbe/test_views.py for + that assertion. """ url = TAXONOMY_LIST_URL create_data = {"name": "Taxonomy Type Test", "export_id": "taxonomy-type-test"} @@ -3283,8 +3287,12 @@ def test_import_no_export_id(self, file_format) -> None: @ddt.unpack def test_import_taxonomy_type_tags_or_omitted(self, taxonomy_type, expected_status) -> None: """ - Posting any accepted taxonomy_type (or omitting it) succeeds and creates a - Taxonomy row. + Posting any accepted taxonomy_type (or omitting it) to this raw create/import + endpoint succeeds and creates a Taxonomy row. + + Doesn't check whether a CompetencyTaxonomy row also gets created for + "competency". + See tests/openedx_learning/applets/cbe/test_views.py for that assertion. """ url = TAXONOMY_CREATE_IMPORT_URL new_tags = [{"id": "tag_1", "value": "Tag 1"}] From a3d573a3856c79425a9a003945d8b3a2324c4c5a Mon Sep 17 00:00:00 2001 From: alezconsultant Date: Fri, 4 Sep 2026 06:37:15 +0400 Subject: [PATCH 4/4] fix: remove redundant tests comments --- tests/openedx_learning/applets/cbe/test_views.py | 3 --- tests/openedx_tagging/test_views.py | 8 -------- 2 files changed, 11 deletions(-) diff --git a/tests/openedx_learning/applets/cbe/test_views.py b/tests/openedx_learning/applets/cbe/test_views.py index 6c261e57d..d3b458203 100644 --- a/tests/openedx_learning/applets/cbe/test_views.py +++ b/tests/openedx_learning/applets/cbe/test_views.py @@ -122,9 +122,6 @@ def test_base_taxonomy_view_competency_type_creates_no_competency_row() -> None: """ Posting taxonomy_type="competency" through the base TaxonomyView creates a plain Taxonomy and no CompetencyTaxonomy row. - - tests/openedx_tagging/test_views.py can't check this directly: it would have - to import CompetencyTaxonomy, breaking the layering rule it's demonstrating. """ request = APIRequestFactory().post( "/fake-taxonomies/", diff --git a/tests/openedx_tagging/test_views.py b/tests/openedx_tagging/test_views.py index a6743440d..da933f7c0 100644 --- a/tests/openedx_tagging/test_views.py +++ b/tests/openedx_tagging/test_views.py @@ -406,10 +406,6 @@ def test_create_taxonomy_type_tags_or_omitted(self, taxonomy_type, expected_stat """ Posting any accepted taxonomy_type (or omitting it) to this raw TaxonomyView succeeds and creates a Taxonomy row. - - Doesn't check whether a CompetencyTaxonomy row also gets created for - "competency". See tests/openedx_learning/applets/cbe/test_views.py for - that assertion. """ url = TAXONOMY_LIST_URL create_data = {"name": "Taxonomy Type Test", "export_id": "taxonomy-type-test"} @@ -3289,10 +3285,6 @@ def test_import_taxonomy_type_tags_or_omitted(self, taxonomy_type, expected_stat """ Posting any accepted taxonomy_type (or omitting it) to this raw create/import endpoint succeeds and creates a Taxonomy row. - - Doesn't check whether a CompetencyTaxonomy row also gets created for - "competency". - See tests/openedx_learning/applets/cbe/test_views.py for that assertion. """ url = TAXONOMY_CREATE_IMPORT_URL new_tags = [{"id": "tag_1", "value": "Tag 1"}]