feat: add taxonomies for org api (#32871)
* feat: add taxonomies for org api * chore: run CI * feat: Add retrieve object_tags REST API (#577) * chore: update requirements --------- Co-authored-by: Yusuf Musleh <yusuf@opencraft.com>
This commit is contained in:
@@ -332,3 +332,8 @@ urlpatterns.extend(get_plugin_url_patterns(ProjectType.CMS))
|
||||
urlpatterns += [
|
||||
path('api/contentstore/', include('cms.djangoapps.contentstore.rest_api.urls'))
|
||||
]
|
||||
|
||||
# Content tagging
|
||||
urlpatterns += [
|
||||
path('api/content_tagging/', include(('openedx.features.content_tagging.urls'))),
|
||||
]
|
||||
|
||||
@@ -101,20 +101,16 @@ def get_taxonomies_for_org(
|
||||
|
||||
|
||||
def get_content_tags(
|
||||
object_id: str, taxonomy: Taxonomy = None, valid_only=True
|
||||
object_id: str, taxonomy_id: str = None
|
||||
) -> Iterator[ContentObjectTag]:
|
||||
"""
|
||||
Generates a list of content tags for a given object.
|
||||
|
||||
Pass taxonomy to limit the returned object_tags to a specific taxonomy.
|
||||
|
||||
Pass valid_only=False when displaying tags to content authors, so they can see invalid tags too.
|
||||
Invalid tags will (probably) be hidden from learners.
|
||||
"""
|
||||
for object_tag in oel_tagging.get_object_tags(
|
||||
object_id=object_id,
|
||||
taxonomy=taxonomy,
|
||||
valid_only=valid_only,
|
||||
taxonomy_id=taxonomy_id,
|
||||
):
|
||||
yield ContentObjectTag.cast(object_tag)
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ class TaxonomyOrg(models.Model):
|
||||
|
||||
@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:
|
||||
|
||||
9
openedx/features/content_tagging/rest_api/urls.py
Normal file
9
openedx/features/content_tagging/rest_api/urls.py
Normal file
@@ -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))]
|
||||
21
openedx/features/content_tagging/rest_api/v1/filters.py
Normal file
21
openedx/features/content_tagging/rest_api/v1/filters.py
Normal file
@@ -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)
|
||||
23
openedx/features/content_tagging/rest_api/v1/serializers.py
Normal file
23
openedx/features/content_tagging/rest_api/v1/serializers.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""
|
||||
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 TaxonomyOrgListQueryParamsSerializer(TaxonomyListQueryParamsSerializer):
|
||||
"""
|
||||
Serializer for the query params for the GET view
|
||||
"""
|
||||
|
||||
org = serializers.SlugRelatedField(
|
||||
slug_field="short_name",
|
||||
queryset=Organization.objects.all(),
|
||||
required=False,
|
||||
)
|
||||
641
openedx/features/content_tagging/rest_api/v1/tests/test_views.py
Normal file
641
openedx/features/content_tagging/rest_api/v1/tests/test_views.py
Normal file
@@ -0,0 +1,641 @@
|
||||
"""
|
||||
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.core.djangolib.testing.utils import skip_unless_cms
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
@skip_unless_cms
|
||||
@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")),
|
||||
# Default page_size=10, and so "tC1" and "tC2" appear on the second page
|
||||
("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
|
||||
|
||||
|
||||
@skip_unless_cms
|
||||
@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
|
||||
"""
|
||||
19
openedx/features/content_tagging/rest_api/v1/urls.py
Normal file
19
openedx/features/content_tagging/rest_api/v1/urls.py
Normal file
@@ -0,0 +1,19 @@
|
||||
"""
|
||||
Taxonomies API v1 URLs.
|
||||
"""
|
||||
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from django.urls.conf import path, include
|
||||
|
||||
from openedx_tagging.core.tagging.rest_api.v1 import views as oel_tagging_views
|
||||
|
||||
from . import views
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register("taxonomies", views.TaxonomyOrgView, basename="taxonomy")
|
||||
router.register("object_tags", oel_tagging_views.ObjectTagView, basename="object_tag")
|
||||
|
||||
urlpatterns = [
|
||||
path('', include(router.urls))
|
||||
]
|
||||
60
openedx/features/content_tagging/rest_api/v1/views.py
Normal file
60
openedx/features/content_tagging/rest_api/v1/views.py
Normal file
@@ -0,0 +1,60 @@
|
||||
"""
|
||||
Tagging Org API Views
|
||||
"""
|
||||
|
||||
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.
|
||||
This view extends the TaxonomyView to add Organization filters.
|
||||
|
||||
Refer to TaxonomyView docstring for usage details.
|
||||
|
||||
**Additional List Query Parameters**
|
||||
* org (optional) - Filter by organization.
|
||||
|
||||
**List Example Requests**
|
||||
GET api/content_tagging/v1/taxonomies?org=orgA - Get all taxonomies for organization A
|
||||
GET api/content_tagging/v1/taxonomies?org=orgA&enabled=true - Get all enabled taxonomies for organization A
|
||||
|
||||
**List Query Returns**
|
||||
* 200 - Success
|
||||
* 400 - Invalid query parameter
|
||||
* 403 - Permission denied
|
||||
"""
|
||||
|
||||
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)
|
||||
@@ -11,13 +11,13 @@ from .models import TaxonomyOrg
|
||||
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.
|
||||
"""
|
||||
if oel_tagging.is_taxonomy_admin(user):
|
||||
return True
|
||||
@@ -35,27 +35,48 @@ 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.
|
||||
"""
|
||||
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 +88,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_admin(user) and (
|
||||
not tag
|
||||
or not taxonomy
|
||||
or (taxonomy and not taxonomy.allow_free_text and not taxonomy.system_defined)
|
||||
@@ -77,18 +98,18 @@ def can_change_taxonomy_tag(user: User, tag: oel_tagging.Tag = None) -> bool:
|
||||
@rules.predicate
|
||||
def can_change_object_tag(user: User, object_tag: oel_tagging.ObjectTag = None) -> bool:
|
||||
"""
|
||||
Taxonomy admins can create or modify object tags on enabled taxonomies.
|
||||
Taxonomy users can create or modify object tags on enabled taxonomies.
|
||||
"""
|
||||
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)
|
||||
|
||||
@@ -189,14 +189,12 @@ class TestAPITaxonomy(TestTaxonomyMixin, TestCase):
|
||||
object_tag_attr,
|
||||
):
|
||||
taxonomy_id = getattr(self, taxonomy_attr).id
|
||||
taxonomy = api.get_taxonomy(taxonomy_id)
|
||||
object_tag = getattr(self, object_tag_attr)
|
||||
with self.assertNumQueries(1):
|
||||
with self.assertNumQueries(2):
|
||||
valid_tags = list(
|
||||
api.get_content_tags(
|
||||
taxonomy=taxonomy,
|
||||
taxonomy_id=taxonomy_id,
|
||||
object_id=object_tag.object_id,
|
||||
valid_only=True,
|
||||
)
|
||||
)
|
||||
assert len(valid_tags) == 1
|
||||
@@ -219,14 +217,12 @@ class TestAPITaxonomy(TestTaxonomyMixin, TestCase):
|
||||
object_tag_attr,
|
||||
):
|
||||
taxonomy_id = getattr(self, taxonomy_attr).id
|
||||
taxonomy = api.get_taxonomy(taxonomy_id)
|
||||
object_tag = getattr(self, object_tag_attr)
|
||||
with self.assertNumQueries(1):
|
||||
with self.assertNumQueries(2):
|
||||
valid_tags = list(
|
||||
api.get_content_tags(
|
||||
taxonomy=taxonomy,
|
||||
taxonomy_id=taxonomy_id,
|
||||
object_id=object_tag.object_id,
|
||||
valid_only=False,
|
||||
)
|
||||
)
|
||||
assert len(valid_tags) == 1
|
||||
|
||||
@@ -103,40 +103,69 @@ class TestRulesTaxonomy(TestTaxonomyMixin, TestCase):
|
||||
|
||||
# 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"),
|
||||
)
|
||||
@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)
|
||||
|
||||
@ddt.data(
|
||||
("oel_tagging.add_taxonomy", "taxonomy_one_org"),
|
||||
("oel_tagging.change_taxonomy", "taxonomy_one_org"),
|
||||
("oel_tagging.delete_taxonomy", "taxonomy_one_org"),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_change_taxonomy_org1(self, perm, taxonomy_attr):
|
||||
taxonomy = getattr(self, taxonomy_attr)
|
||||
self._expected_users_have_perm(perm, taxonomy, user_org2=False)
|
||||
|
||||
@ddt.data(
|
||||
"oel_tagging.add_taxonomy",
|
||||
"oel_tagging.change_taxonomy",
|
||||
"oel_tagging.delete_taxonomy",
|
||||
)
|
||||
def test_taxonomy_base_edit_permissions(self, perm):
|
||||
"""
|
||||
Test that only Staff & Superuser can call add/edit/delete 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 taxonomy.
|
||||
"""
|
||||
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.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(self, perm, taxonomy_attr):
|
||||
"""
|
||||
Test that only Staff & Superuser can edit/delete taxonomies.
|
||||
"""
|
||||
taxonomy = getattr(self, taxonomy_attr)
|
||||
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.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 @@ class TestRulesTaxonomy(TestTaxonomyMixin, TestCase):
|
||||
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"),
|
||||
"taxonomy_all_orgs",
|
||||
"taxonomy_both_orgs",
|
||||
"taxonomy_one_org",
|
||||
"taxonomy_no_orgs",
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_view_taxonomy_enabled(self, enabled, taxonomy_attr):
|
||||
"""Anyone can see enabled taxonomies, but learners cannot see disabled taxonomies"""
|
||||
def test_view_taxonomy_enabled(self, taxonomy_attr):
|
||||
"""
|
||||
Test that anyone can view enabled taxonomies.
|
||||
"""
|
||||
taxonomy = getattr(self, taxonomy_attr)
|
||||
taxonomy.enabled = enabled
|
||||
taxonomy.enabled = True
|
||||
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"),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_view_taxonomy_no_orgs(self, enabled, taxonomy_attr):
|
||||
"""
|
||||
Enabled taxonomies with no org can be viewed by anyone.
|
||||
Disabled taxonomies with no org can only be viewed by staff/superusers.
|
||||
"""
|
||||
taxonomy = getattr(self, taxonomy_attr)
|
||||
taxonomy.enabled = enabled
|
||||
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)
|
||||
@@ -202,39 +222,115 @@ class TestRulesTaxonomy(TestTaxonomyMixin, TestCase):
|
||||
|
||||
# Tag
|
||||
|
||||
@ddt.data(
|
||||
("oel_tagging.add_tag", "tag_all_orgs"),
|
||||
("oel_tagging.add_tag", "tag_both_orgs"),
|
||||
("oel_tagging.add_tag", "tag_disabled"),
|
||||
("oel_tagging.change_tag", "tag_all_orgs"),
|
||||
("oel_tagging.change_tag", "tag_both_orgs"),
|
||||
("oel_tagging.change_tag", "tag_disabled"),
|
||||
("oel_tagging.delete_tag", "tag_all_orgs"),
|
||||
("oel_tagging.delete_tag", "tag_both_orgs"),
|
||||
("oel_tagging.delete_tag", "tag_disabled"),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_change_tag_all_orgs(self, perm, tag_attr):
|
||||
"""Taxonomy administrators can modify tags on non-free-text taxonomies"""
|
||||
tag = getattr(self, tag_attr)
|
||||
self._expected_users_have_perm(perm, tag)
|
||||
|
||||
@ddt.data(
|
||||
("oel_tagging.add_tag", "tag_one_org"),
|
||||
("oel_tagging.change_tag", "tag_one_org"),
|
||||
("oel_tagging.delete_tag", "tag_one_org"),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_change_tag_org1(self, perm, tag_attr):
|
||||
"""Taxonomy administrators can modify tags on non-free-text taxonomies"""
|
||||
tag = getattr(self, tag_attr)
|
||||
self._expected_users_have_perm(perm, tag, user_org2=False)
|
||||
|
||||
@ddt.data(
|
||||
"oel_tagging.add_tag",
|
||||
"oel_tagging.change_tag",
|
||||
"oel_tagging.delete_tag",
|
||||
)
|
||||
def test_tag_base_edit_permissions(self, perm):
|
||||
"""
|
||||
Test that only Staff & Superuser can call add/edit/delete tags.
|
||||
"""
|
||||
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_tag",
|
||||
)
|
||||
def test_tag_base_view_permissions(self, perm):
|
||||
"""
|
||||
Test that everyone can call view tag.
|
||||
"""
|
||||
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.change_tag", "tag_all_orgs"),
|
||||
("oel_tagging.change_tag", "tag_disabled"),
|
||||
("oel_tagging.change_tag", "tag_both_orgs"),
|
||||
("oel_tagging.change_tag", "tag_one_org"),
|
||||
("oel_tagging.change_tag", "tag_no_orgs"),
|
||||
("oel_tagging.delete_tag", "tag_all_orgs"),
|
||||
("oel_tagging.delete_tag", "tag_disabled"),
|
||||
("oel_tagging.delete_tag", "tag_both_orgs"),
|
||||
("oel_tagging.delete_tag", "tag_one_org"),
|
||||
("oel_tagging.delete_tag", "tag_no_orgs"),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_change_tag(self, perm, tag_attr):
|
||||
"""
|
||||
Test that only Staff & Superuser can edit/delete taxonomies.
|
||||
"""
|
||||
tag = getattr(self, tag_attr)
|
||||
assert self.superuser.has_perm(perm, tag)
|
||||
assert self.staff.has_perm(perm, tag)
|
||||
assert not self.user_all_orgs.has_perm(perm, tag)
|
||||
assert not self.user_both_orgs.has_perm(perm, tag)
|
||||
assert not self.user_org2.has_perm(perm, tag)
|
||||
assert not self.learner.has_perm(perm, tag)
|
||||
|
||||
@ddt.data(
|
||||
"oel_tagging.change_tag",
|
||||
"oel_tagging.delete_tag",
|
||||
)
|
||||
def test_system_taxonomy_tag(self, perm):
|
||||
"""
|
||||
Test that even taxonomy administrators cannot edit/delete tags on system taxonomies.
|
||||
"""
|
||||
system_taxonomy = api.create_taxonomy(
|
||||
name="System Languages",
|
||||
)
|
||||
system_taxonomy.taxonomy_class = UserSystemDefinedTaxonomy
|
||||
system_taxonomy = system_taxonomy.cast()
|
||||
tag_system_taxonomy = Tag.objects.create(
|
||||
taxonomy=system_taxonomy,
|
||||
value="en",
|
||||
)
|
||||
|
||||
assert self.superuser.has_perm(perm, tag_system_taxonomy)
|
||||
assert not self.staff.has_perm(perm, tag_system_taxonomy)
|
||||
assert not self.user_all_orgs.has_perm(perm, tag_system_taxonomy)
|
||||
assert not self.user_both_orgs.has_perm(perm, tag_system_taxonomy)
|
||||
assert not self.user_org2.has_perm(perm, tag_system_taxonomy)
|
||||
assert not self.learner.has_perm(perm, tag_system_taxonomy)
|
||||
|
||||
@ddt.data(
|
||||
"oel_tagging.change_tag",
|
||||
"oel_tagging.delete_tag",
|
||||
)
|
||||
def test_free_text_taxonomy_tag(self, perm):
|
||||
"""
|
||||
Test that even taxonomy administrators cannot edit/delete tags on free text taxonomies.
|
||||
"""
|
||||
free_text_taxonomy = api.create_taxonomy(
|
||||
name="Free text",
|
||||
allow_free_text=True,
|
||||
)
|
||||
|
||||
tag_free_text_taxonomy = Tag.objects.create(
|
||||
taxonomy=free_text_taxonomy,
|
||||
value="value1",
|
||||
)
|
||||
|
||||
assert self.superuser.has_perm(perm, tag_free_text_taxonomy)
|
||||
assert not self.staff.has_perm(perm, tag_free_text_taxonomy)
|
||||
assert not self.user_all_orgs.has_perm(perm, tag_free_text_taxonomy)
|
||||
assert not self.user_both_orgs.has_perm(perm, tag_free_text_taxonomy)
|
||||
assert not self.user_org2.has_perm(perm, tag_free_text_taxonomy)
|
||||
assert not self.learner.has_perm(perm, tag_free_text_taxonomy)
|
||||
|
||||
@ddt.data(
|
||||
"oel_tagging.change_tag",
|
||||
"oel_tagging.delete_tag",
|
||||
)
|
||||
def test_tag_no_taxonomy(self, perm):
|
||||
"""Taxonomy administrators can modify any Tag, even those with no Taxonnmy."""
|
||||
tag = Tag()
|
||||
@@ -242,10 +338,9 @@ class TestRulesTaxonomy(TestTaxonomyMixin, TestCase):
|
||||
# Global Taxonomy Admins can do pretty much anything
|
||||
assert self.superuser.has_perm(perm, tag)
|
||||
assert self.staff.has_perm(perm, tag)
|
||||
assert self.user_all_orgs.has_perm(perm, tag)
|
||||
|
||||
# Org content creators are bound by a taxonomy's org restrictions,
|
||||
# so if there's no taxonomy, they can't do anything to it.
|
||||
# Everyone else can't do anything
|
||||
assert not self.user_all_orgs.has_perm(perm, tag)
|
||||
assert not self.user_both_orgs.has_perm(perm, tag)
|
||||
assert not self.user_org2.has_perm(perm, tag)
|
||||
assert not self.learner.has_perm(perm, tag)
|
||||
@@ -403,26 +498,6 @@ class TestRulesTaxonomyNoCreatorGroup(
|
||||
user_org2=True,
|
||||
)
|
||||
|
||||
@ddt.data(
|
||||
"oel_tagging.add_tag",
|
||||
"oel_tagging.change_tag",
|
||||
"oel_tagging.delete_tag",
|
||||
)
|
||||
def test_tag_no_taxonomy(self, perm):
|
||||
"""Taxonomy administrators can modify any Tag, even those with no Taxonnmy."""
|
||||
tag = Tag()
|
||||
|
||||
# Global Taxonomy Admins can do pretty much anything
|
||||
assert self.superuser.has_perm(perm, tag)
|
||||
assert self.staff.has_perm(perm, tag)
|
||||
assert self.user_all_orgs.has_perm(perm, tag)
|
||||
|
||||
# Org content creators are bound by a taxonomy's org restrictions,
|
||||
# but since there's no org restrictions enabled, anyone has these permissions.
|
||||
assert self.user_both_orgs.has_perm(perm, tag)
|
||||
assert self.user_org2.has_perm(perm, tag)
|
||||
assert self.learner.has_perm(perm, tag)
|
||||
|
||||
@ddt.data(
|
||||
"oel_tagging.add_object_tag",
|
||||
"oel_tagging.change_object_tag",
|
||||
@@ -446,11 +521,6 @@ class TestRulesTaxonomyNoCreatorGroup(
|
||||
# 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 +540,11 @@ class TestRulesTaxonomyNoCreatorGroup(
|
||||
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)
|
||||
|
||||
10
openedx/features/content_tagging/urls.py
Normal file
10
openedx/features/content_tagging/urls.py
Normal file
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
Content Tagging URLs
|
||||
"""
|
||||
from django.urls import path, include
|
||||
|
||||
from .rest_api import urls
|
||||
|
||||
urlpatterns = [
|
||||
path('', include(urls)),
|
||||
]
|
||||
@@ -131,8 +131,3 @@ click==8.1.6
|
||||
|
||||
# plz upgrade this in separate ticket
|
||||
redis==4.6.0
|
||||
|
||||
# openedx-learning new version has some changes which are breaking quality tests
|
||||
# See https://github.com/openedx/openedx-learning/pull/68 for the changes.
|
||||
# It needs to be updated in a separate issue.
|
||||
openedx-learning==0.1.2
|
||||
|
||||
@@ -43,6 +43,7 @@ attrs==23.1.0
|
||||
# lti-consumer-xblock
|
||||
# openedx-blockstore
|
||||
# openedx-events
|
||||
# openedx-learning
|
||||
# referencing
|
||||
babel==2.11.0
|
||||
# via
|
||||
@@ -98,6 +99,7 @@ celery==5.3.1
|
||||
# edx-celeryutils
|
||||
# edx-enterprise
|
||||
# event-tracking
|
||||
# openedx-learning
|
||||
certifi==2023.7.22
|
||||
# via
|
||||
# -r requirements/edx/paver.txt
|
||||
@@ -770,10 +772,8 @@ openedx-filters==1.5.0
|
||||
# via
|
||||
# -r requirements/edx/kernel.in
|
||||
# lti-consumer-xblock
|
||||
openedx-learning==0.1.2
|
||||
# via
|
||||
# -c requirements/edx/../constraints.txt
|
||||
# -r requirements/edx/kernel.in
|
||||
openedx-learning==0.1.5
|
||||
# via -r requirements/edx/kernel.in
|
||||
openedx-mongodbproxy==0.2.0
|
||||
# via -r requirements/edx/kernel.in
|
||||
optimizely-sdk==4.1.1
|
||||
|
||||
@@ -92,6 +92,7 @@ attrs==23.1.0
|
||||
# lti-consumer-xblock
|
||||
# openedx-blockstore
|
||||
# openedx-events
|
||||
# openedx-learning
|
||||
# referencing
|
||||
babel==2.11.0
|
||||
# via
|
||||
@@ -175,6 +176,7 @@ celery==5.3.1
|
||||
# edx-celeryutils
|
||||
# edx-enterprise
|
||||
# event-tracking
|
||||
# openedx-learning
|
||||
certifi==2023.7.22
|
||||
# via
|
||||
# -r requirements/edx/doc.txt
|
||||
@@ -1299,9 +1301,8 @@ openedx-filters==1.5.0
|
||||
# -r requirements/edx/doc.txt
|
||||
# -r requirements/edx/testing.txt
|
||||
# lti-consumer-xblock
|
||||
openedx-learning==0.1.2
|
||||
openedx-learning==0.1.5
|
||||
# via
|
||||
# -c requirements/edx/../constraints.txt
|
||||
# -r requirements/edx/doc.txt
|
||||
# -r requirements/edx/testing.txt
|
||||
openedx-mongodbproxy==0.2.0
|
||||
|
||||
@@ -60,6 +60,7 @@ attrs==23.1.0
|
||||
# lti-consumer-xblock
|
||||
# openedx-blockstore
|
||||
# openedx-events
|
||||
# openedx-learning
|
||||
# referencing
|
||||
babel==2.11.0
|
||||
# via
|
||||
@@ -125,6 +126,7 @@ celery==5.3.1
|
||||
# edx-celeryutils
|
||||
# edx-enterprise
|
||||
# event-tracking
|
||||
# openedx-learning
|
||||
certifi==2023.7.22
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
@@ -910,10 +912,8 @@ openedx-filters==1.5.0
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
# lti-consumer-xblock
|
||||
openedx-learning==0.1.2
|
||||
# via
|
||||
# -c requirements/edx/../constraints.txt
|
||||
# -r requirements/edx/base.txt
|
||||
openedx-learning==0.1.5
|
||||
# via -r requirements/edx/base.txt
|
||||
openedx-mongodbproxy==0.2.0
|
||||
# via -r requirements/edx/base.txt
|
||||
optimizely-sdk==4.1.1
|
||||
|
||||
@@ -66,6 +66,7 @@ attrs==23.1.0
|
||||
# lti-consumer-xblock
|
||||
# openedx-blockstore
|
||||
# openedx-events
|
||||
# openedx-learning
|
||||
# referencing
|
||||
babel==2.11.0
|
||||
# via
|
||||
@@ -131,6 +132,7 @@ celery==5.3.1
|
||||
# edx-celeryutils
|
||||
# edx-enterprise
|
||||
# event-tracking
|
||||
# openedx-learning
|
||||
certifi==2023.7.22
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
@@ -979,10 +981,8 @@ openedx-filters==1.5.0
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
# lti-consumer-xblock
|
||||
openedx-learning==0.1.2
|
||||
# via
|
||||
# -c requirements/edx/../constraints.txt
|
||||
# -r requirements/edx/base.txt
|
||||
openedx-learning==0.1.5
|
||||
# via -r requirements/edx/base.txt
|
||||
openedx-mongodbproxy==0.2.0
|
||||
# via -r requirements/edx/base.txt
|
||||
optimizely-sdk==4.1.1
|
||||
|
||||
Reference in New Issue
Block a user