From c81c7bdbacc75aeaf9310b637974e457d0f95c8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Thu, 10 Aug 2023 13:24:14 -0300 Subject: [PATCH] feat: add taxonomies for org api --- cms/urls.py | 5 + lms/urls.py | 5 + .../features/content_tagging/models/base.py | 2 +- .../content_tagging/rest_api/__init__.py | 0 .../features/content_tagging/rest_api/urls.py | 9 + .../content_tagging/rest_api/v1/__init__.py | 0 .../content_tagging/rest_api/v1/filters.py | 21 + .../rest_api/v1/serializers.py | 35 + .../rest_api/v1/tests/__init__.py | 0 .../rest_api/v1/tests/test_views.py | 637 ++++++++++++++++++ .../content_tagging/rest_api/v1/urls.py | 16 + .../content_tagging/rest_api/v1/views.py | 153 +++++ openedx/features/content_tagging/rules.py | 58 +- .../content_tagging/tests/test_rules.py | 129 ++-- openedx/features/content_tagging/urls.py | 10 + 15 files changed, 1003 insertions(+), 77 deletions(-) create mode 100644 openedx/features/content_tagging/rest_api/__init__.py create mode 100644 openedx/features/content_tagging/rest_api/urls.py create mode 100644 openedx/features/content_tagging/rest_api/v1/__init__.py create mode 100644 openedx/features/content_tagging/rest_api/v1/filters.py create mode 100644 openedx/features/content_tagging/rest_api/v1/serializers.py create mode 100644 openedx/features/content_tagging/rest_api/v1/tests/__init__.py create mode 100644 openedx/features/content_tagging/rest_api/v1/tests/test_views.py create mode 100644 openedx/features/content_tagging/rest_api/v1/urls.py create mode 100644 openedx/features/content_tagging/rest_api/v1/views.py create mode 100644 openedx/features/content_tagging/urls.py diff --git a/cms/urls.py b/cms/urls.py index 5631c3aa957b..a4e2309ad85c 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -332,3 +332,8 @@ urlpatterns += [ path('api/contentstore/', include('cms.djangoapps.contentstore.rest_api.urls')) ] + +# Content tagging +urlpatterns += [ + path('api/content_tagging/', include(('openedx.features.content_tagging.urls'))), +] diff --git a/lms/urls.py b/lms/urls.py index 8b8462034d86..7fd32bf12797 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -1049,3 +1049,8 @@ urlpatterns += [ path('api/notifications/', include('openedx.core.djangoapps.notifications.urls')), ] + +# Content tagging +urlpatterns += [ + path('api/content_tagging/', include(('openedx.features.content_tagging.urls'))), +] diff --git a/openedx/features/content_tagging/models/base.py b/openedx/features/content_tagging/models/base.py index 255d3852fa47..16df0d3752e0 100644 --- a/openedx/features/content_tagging/models/base.py +++ b/openedx/features/content_tagging/models/base.py @@ -49,7 +49,7 @@ class Meta: @classmethod def get_relationships( - cls, taxonomy: Taxonomy, rel_type: RelType, org_short_name: str = None + cls, taxonomy: Taxonomy, rel_type: RelType, org_short_name: Union[str, None] = None ) -> QuerySet: """ Returns the relationships of the given rel_type and taxonomy where: diff --git a/openedx/features/content_tagging/rest_api/__init__.py b/openedx/features/content_tagging/rest_api/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/openedx/features/content_tagging/rest_api/urls.py b/openedx/features/content_tagging/rest_api/urls.py new file mode 100644 index 000000000000..d7f012bb7ba1 --- /dev/null +++ b/openedx/features/content_tagging/rest_api/urls.py @@ -0,0 +1,9 @@ +""" +Taxonomies API URLs. +""" + +from django.urls import path, include + +from .v1 import urls as v1_urls + +urlpatterns = [path("v1/", include(v1_urls))] diff --git a/openedx/features/content_tagging/rest_api/v1/__init__.py b/openedx/features/content_tagging/rest_api/v1/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/openedx/features/content_tagging/rest_api/v1/filters.py b/openedx/features/content_tagging/rest_api/v1/filters.py new file mode 100644 index 000000000000..ee8771d17ee8 --- /dev/null +++ b/openedx/features/content_tagging/rest_api/v1/filters.py @@ -0,0 +1,21 @@ +""" +API Filters for content tagging org +""" + +from rest_framework.filters import BaseFilterBackend + +from ...rules import is_taxonomy_admin + + +class UserOrgFilterBackend(BaseFilterBackend): + """ + Taxonomy admin can see all taxonomies + Everyone else can see only enabled taxonomies + + """ + + def filter_queryset(self, request, queryset, _): + if is_taxonomy_admin(request.user): + return queryset + + return queryset.filter(enabled=True) diff --git a/openedx/features/content_tagging/rest_api/v1/serializers.py b/openedx/features/content_tagging/rest_api/v1/serializers.py new file mode 100644 index 000000000000..1ef9f531b70e --- /dev/null +++ b/openedx/features/content_tagging/rest_api/v1/serializers.py @@ -0,0 +1,35 @@ +""" +API Serializers for content tagging org +""" + +from rest_framework import serializers + +from openedx_tagging.core.tagging.rest_api.v1.serializers import ( + TaxonomyListQueryParamsSerializer, +) + +from organizations.models import Organization + + +class OrganizationField(serializers.Field): + """ + Custom field for organization + """ + def to_representation(self, value): + return value.short_name + + def to_internal_value(self, data): + try: + return Organization.objects.get(short_name=data) + except Organization.DoesNotExist as exc: + raise serializers.ValidationError( + "Invalid organization short name" + ) from exc + + +class TaxonomyOrgListQueryParamsSerializer(TaxonomyListQueryParamsSerializer): + """ + Serializer for the query params for the GET view + """ + + org = OrganizationField(required=False) diff --git a/openedx/features/content_tagging/rest_api/v1/tests/__init__.py b/openedx/features/content_tagging/rest_api/v1/tests/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/openedx/features/content_tagging/rest_api/v1/tests/test_views.py b/openedx/features/content_tagging/rest_api/v1/tests/test_views.py new file mode 100644 index 000000000000..a02d046d2e1b --- /dev/null +++ b/openedx/features/content_tagging/rest_api/v1/tests/test_views.py @@ -0,0 +1,637 @@ +""" +Tests tagging rest api views +""" + +from urllib.parse import parse_qs, urlparse + +import ddt +from django.contrib.auth import get_user_model +from django.test.testcases import override_settings +from openedx_tagging.core.tagging.models import Taxonomy +from openedx_tagging.core.tagging.models.system_defined import SystemDefinedTaxonomy +from openedx_tagging.core.tagging.rest_api.v1.serializers import TaxonomySerializer +from organizations.models import Organization +from rest_framework import status +from rest_framework.test import APITestCase + +from common.djangoapps.student.auth import update_org_role +from common.djangoapps.student.roles import OrgContentCreatorRole +from openedx.features.content_tagging.models import TaxonomyOrg + +User = get_user_model() + +TAXONOMY_ORG_LIST_URL = "/api/content_tagging/v1/taxonomies/" +TAXONOMY_ORG_DETAIL_URL = "/api/content_tagging/v1/taxonomies/{pk}/" + + +def check_taxonomy( + data, + pk, + name, + description=None, + enabled=True, + required=False, + allow_multiple=False, + allow_free_text=False, + system_defined=False, + visible_to_authors=True, + **_ +): + """ + Check the given data against the expected values. + """ + assert data["id"] == pk + assert data["name"] == name + assert data["description"] == description + assert data["enabled"] == enabled + assert data["required"] == required + assert data["allow_multiple"] == allow_multiple + assert data["allow_free_text"] == allow_free_text + assert data["system_defined"] == system_defined + assert data["visible_to_authors"] == visible_to_authors + + +class TestTaxonomyViewSetMixin: + """ + Sets up data for testing Content Taxonomies. + """ + + def setUp(self): + super().setUp() + self.user = User.objects.create( + username="user", + email="user@example.com", + ) + self.userS = User.objects.create( + username="staff", + email="staff@example.com", + is_staff=True, + ) + + self.orgA = Organization.objects.create(name="Organization A", short_name="orgA") + self.orgB = Organization.objects.create(name="Organization B", short_name="orgB") + self.orgX = Organization.objects.create(name="Organization X", short_name="orgX") + + self.userA = User.objects.create( + username="userA", + email="userA@example.com", + ) + update_org_role(self.userS, OrgContentCreatorRole, self.userA, [self.orgA.short_name]) + + # Orphaned taxonomy + self.ot1 = Taxonomy.objects.create(name="ot1", enabled=True) + self.ot2 = Taxonomy.objects.create(name="ot2", enabled=False) + + # System defined taxonomy + self.st1 = Taxonomy.objects.create(name="st1", enabled=True) + self.st1.taxonomy_class = SystemDefinedTaxonomy + self.st1.save() + TaxonomyOrg.objects.create( + taxonomy=self.st1, + rel_type=TaxonomyOrg.RelType.OWNER, + org=None, + ) + self.st2 = Taxonomy.objects.create(name="st2", enabled=False) + self.st2.taxonomy_class = SystemDefinedTaxonomy + self.st2.save() + TaxonomyOrg.objects.create( + taxonomy=self.st2, + rel_type=TaxonomyOrg.RelType.OWNER, + ) + + # Global taxonomy + self.t1 = Taxonomy.objects.create(name="t1", enabled=True) + TaxonomyOrg.objects.create( + taxonomy=self.t1, + rel_type=TaxonomyOrg.RelType.OWNER, + ) + self.t2 = Taxonomy.objects.create(name="t2", enabled=False) + TaxonomyOrg.objects.create( + taxonomy=self.t2, + rel_type=TaxonomyOrg.RelType.OWNER, + ) + + # OrgA taxonomy + self.tA1 = Taxonomy.objects.create(name="tA1", enabled=True) + TaxonomyOrg.objects.create( + taxonomy=self.tA1, + org=self.orgA, + rel_type=TaxonomyOrg.RelType.OWNER, + ) + self.tA2 = Taxonomy.objects.create(name="tA2", enabled=False) + TaxonomyOrg.objects.create( + taxonomy=self.tA2, + org=self.orgA, + rel_type=TaxonomyOrg.RelType.OWNER, + ) + + # OrgB taxonomy + self.tB1 = Taxonomy.objects.create(name="tB1", enabled=True) + TaxonomyOrg.objects.create( + taxonomy=self.tB1, + org=self.orgB, + rel_type=TaxonomyOrg.RelType.OWNER, + ) + self.tB2 = Taxonomy.objects.create(name="tB2", enabled=False) + TaxonomyOrg.objects.create( + taxonomy=self.tB2, + org=self.orgB, + rel_type=TaxonomyOrg.RelType.OWNER, + ) + + # OrgA and OrgB taxonomy + self.tC1 = Taxonomy.objects.create(name="tC1", enabled=True) + TaxonomyOrg.objects.create( + taxonomy=self.tC1, + org=self.orgA, + rel_type=TaxonomyOrg.RelType.OWNER, + ) + TaxonomyOrg.objects.create( + taxonomy=self.tC1, + org=self.orgB, + rel_type=TaxonomyOrg.RelType.OWNER, + ) + self.tC2 = Taxonomy.objects.create(name="tC2", enabled=False) + TaxonomyOrg.objects.create( + taxonomy=self.tC2, + org=self.orgA, + rel_type=TaxonomyOrg.RelType.OWNER, + ) + TaxonomyOrg.objects.create( + taxonomy=self.tC2, + org=self.orgB, + rel_type=TaxonomyOrg.RelType.OWNER, + ) + + +@ddt.ddt +@override_settings(FEATURES={"ENABLE_CREATOR_GROUP": True}) +class TestTaxonomyViewSet(TestTaxonomyViewSetMixin, APITestCase): + """ + Test cases for TaxonomyViewSet when ENABLE_CREATOR_GROUP is True + """ + + @ddt.data( + ("user", None, None, ("ot1", "st1", "t1", "tA1", "tB1", "tC1")), + ("userA", None, None, ("ot1", "st1", "t1", "tA1", "tB1", "tC1")), + ("userS", None, None, ("ot1", "ot2", "st1", "st2", "t1", "t2", "tA1", "tA2", "tB1", "tB2", "tC1", "tC2")), + ("user", True, None, ("ot1", "st1", "t1", "tA1", "tB1", "tC1")), + ("userA", True, None, ("ot1", "st1", "t1", "tA1", "tB1", "tC1")), + ("userS", True, None, ("ot1", "st1", "t1", "tA1", "tB1", "tC1")), + ("user", False, None, ()), + ("userA", False, None, ()), + ("userS", False, None, ("ot2", "st2", "t2", "tA2", "tB2", "tC2")), + ("user", None, "orgA", ("st1", "t1", "tA1", "tC1")), + ("userA", None, "orgA", ("st1", "t1", "tA1", "tC1")), + ("userS", None, "orgA", ("st1", "st2", "t1", "t2", "tA1", "tA2", "tC1", "tC2")), + ("user", True, "orgA", ("st1", "t1", "tA1", "tC1")), + ("userA", True, "orgA", ("st1", "t1", "tA1", "tC1")), + ("userS", True, "orgA", ("st1", "t1", "tA1", "tC1")), + ("user", False, "orgA", ()), + ("userA", False, "orgA", ()), + ("userS", False, "orgA", ("st2", "t2", "tA2", "tC2")), + ("user", None, "orgX", ("st1", "t1")), + ("userA", None, "orgX", ("st1", "t1")), + ("userS", None, "orgX", ("st1", "st2", "t1", "t2")), + ("user", True, "orgX", ("st1", "t1")), + ("userA", True, "orgX", ("st1", "t1")), + ("userS", True, "orgX", ("st1", "t1")), + ("user", False, "orgX", ()), + ("userA", False, "orgX", ()), + ("userS", False, "orgX", ("st2", "t2")), + ) + @ddt.unpack + def test_list_taxonomy(self, user_attr, enabled_parameter, org_name, expected_taxonomies): + url = TAXONOMY_ORG_LIST_URL + + if user_attr: + user = getattr(self, user_attr) + self.client.force_authenticate(user=user) + + # Set parameters cleaning empty values + query_params = {k: v for k, v in {"enabled": enabled_parameter, "org": org_name}.items() if v is not None} + + response = self.client.get(url, query_params, format="json") + + assert response.status_code == status.HTTP_200_OK + self.assertEqual(set(t["name"] for t in response.data["results"]), set(expected_taxonomies)) + + def test_list_taxonomy_invalid_org( + self, + ): + url = TAXONOMY_ORG_LIST_URL + + self.client.force_authenticate(user=self.userS) + + # Set parameters cleaning empty values + query_params = {"org": "invalidOrg"} + + response = self.client.get(url, query_params, format="json") + + assert response.status_code == status.HTTP_400_BAD_REQUEST + + @ddt.data( + ("user", ("tA1", "tB1", "tC1"), None), + ("userA", ("tA1", "tB1", "tC1"), None), + ("userS", ("st2", "t1", "t2"), "3"), + ) + @ddt.unpack + def test_list_taxonomy_pagination(self, user_attr, expected_taxonomies, expected_next_page): + url = TAXONOMY_ORG_LIST_URL + + if user_attr: + user = getattr(self, user_attr) + self.client.force_authenticate(user=user) + + query_params = {"page_size": 3, "page": 2} + + response = self.client.get(url, query_params, format="json") + + assert response.status_code == status.HTTP_200_OK + self.assertEqual(set(t["name"] for t in response.data["results"]), set(expected_taxonomies)) + parsed_url = urlparse(response.data["next"]) + + next_page = parse_qs(parsed_url.query).get("page", [None])[0] + assert next_page == expected_next_page + + def test_list_invalid_page(self): + url = TAXONOMY_ORG_LIST_URL + + self.client.force_authenticate(user=self.user) + + query_params = {"page": 123123} + + response = self.client.get(url, query_params, format="json") + + assert response.status_code == status.HTTP_404_NOT_FOUND + + @ddt.data( + (None, "ot1", status.HTTP_403_FORBIDDEN), + (None, "ot2", status.HTTP_403_FORBIDDEN), + (None, "st1", status.HTTP_403_FORBIDDEN), + (None, "st2", status.HTTP_403_FORBIDDEN), + (None, "t1", status.HTTP_403_FORBIDDEN), + (None, "t2", status.HTTP_403_FORBIDDEN), + (None, "tA1", status.HTTP_403_FORBIDDEN), + (None, "tA2", status.HTTP_403_FORBIDDEN), + (None, "tB1", status.HTTP_403_FORBIDDEN), + (None, "tB2", status.HTTP_403_FORBIDDEN), + (None, "tC1", status.HTTP_403_FORBIDDEN), + (None, "tC2", status.HTTP_403_FORBIDDEN), + ("user", "ot1", status.HTTP_200_OK), + ("user", "ot2", status.HTTP_404_NOT_FOUND), + ("user", "st1", status.HTTP_200_OK), + ("user", "st2", status.HTTP_404_NOT_FOUND), + ("user", "t1", status.HTTP_200_OK), + ("user", "t2", status.HTTP_404_NOT_FOUND), + ("user", "tA1", status.HTTP_200_OK), + ("user", "tA2", status.HTTP_404_NOT_FOUND), + ("user", "tB1", status.HTTP_200_OK), + ("user", "tB2", status.HTTP_404_NOT_FOUND), + ("user", "tC1", status.HTTP_200_OK), + ("user", "tC2", status.HTTP_404_NOT_FOUND), + ("userA", "ot1", status.HTTP_200_OK), + ("userA", "ot2", status.HTTP_404_NOT_FOUND), + ("userA", "st1", status.HTTP_200_OK), + ("userA", "st2", status.HTTP_404_NOT_FOUND), + ("userA", "t1", status.HTTP_200_OK), + ("userA", "t2", status.HTTP_404_NOT_FOUND), + ("userA", "tA1", status.HTTP_200_OK), + ("userA", "tA2", status.HTTP_404_NOT_FOUND), + ("userA", "tB1", status.HTTP_200_OK), + ("userA", "tB2", status.HTTP_404_NOT_FOUND), + ("userA", "tC1", status.HTTP_200_OK), + ("userA", "tC2", status.HTTP_404_NOT_FOUND), + ("userS", "ot1", status.HTTP_200_OK), + ("userS", "ot2", status.HTTP_200_OK), + ("userS", "st1", status.HTTP_200_OK), + ("userS", "st2", status.HTTP_200_OK), + ("userS", "t1", status.HTTP_200_OK), + ("userS", "t2", status.HTTP_200_OK), + ("userS", "tA1", status.HTTP_200_OK), + ("userS", "tA2", status.HTTP_200_OK), + ("userS", "tB1", status.HTTP_200_OK), + ("userS", "tB2", status.HTTP_200_OK), + ("userS", "tC1", status.HTTP_200_OK), + ("userS", "tC2", status.HTTP_200_OK), + ) + @ddt.unpack + def test_detail_taxonomy(self, user_attr, taxonomy_attr, expected_status): + taxonomy = getattr(self, taxonomy_attr) + + url = TAXONOMY_ORG_DETAIL_URL.format(pk=taxonomy.pk) + + if user_attr: + user = getattr(self, user_attr) + self.client.force_authenticate(user=user) + + response = self.client.get(url) + assert response.status_code == expected_status + + if status.is_success(expected_status): + check_taxonomy(response.data, taxonomy.pk, **(TaxonomySerializer(taxonomy.cast()).data)) + + @ddt.data( + (None, status.HTTP_403_FORBIDDEN), + ("user", status.HTTP_403_FORBIDDEN), + ("userA", status.HTTP_403_FORBIDDEN), + ("userS", status.HTTP_201_CREATED), + ) + @ddt.unpack + def test_create_taxonomy(self, user_attr, expected_status): + url = TAXONOMY_ORG_LIST_URL + + create_data = { + "name": "taxonomy_data", + "description": "This is a description", + "enabled": True, + "required": True, + "allow_multiple": True, + } + + if user_attr: + user = getattr(self, user_attr) + self.client.force_authenticate(user=user) + + response = self.client.post(url, create_data, format="json") + assert response.status_code == expected_status + + # If we were able to create the taxonomy, check if it was created + if status.is_success(expected_status): + check_taxonomy(response.data, response.data["id"], **create_data) + url = TAXONOMY_ORG_DETAIL_URL.format(pk=response.data["id"]) + + response = self.client.get(url) + check_taxonomy(response.data, response.data["id"], **create_data) + + @ddt.data( + (None, "ot1", status.HTTP_403_FORBIDDEN), + (None, "ot2", status.HTTP_403_FORBIDDEN), + (None, "st1", status.HTTP_403_FORBIDDEN), + (None, "st2", status.HTTP_403_FORBIDDEN), + (None, "t1", status.HTTP_403_FORBIDDEN), + (None, "t2", status.HTTP_403_FORBIDDEN), + (None, "tA1", status.HTTP_403_FORBIDDEN), + (None, "tA2", status.HTTP_403_FORBIDDEN), + (None, "tB1", status.HTTP_403_FORBIDDEN), + (None, "tB2", status.HTTP_403_FORBIDDEN), + (None, "tC1", status.HTTP_403_FORBIDDEN), + (None, "tC2", status.HTTP_403_FORBIDDEN), + ("user", "ot1", status.HTTP_403_FORBIDDEN), + ("user", "ot2", status.HTTP_403_FORBIDDEN), + ("user", "st1", status.HTTP_403_FORBIDDEN), + ("user", "st2", status.HTTP_403_FORBIDDEN), + ("user", "t1", status.HTTP_403_FORBIDDEN), + ("user", "t2", status.HTTP_403_FORBIDDEN), + ("user", "tA1", status.HTTP_403_FORBIDDEN), + ("user", "tA2", status.HTTP_403_FORBIDDEN), + ("user", "tB1", status.HTTP_403_FORBIDDEN), + ("user", "tB2", status.HTTP_403_FORBIDDEN), + ("user", "tC1", status.HTTP_403_FORBIDDEN), + ("user", "tC2", status.HTTP_403_FORBIDDEN), + ("userA", "ot1", status.HTTP_403_FORBIDDEN), + ("userA", "ot2", status.HTTP_403_FORBIDDEN), + ("userA", "st1", status.HTTP_403_FORBIDDEN), + ("userA", "st2", status.HTTP_403_FORBIDDEN), + ("userA", "t1", status.HTTP_403_FORBIDDEN), + ("userA", "t2", status.HTTP_403_FORBIDDEN), + ("userA", "tA1", status.HTTP_403_FORBIDDEN), + ("userA", "tA2", status.HTTP_403_FORBIDDEN), + ("userA", "tB1", status.HTTP_403_FORBIDDEN), + ("userA", "tB2", status.HTTP_403_FORBIDDEN), + ("userA", "tC1", status.HTTP_403_FORBIDDEN), + ("userA", "tC2", status.HTTP_403_FORBIDDEN), + ("userS", "ot1", status.HTTP_200_OK), + ("userS", "ot2", status.HTTP_200_OK), + ("userS", "st1", status.HTTP_403_FORBIDDEN), + ("userS", "st2", status.HTTP_403_FORBIDDEN), + ("userS", "t1", status.HTTP_200_OK), + ("userS", "t2", status.HTTP_200_OK), + ("userS", "t1", status.HTTP_200_OK), + ("userS", "t2", status.HTTP_200_OK), + ("userS", "tA1", status.HTTP_200_OK), + ("userS", "tA2", status.HTTP_200_OK), + ("userS", "tB1", status.HTTP_200_OK), + ("userS", "tB2", status.HTTP_200_OK), + ("userS", "tC1", status.HTTP_200_OK), + ("userS", "tC2", status.HTTP_200_OK), + ) + @ddt.unpack + def test_update_taxonomy(self, user_attr, taxonomy_attr, expected_status): + taxonomy = getattr(self, taxonomy_attr) + + url = TAXONOMY_ORG_DETAIL_URL.format(pk=taxonomy.pk) + + if user_attr: + user = getattr(self, user_attr) + self.client.force_authenticate(user=user) + + response = self.client.put(url, {"name": "new name"}, format="json") + assert response.status_code == expected_status + + # If we were able to update the taxonomy, check if the name changed + if status.is_success(expected_status): + response = self.client.get(url) + check_taxonomy( + response.data, + response.data["id"], + **{ + "name": "new name", + "description": taxonomy.description, + "enabled": taxonomy.enabled, + "required": taxonomy.required, + }, + ) + + @ddt.data( + (False, status.HTTP_403_FORBIDDEN), + (True, status.HTTP_403_FORBIDDEN), + ) + @ddt.unpack + def test_update_taxonomy_system_defined(self, update_value, expected_status): + """ + Test that we can't update system_defined field + """ + url = TAXONOMY_ORG_DETAIL_URL.format(pk=self.st1.pk) + + self.client.force_authenticate(user=self.userS) + response = self.client.put(url, {"name": "new name", "system_defined": update_value}, format="json") + assert response.status_code == expected_status + + # Verify that system_defined has not changed + response = self.client.get(url) + assert response.data["system_defined"] is True + + @ddt.data( + (None, "ot1", status.HTTP_403_FORBIDDEN), + (None, "ot2", status.HTTP_403_FORBIDDEN), + (None, "st1", status.HTTP_403_FORBIDDEN), + (None, "st2", status.HTTP_403_FORBIDDEN), + (None, "t1", status.HTTP_403_FORBIDDEN), + (None, "t2", status.HTTP_403_FORBIDDEN), + (None, "tA1", status.HTTP_403_FORBIDDEN), + (None, "tA2", status.HTTP_403_FORBIDDEN), + (None, "tB1", status.HTTP_403_FORBIDDEN), + (None, "tB2", status.HTTP_403_FORBIDDEN), + (None, "tC1", status.HTTP_403_FORBIDDEN), + (None, "tC2", status.HTTP_403_FORBIDDEN), + ("user", "ot1", status.HTTP_403_FORBIDDEN), + ("user", "ot2", status.HTTP_403_FORBIDDEN), + ("user", "st1", status.HTTP_403_FORBIDDEN), + ("user", "st2", status.HTTP_403_FORBIDDEN), + ("user", "t1", status.HTTP_403_FORBIDDEN), + ("user", "t2", status.HTTP_403_FORBIDDEN), + ("user", "tA1", status.HTTP_403_FORBIDDEN), + ("user", "tA2", status.HTTP_403_FORBIDDEN), + ("user", "tB1", status.HTTP_403_FORBIDDEN), + ("user", "tB2", status.HTTP_403_FORBIDDEN), + ("user", "tC1", status.HTTP_403_FORBIDDEN), + ("user", "tC2", status.HTTP_403_FORBIDDEN), + ("userA", "ot1", status.HTTP_403_FORBIDDEN), + ("userA", "ot2", status.HTTP_403_FORBIDDEN), + ("userA", "st1", status.HTTP_403_FORBIDDEN), + ("userA", "st2", status.HTTP_403_FORBIDDEN), + ("userA", "t1", status.HTTP_403_FORBIDDEN), + ("userA", "t2", status.HTTP_403_FORBIDDEN), + ("userA", "tA1", status.HTTP_403_FORBIDDEN), + ("userA", "tA2", status.HTTP_403_FORBIDDEN), + ("userA", "tB1", status.HTTP_403_FORBIDDEN), + ("userA", "tB2", status.HTTP_403_FORBIDDEN), + ("userA", "tC1", status.HTTP_403_FORBIDDEN), + ("userA", "tC2", status.HTTP_403_FORBIDDEN), + ("userS", "ot1", status.HTTP_200_OK), + ("userS", "ot2", status.HTTP_200_OK), + ("userS", "st1", status.HTTP_403_FORBIDDEN), + ("userS", "st2", status.HTTP_403_FORBIDDEN), + ("userS", "t1", status.HTTP_200_OK), + ("userS", "t2", status.HTTP_200_OK), + ("userS", "tA1", status.HTTP_200_OK), + ("userS", "tA2", status.HTTP_200_OK), + ("userS", "tB1", status.HTTP_200_OK), + ("userS", "tB2", status.HTTP_200_OK), + ("userS", "tC1", status.HTTP_200_OK), + ("userS", "tC2", status.HTTP_200_OK), + ) + @ddt.unpack + def test_patch_taxonomy(self, user_attr, taxonomy_attr, expected_status): + taxonomy = getattr(self, taxonomy_attr) + + url = TAXONOMY_ORG_DETAIL_URL.format(pk=taxonomy.pk) + + if user_attr: + user = getattr(self, user_attr) + self.client.force_authenticate(user=user) + + response = self.client.patch(url, {"name": "new name"}, format="json") + assert response.status_code == expected_status + + # If we were able to patch the taxonomy, check if the name changed + if status.is_success(expected_status): + response = self.client.get(url) + check_taxonomy( + response.data, + response.data["id"], + **{ + "name": "new name", + "description": taxonomy.description, + "enabled": taxonomy.enabled, + "required": taxonomy.required, + }, + ) + + @ddt.data( + (False, status.HTTP_403_FORBIDDEN), + (True, status.HTTP_403_FORBIDDEN), + ) + @ddt.unpack + def test_patch_taxonomy_system_defined(self, update_value, expected_status): + """ + Test that we can't patch system_defined field + """ + url = TAXONOMY_ORG_DETAIL_URL.format(pk=self.st1.pk) + + self.client.force_authenticate(user=self.userS) + response = self.client.patch(url, {"name": "new name", "system_defined": update_value}, format="json") + assert response.status_code == expected_status + + # Verify that system_defined has not changed + response = self.client.get(url) + assert response.data["system_defined"] is True + + @ddt.data( + (None, "ot1", status.HTTP_403_FORBIDDEN), + (None, "ot2", status.HTTP_403_FORBIDDEN), + (None, "st1", status.HTTP_403_FORBIDDEN), + (None, "st2", status.HTTP_403_FORBIDDEN), + (None, "t1", status.HTTP_403_FORBIDDEN), + (None, "t2", status.HTTP_403_FORBIDDEN), + (None, "tA1", status.HTTP_403_FORBIDDEN), + (None, "tA2", status.HTTP_403_FORBIDDEN), + (None, "tB1", status.HTTP_403_FORBIDDEN), + (None, "tB2", status.HTTP_403_FORBIDDEN), + (None, "tC1", status.HTTP_403_FORBIDDEN), + (None, "tC2", status.HTTP_403_FORBIDDEN), + ("user", "ot1", status.HTTP_403_FORBIDDEN), + ("user", "ot2", status.HTTP_403_FORBIDDEN), + ("user", "st1", status.HTTP_403_FORBIDDEN), + ("user", "st2", status.HTTP_403_FORBIDDEN), + ("user", "t1", status.HTTP_403_FORBIDDEN), + ("user", "t2", status.HTTP_403_FORBIDDEN), + ("user", "tA1", status.HTTP_403_FORBIDDEN), + ("user", "tA2", status.HTTP_403_FORBIDDEN), + ("user", "tB1", status.HTTP_403_FORBIDDEN), + ("user", "tB2", status.HTTP_403_FORBIDDEN), + ("user", "tC1", status.HTTP_403_FORBIDDEN), + ("user", "tC2", status.HTTP_403_FORBIDDEN), + ("userA", "ot1", status.HTTP_403_FORBIDDEN), + ("userA", "ot2", status.HTTP_403_FORBIDDEN), + ("userA", "st1", status.HTTP_403_FORBIDDEN), + ("userA", "st2", status.HTTP_403_FORBIDDEN), + ("userA", "t1", status.HTTP_403_FORBIDDEN), + ("userA", "t2", status.HTTP_403_FORBIDDEN), + ("userA", "tA1", status.HTTP_403_FORBIDDEN), + ("userA", "tA2", status.HTTP_403_FORBIDDEN), + ("userA", "tB1", status.HTTP_403_FORBIDDEN), + ("userA", "tB2", status.HTTP_403_FORBIDDEN), + ("userA", "tC1", status.HTTP_403_FORBIDDEN), + ("userA", "tC2", status.HTTP_403_FORBIDDEN), + ("userS", "ot1", status.HTTP_204_NO_CONTENT), + ("userS", "ot2", status.HTTP_204_NO_CONTENT), + ("userS", "st1", status.HTTP_403_FORBIDDEN), + ("userS", "st2", status.HTTP_403_FORBIDDEN), + ("userS", "t1", status.HTTP_204_NO_CONTENT), + ("userS", "t2", status.HTTP_204_NO_CONTENT), + ("userS", "tA1", status.HTTP_204_NO_CONTENT), + ("userS", "tA2", status.HTTP_204_NO_CONTENT), + ("userS", "tB1", status.HTTP_204_NO_CONTENT), + ("userS", "tB2", status.HTTP_204_NO_CONTENT), + ("userS", "tC1", status.HTTP_204_NO_CONTENT), + ("userS", "tC2", status.HTTP_204_NO_CONTENT), + ) + @ddt.unpack + def test_delete_taxonomy(self, user_attr, taxonomy_attr, expected_status): + taxonomy = getattr(self, taxonomy_attr) + + url = TAXONOMY_ORG_DETAIL_URL.format(pk=taxonomy.pk) + + if user_attr: + user = getattr(self, user_attr) + self.client.force_authenticate(user=user) + + response = self.client.delete(url) + assert response.status_code == expected_status + + # If we were able to delete the taxonomy, check that it's really gone + if status.is_success(expected_status): + response = self.client.get(url) + assert response.status_code == status.HTTP_404_NOT_FOUND + + +@ddt.ddt +@override_settings(FEATURES={"ENABLE_CREATOR_GROUP": False}) +class TestTaxonomyViewSetNoCreatorGroup(TestTaxonomyViewSet): # pylint: disable=test-inherits-tests + """ + Test cases for TaxonomyViewSet when ENABLE_CREATOR_GROUP is False + + The permissions are the same for when ENABLED_CREATOR_GRUP is True + """ diff --git a/openedx/features/content_tagging/rest_api/v1/urls.py b/openedx/features/content_tagging/rest_api/v1/urls.py new file mode 100644 index 000000000000..a72375bab50c --- /dev/null +++ b/openedx/features/content_tagging/rest_api/v1/urls.py @@ -0,0 +1,16 @@ +""" +Taxonomies API v1 URLs. +""" + +from rest_framework.routers import DefaultRouter + +from django.urls.conf import path, include + +from . import views + +router = DefaultRouter() +router.register("taxonomies", views.TaxonomyOrgView, basename="taxonomy") + +urlpatterns = [ + path('', include(router.urls)) +] diff --git a/openedx/features/content_tagging/rest_api/v1/views.py b/openedx/features/content_tagging/rest_api/v1/views.py new file mode 100644 index 000000000000..d32dbab6a0b2 --- /dev/null +++ b/openedx/features/content_tagging/rest_api/v1/views.py @@ -0,0 +1,153 @@ +""" +Tagging Org API Views +""" + +from edx_rest_framework_extensions.paginators import DefaultPagination +from openedx_tagging.core.tagging.rest_api.v1.views import TaxonomyView + + +from ...api import ( + create_taxonomy, + get_taxonomies, + get_taxonomies_for_org, +) +from .serializers import TaxonomyOrgListQueryParamsSerializer +from .filters import UserOrgFilterBackend + + +class TaxonomyOrgView(TaxonomyView): + """ + View to list, create, retrieve, update, or delete Taxonomies. + + **List Query Parameters** + * enabled (optional) - Filter by enabled status. Valid values: true, false, 1, 0, "true", "false", "1" + * org (optional) - Filter by organization. + * page (optional) - Page number (default: 1) + * page_size (optional) - Number of items per page (default: 100) + + **List Example Requests** + GET api/tagging/v1/taxonomy - Get all taxonomies + GET api/tagging/v1/taxonomy?enabled=true - Get all enabled taxonomies + GET api/tagging/v1/taxonomy?enabled=false - Get all disabled taxonomies + GET api/tagging/v1/taxonomy?org=A - Get all taxonomies for organization A + GET api/tagging/v1/taxonomy?org=A&enabled=true - Get all enabled taxonomies for organization A + + **List Query Returns** + * 200 - Success + * 400 - Invalid query parameter + * 403 - Permission denied + + **Retrieve Parameters** + * id (required): - The id of the taxonomy to retrieve + + **Retrieve Example Requests** + GET api/tagging/v1/taxonomy/:id - Get a specific taxonomy + + **Retrieve Query Returns** + * 200 - Success + * 404 - Taxonomy not found or User does not have permission to access the taxonomy + + **Create Parameters** + * name (required): User-facing label used when applying tags from this taxonomy to Open edX objects. + * description (optional): Provides extra information for the user when applying tags from this taxonomy + to an object. + * enabled (optional): Only enabled taxonomies will be shown to authors (default: true). + * required (optional): Indicates that one or more tags from this taxonomy must be added to an + object (default: False). + * allow_multiple (optional): Indicates that multiple tags from this taxonomy may be added to + an object (default: False). + * allow_free_text (optional): Indicates that tags in this taxonomy need not be predefined; authors + may enter their own tag values (default: False). + + **Create Example Requests** + POST api/tagging/v1/taxonomy - Create a taxonomy + { + "name": "Taxonomy Name", - User-facing label used when applying tags from + this taxonomy to Open edX objects." + "description": "This is a description", + "enabled": True, + "required": True, + "allow_multiple": True, + "allow_free_text": True, + } + + + **Create Query Returns** + * 201 - Success + * 403 - Permission denied + + **Update Parameters** + * id (required): - The id of the taxonomy to update + + **Update Request Body** + * name (optional): User-facing label used when applying tags from this taxonomy to Open edX objects. + * description (optional): Provides extra information for the user when applying tags from this taxonomy + to an object. + * enabled (optional): Only enabled taxonomies will be shown to authors. + * required (optional): Indicates that one or more tags from this taxonomy must be added to an object. + * allow_multiple (optional): Indicates that multiple tags from this taxonomy may be added to an object. + * allow_free_text (optional): Indicates that tags in this taxonomy need not be predefined; authors may + enter their own tag values. + + **Update Example Requests** + PUT api/tagging/v1/taxonomy/:id - Update a taxonomy + { + "name": "Taxonomy New Name", + "description": "This is a new description", + "enabled": False, + "required": False, + "allow_multiple": False, + "allow_free_text": True, + } + PATCH api/tagging/v1/taxonomy/:id - Partially update a taxonomy + { + "name": "Taxonomy New Name", + } + + **Update Query Returns** + * 200 - Success + * 403 - Permission denied + + **Delete Parameters** + * id (required): - The id of the taxonomy to delete + + **Delete Example Requests** + DELETE api/tagging/v1/taxonomy/:id - Delete a taxonomy + + **Delete Query Returns** + * 200 - Success + * 404 - Taxonomy not found + * 403 - Permission denied + + """ + + class TaxonomyPagination(DefaultPagination): + page_size = 100 + max_page_size = 1000 + + pagination_class = TaxonomyPagination + + filter_backends = [UserOrgFilterBackend] + + def get_queryset(self): + """ + Return a list of taxonomies. + + Returns all taxonomies by default. + If you want the disabled taxonomies, pass enabled=False. + If you want the enabled taxonomies, pass enabled=True. + """ + query_params = TaxonomyOrgListQueryParamsSerializer(data=self.request.query_params.dict()) + query_params.is_valid(raise_exception=True) + enabled = query_params.validated_data.get("enabled", None) + org = query_params.validated_data.get("org", None) + if org: + return get_taxonomies_for_org(enabled, org) + else: + return get_taxonomies(enabled) + + def perform_create(self, serializer): + """ + Create a new taxonomy. + """ + serializer.instance = create_taxonomy(**serializer.validated_data) diff --git a/openedx/features/content_tagging/rules.py b/openedx/features/content_tagging/rules.py index 9ab7072bf96e..f0cd38b5860c 100644 --- a/openedx/features/content_tagging/rules.py +++ b/openedx/features/content_tagging/rules.py @@ -11,14 +11,16 @@ User = get_user_model() -def is_taxonomy_admin(user: User, taxonomy: oel_tagging.Taxonomy = None) -> bool: +def is_taxonomy_user(user: User, taxonomy: oel_tagging.Taxonomy = None) -> bool: """ - Returns True if the given user is a Taxonomy Admin for the given content taxonomy. + Returns True if the given user is a Taxonomy User for the given content taxonomy. - Global Taxonomy Admins include global staff and superusers, plus course creators who can create courses for any org. - Otherwise, a taxonomy must be provided to determine if the user is a org-level course creator for one of the - taxonomy's org owners. + Taxonomy users include global staff and superusers, plus course creators who can create courses for any org. + Otherwise, we need a taxonomy provided to determine if the user is an org-level course creator for one of the + orgs allowed to use this taxonomy. """ + # ToDo: This is a temporary fix to not break can_change_taxonomy_tag and can_change_object_tag + # Should be revised when the API that uses these roles is implemented if oel_tagging.is_taxonomy_admin(user): return True @@ -35,27 +37,51 @@ def is_taxonomy_admin(user: User, taxonomy: oel_tagging.Taxonomy = None) -> bool return False +def is_taxonomy_admin(user: User) -> bool: + """ + Returns True if the given user is a Taxonomy Admin. + + Taxonomy Admins include global staff and superusers. + """ + return oel_tagging.is_taxonomy_admin(user) + + @rules.predicate def can_view_taxonomy(user: User, taxonomy: oel_tagging.Taxonomy = None) -> bool: """ - Anyone can view an enabled taxonomy, - but only taxonomy admins can view a disabled taxonomy. + Everyone can potentially view a taxonomy (taxonomy=None). The object permission must be checked + to determine if the user can view a specific taxonomy. + Only taxonomy admins can view a disabled taxonomy. """ - if taxonomy: - taxonomy = taxonomy.cast() - return (taxonomy and taxonomy.enabled) or is_taxonomy_admin(user, taxonomy) + if not taxonomy: + return True + + taxonomy = taxonomy.cast() + + return taxonomy.enabled or is_taxonomy_admin(user) + + +@rules.predicate +def can_add_taxonomy(user: User) -> bool: + """ + Only taxonomy admins can add taxonomies. + + To do: There is currently no REST API method to create a taxonomy associated with an organization + neither to add a taxonomy to an organization. + """ + return is_taxonomy_admin(user) @rules.predicate def can_change_taxonomy(user: User, taxonomy: oel_tagging.Taxonomy = None) -> bool: """ + Only taxonomy admins can change a taxonomies. Even taxonomy admins cannot change system taxonomies. """ if taxonomy: taxonomy = taxonomy.cast() - return is_taxonomy_admin(user, taxonomy) and ( - not taxonomy or (taxonomy and not taxonomy.system_defined) - ) + + return (not taxonomy or (not taxonomy.system_defined)) and is_taxonomy_admin(user) @rules.predicate @@ -67,7 +93,7 @@ def can_change_taxonomy_tag(user: User, tag: oel_tagging.Tag = None) -> bool: taxonomy = tag.taxonomy if tag else None if taxonomy: taxonomy = taxonomy.cast() - return is_taxonomy_admin(user, taxonomy) and ( + return is_taxonomy_user(user, taxonomy) and ( not tag or not taxonomy or (taxonomy and not taxonomy.allow_free_text and not taxonomy.system_defined) @@ -82,13 +108,13 @@ def can_change_object_tag(user: User, object_tag: oel_tagging.ObjectTag = None) taxonomy = object_tag.taxonomy if object_tag else None if taxonomy: taxonomy = taxonomy.cast() - return is_taxonomy_admin(user, taxonomy) and ( + return is_taxonomy_user(user, taxonomy) and ( not object_tag or not taxonomy or (taxonomy and taxonomy.cast().enabled) ) # Taxonomy -rules.set_perm("oel_tagging.add_taxonomy", can_change_taxonomy) +rules.set_perm("oel_tagging.add_taxonomy", can_add_taxonomy) rules.set_perm("oel_tagging.change_taxonomy", can_change_taxonomy) rules.set_perm("oel_tagging.delete_taxonomy", can_change_taxonomy) rules.set_perm("oel_tagging.view_taxonomy", can_view_taxonomy) diff --git a/openedx/features/content_tagging/tests/test_rules.py b/openedx/features/content_tagging/tests/test_rules.py index 77dcc2270b28..da52418b6802 100644 --- a/openedx/features/content_tagging/tests/test_rules.py +++ b/openedx/features/content_tagging/tests/test_rules.py @@ -104,39 +104,68 @@ def _expected_users_have_perm( # Taxonomy @ddt.data( - ("oel_tagging.add_taxonomy", "taxonomy_all_orgs"), - ("oel_tagging.add_taxonomy", "taxonomy_both_orgs"), - ("oel_tagging.add_taxonomy", "taxonomy_disabled"), - ("oel_tagging.change_taxonomy", "taxonomy_all_orgs"), - ("oel_tagging.change_taxonomy", "taxonomy_both_orgs"), - ("oel_tagging.change_taxonomy", "taxonomy_disabled"), - ("oel_tagging.delete_taxonomy", "taxonomy_all_orgs"), - ("oel_tagging.delete_taxonomy", "taxonomy_both_orgs"), - ("oel_tagging.delete_taxonomy", "taxonomy_disabled"), + "oel_tagging.add_taxonomy", + "oel_tagging.change_taxonomy", + "oel_tagging.delete_taxonomy", ) - @ddt.unpack - def test_change_taxonomy_all_orgs(self, perm, taxonomy_attr): - """Taxonomy administrators with course creator access for the taxonomy org""" - taxonomy = getattr(self, taxonomy_attr) - self._expected_users_have_perm(perm, taxonomy) + def test_taxonomy_base_edit_permissions(self, perm): + """ + Test that only Staff & Superuser can edit/delete to global taxonomies. + """ + assert self.superuser.has_perm(perm) + assert self.staff.has_perm(perm) + assert not self.user_all_orgs.has_perm(perm) + assert not self.user_both_orgs.has_perm(perm) + assert not self.user_org2.has_perm(perm) + assert not self.learner.has_perm(perm) + + @ddt.data( + "oel_tagging.view_taxonomy", + ) + def test_taxonomy_base_view_permissions(self, perm): + """ + Test that everyone can call view + """ + assert self.superuser.has_perm(perm) + assert self.staff.has_perm(perm) + assert self.user_all_orgs.has_perm(perm) + assert self.user_both_orgs.has_perm(perm) + assert self.user_org2.has_perm(perm) + assert self.learner.has_perm(perm) @ddt.data( - ("oel_tagging.add_taxonomy", "taxonomy_one_org"), + ("oel_tagging.change_taxonomy", "taxonomy_all_orgs"), + ("oel_tagging.change_taxonomy", "taxonomy_disabled"), + ("oel_tagging.change_taxonomy", "taxonomy_both_orgs"), ("oel_tagging.change_taxonomy", "taxonomy_one_org"), + ("oel_tagging.change_taxonomy", "taxonomy_no_orgs"), + ("oel_tagging.delete_taxonomy", "taxonomy_all_orgs"), + ("oel_tagging.delete_taxonomy", "taxonomy_disabled"), + ("oel_tagging.delete_taxonomy", "taxonomy_both_orgs"), ("oel_tagging.delete_taxonomy", "taxonomy_one_org"), + ("oel_tagging.delete_taxonomy", "taxonomy_no_orgs"), ) @ddt.unpack - def test_change_taxonomy_org1(self, perm, taxonomy_attr): + def test_change_taxonomy(self, perm, taxonomy_attr): + """ + Test that only Staff & Superuser can edit/delete taxonomies. + """ taxonomy = getattr(self, taxonomy_attr) - self._expected_users_have_perm(perm, taxonomy, user_org2=False) + assert self.superuser.has_perm(perm, taxonomy) + assert self.staff.has_perm(perm, taxonomy) + assert not self.user_all_orgs.has_perm(perm, taxonomy) + assert not self.user_both_orgs.has_perm(perm, taxonomy) + assert not self.user_org2.has_perm(perm, taxonomy) + assert not self.learner.has_perm(perm, taxonomy) @ddt.data( - "oel_tagging.add_taxonomy", "oel_tagging.change_taxonomy", "oel_tagging.delete_taxonomy", ) def test_system_taxonomy(self, perm): - """Taxonomy administrators cannot edit system taxonomies""" + """ + Test that even taxonomy administrators cannot edit/delete system taxonomies + """ system_taxonomy = api.create_taxonomy( name="System Languages", ) @@ -150,49 +179,40 @@ def test_system_taxonomy(self, perm): assert not self.learner.has_perm(perm, system_taxonomy) @ddt.data( - (True, "taxonomy_all_orgs"), - (False, "taxonomy_all_orgs"), - (True, "taxonomy_both_orgs"), - (False, "taxonomy_both_orgs"), - ) - @ddt.unpack - def test_view_taxonomy_enabled(self, enabled, taxonomy_attr): - """Anyone can see enabled taxonomies, but learners cannot see disabled taxonomies""" - taxonomy = getattr(self, taxonomy_attr) - taxonomy.enabled = enabled - perm = "oel_tagging.view_taxonomy" - self._expected_users_have_perm(perm, taxonomy, learner_obj=enabled) - - @ddt.data( - (True, "taxonomy_no_orgs"), - (False, "taxonomy_no_orgs"), + "taxonomy_all_orgs", + "taxonomy_both_orgs", + "taxonomy_one_org", + "taxonomy_no_orgs", ) - @ddt.unpack - def test_view_taxonomy_no_orgs(self, enabled, taxonomy_attr): + def test_view_taxonomy_enabled(self, taxonomy_attr): """ - Enabled taxonomies with no org can be viewed by anyone. - Disabled taxonomies with no org can only be viewed by staff/superusers. + Test that anyone can view enabled taxonomies """ taxonomy = getattr(self, taxonomy_attr) - taxonomy.enabled = enabled + taxonomy.enabled = True perm = "oel_tagging.view_taxonomy" + assert self.superuser.has_perm(perm, taxonomy) assert self.staff.has_perm(perm, taxonomy) - assert self.user_all_orgs.has_perm(perm, taxonomy) == enabled - assert self.user_both_orgs.has_perm(perm, taxonomy) == enabled - assert self.user_org2.has_perm(perm, taxonomy) == enabled - assert self.learner.has_perm(perm, taxonomy) == enabled + assert self.user_all_orgs.has_perm(perm, taxonomy) + assert self.user_both_orgs.has_perm(perm, taxonomy) + assert self.user_org2.has_perm(perm, taxonomy) + assert self.learner.has_perm(perm, taxonomy) @ddt.data( - ("oel_tagging.change_taxonomy", "taxonomy_no_orgs"), - ("oel_tagging.delete_taxonomy", "taxonomy_no_orgs"), + "taxonomy_all_orgs", + "taxonomy_both_orgs", + "taxonomy_one_org", + "taxonomy_no_orgs", ) - @ddt.unpack - def test_change_taxonomy_no_orgs(self, perm, taxonomy_attr): + def test_view_taxonomy_disabled(self, taxonomy_attr): """ - Taxonomies with no org can only be changed by staff and superusers. + Test that only Staff & Superuser can view disabled taxonomies. """ taxonomy = getattr(self, taxonomy_attr) + taxonomy.enabled = False + perm = "oel_tagging.view_taxonomy" + assert self.superuser.has_perm(perm, taxonomy) assert self.staff.has_perm(perm, taxonomy) assert not self.user_all_orgs.has_perm(perm, taxonomy) @@ -446,11 +466,6 @@ def test_object_tag_no_taxonomy(self, perm): # Taxonomy @ddt.data( - ("oel_tagging.add_taxonomy", "taxonomy_all_orgs"), - ("oel_tagging.add_taxonomy", "taxonomy_both_orgs"), - ("oel_tagging.add_taxonomy", "taxonomy_disabled"), - ("oel_tagging.add_taxonomy", "taxonomy_one_org"), - ("oel_tagging.add_taxonomy", "taxonomy_no_orgs"), ("oel_tagging.change_taxonomy", "taxonomy_all_orgs"), ("oel_tagging.change_taxonomy", "taxonomy_both_orgs"), ("oel_tagging.change_taxonomy", "taxonomy_disabled"), @@ -470,17 +485,11 @@ def test_no_orgs_no_perms(self, perm, taxonomy_attr): Organization.objects.all().delete() taxonomy = getattr(self, taxonomy_attr) # Superusers & Staff always have access - assert self.superuser.has_perm(perm) assert self.superuser.has_perm(perm, taxonomy) - assert self.staff.has_perm(perm) assert self.staff.has_perm(perm, taxonomy) # But everyone else's object-level access is removed - assert self.user_all_orgs.has_perm(perm) assert not self.user_all_orgs.has_perm(perm, taxonomy) - assert self.user_both_orgs.has_perm(perm) assert not self.user_both_orgs.has_perm(perm, taxonomy) - assert self.user_org2.has_perm(perm) assert not self.user_org2.has_perm(perm, taxonomy) - assert self.learner.has_perm(perm) assert not self.learner.has_perm(perm, taxonomy) diff --git a/openedx/features/content_tagging/urls.py b/openedx/features/content_tagging/urls.py new file mode 100644 index 000000000000..b81c01e1b048 --- /dev/null +++ b/openedx/features/content_tagging/urls.py @@ -0,0 +1,10 @@ +""" +Content Tagging URLs +""" +from django.urls import path, include + +from .rest_api import urls + +urlpatterns = [ + path('', include(urls)), +]