diff --git a/enterprise_catalog/apps/api/urls.py b/enterprise_catalog/apps/api/urls.py index 153b5f93..097f4eea 100644 --- a/enterprise_catalog/apps/api/urls.py +++ b/enterprise_catalog/apps/api/urls.py @@ -8,9 +8,11 @@ from django.urls import include, path from enterprise_catalog.apps.api.v1 import urls as v1_urls +from enterprise_catalog.apps.api.v2 import urls as v2_urls app_name = 'api' urlpatterns = [ path('v1/', include(v1_urls)), + path('v2/', include(v2_urls)), ] diff --git a/enterprise_catalog/apps/api/v2/tests/__init__.py b/enterprise_catalog/apps/api/v2/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/enterprise_catalog/apps/api/v2/tests/test_views.py b/enterprise_catalog/apps/api/v2/tests/test_views.py new file mode 100644 index 00000000..a5f412c1 --- /dev/null +++ b/enterprise_catalog/apps/api/v2/tests/test_views.py @@ -0,0 +1,485 @@ +from datetime import datetime +from unittest import mock + +import ddt +import pytz +from rest_framework import status +from rest_framework.reverse import reverse + +from enterprise_catalog.apps.api.v1.tests.mixins import APITestMixin +from enterprise_catalog.apps.catalog.constants import ( + COURSE_RUN_RESTRICTION_TYPE_KEY, + RESTRICTION_FOR_B2B, +) +from enterprise_catalog.apps.catalog.models import ContentMetadata +from enterprise_catalog.apps.catalog.tests import test_utils +from enterprise_catalog.apps.catalog.tests.factories import ( + EnterpriseCatalogFactory, +) + + +@ddt.ddt +class EnterpriseCatalogGetContentMetadataTests(APITestMixin): + """ + Tests on the get_content_metadata endpoint + """ + + def setUp(self): + super().setUp() + # Set up catalog.has_learner_access permissions + self.set_up_catalog_learner() + self.enterprise_catalog = EnterpriseCatalogFactory(enterprise_uuid=self.enterprise_uuid) + self.enterprise_catalog.catalog_query.save() + + # Delete any existing ContentMetadata records. + ContentMetadata.objects.all().delete() + + def _get_content_metadata_url(self, enterprise_catalog): + """ + Helper to get the get_content_metadata endpoint url for a given catalog + """ + return reverse('api:v2:get-content-metadata-v2', kwargs={'uuid': enterprise_catalog.uuid}) + + def test_get_content_metadata_no_catalog_query(self): + """ + Verify the get_content_metadata endpoint returns no results if the catalog has no catalog query + """ + no_catalog_query_catalog = EnterpriseCatalogFactory( + catalog_query=None, + enterprise_uuid=self.enterprise_uuid, + ) + url = self._get_content_metadata_url(no_catalog_query_catalog) + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.json()['results'], []) + + @mock.patch('enterprise_catalog.apps.api_client.enterprise_cache.EnterpriseApiClient') + @ddt.data( + # Create a course with both an unrestricted (run1) and restricted run (run2), and the restricted run is allowed + # by the CatalogQuery. + { + 'create_catalog_query': { + '11111111-1111-1111-1111-111111111111': { + 'content_filter': { + 'restricted_runs_allowed': { + 'course:edX+course': [ + 'course-v1:edX+course+run2', + ], + }, + }, + }, + }, + 'create_content_metadata': { + 'edX+course': { + 'create_runs': { + 'course-v1:edX+course+run1': {'is_restricted': False}, + 'course-v1:edX+course+run2': {'is_restricted': True}, + }, + 'json_metadata': { + 'key': 'edX+course', + 'course_runs': [ + { + 'key': 'course-v1:edX+course+run1', + 'status': 'published', + 'is_enrollable': True, + 'is_marketable': True, + }, + ], + }, + 'associate_with_catalog_query': '11111111-1111-1111-1111-111111111111', + }, + }, + 'create_restricted_courses': { + 1: { + 'content_key': 'edX+course', + 'catalog_query': '11111111-1111-1111-1111-111111111111', + 'json_metadata': { + 'key': 'edX+course', + 'course_runs': [ + { + 'key': 'course-v1:edX+course+run1', + 'status': 'published', + 'is_enrollable': True, + 'is_marketable': True, + }, + { + 'key': 'course-v1:edX+course+run2', + 'status': 'published', + 'is_enrollable': True, + 'is_marketable': True, + }, + ], + }, + }, + }, + 'create_restricted_run_allowed_for_restricted_course': [ + {'course': 1, 'run': 'course-v1:edX+course+run2'}, + ], + }, + ) + @ddt.unpack + def test_get_content_metadata_content_filters( + + self, + mock_api_client, + create_catalog_query, + create_content_metadata=None, + create_restricted_courses=None, + create_restricted_run_allowed_for_restricted_course=None, + ): + """ + Test that the get_content_metadata view GET view will filter provided content_keys (up to a limit) + """ + mock_api_client.return_value.get_enterprise_customer.return_value = { + 'slug': self.enterprise_slug, + 'enable_learner_portal': True, + 'modified': str(datetime.now().replace(tzinfo=pytz.UTC)), + } + + main_catalog, catalog_queries, content_metadata, restricted_courses = test_utils.setup_scaffolding( + create_catalog_query, + create_content_metadata, + create_restricted_courses, + create_restricted_run_allowed_for_restricted_course, + ) + main_catalog.enterprise_uuid = self.enterprise_uuid + main_catalog.save() + + filtered_content_keys = ['course-v1:edX+course+run1', 'course-v1:edX+course+run2', ] + url = self._get_content_metadata_url(main_catalog) + for filter_content_key in filtered_content_keys: + url += f"&content_keys={filter_content_key}" + + response = self.client.get( + url, + {'content_keys': filtered_content_keys} + ) + + self.assertEqual(response.data.get('count'), 1) + results = response.data.get('results')[0] + self.assertEqual(results.get('key'), 'edX+course') + course_runs = results.get('course_runs') + + for course_run in course_runs: + self.assertIn(course_run.get('key'), filtered_content_keys) + + @mock.patch('enterprise_catalog.apps.api_client.enterprise_cache.EnterpriseApiClient') + @ddt.data( + # Create a course with both an unrestricted (run1) and restricted run (run2), and the restricted run is NOT + # allowed by the CatalogQuery. + { + 'create_catalog_query': { + '11111111-1111-1111-1111-111111111111': { + 'content_filter': { + 'restricted_runs_allowed': {}, + }, + }, + }, + 'create_content_metadata': { + 'edX+course': { + 'create_runs': { + 'course-v1:edX+course+run1': {'is_restricted': False}, + 'course-v1:edX+course+run2': {'is_restricted': True}, + }, + 'json_metadata': { + 'key': 'edX+course', + 'course_runs': [ + { + 'key': 'course-v1:edX+course+run1', + 'status': 'published', + 'is_enrollable': True, + 'is_marketable': False, + COURSE_RUN_RESTRICTION_TYPE_KEY: RESTRICTION_FOR_B2B, + }, + ], + }, + 'associate_with_catalog_query': '11111111-1111-1111-1111-111111111111', + }, + }, + 'create_restricted_courses': { + 1: { + 'content_key': 'edX+course', + 'catalog_query': '11111111-1111-1111-1111-111111111111', + 'json_metadata': { + 'key': 'edX+course', + 'course_runs': [ + { + 'key': 'course-v1:edX+course+run1', + 'status': 'published', + 'is_enrollable': True, + 'is_marketable': False, + COURSE_RUN_RESTRICTION_TYPE_KEY: RESTRICTION_FOR_B2B, + }, + { + 'key': 'course-v1:edX+course+run2', + 'status': 'published', + 'is_enrollable': True, + 'is_marketable': False, + COURSE_RUN_RESTRICTION_TYPE_KEY: RESTRICTION_FOR_B2B, + }, + ], + }, + }, + }, + 'create_restricted_run_allowed_for_restricted_course': [ + {'course': 1, 'run': 'course-v1:edX+course+run2'}, + ], + }, + ) + @ddt.unpack + def test_get_content_metadata( + self, + mock_api_client, + create_catalog_query, + create_content_metadata=None, + create_restricted_courses=None, + create_restricted_run_allowed_for_restricted_course=None, + ): + """ + Test that the get_content_metadata view GET view will filter provided content_keys (up to a limit) + """ + mock_api_client.return_value.get_enterprise_customer.return_value = { + 'slug': self.enterprise_slug, + 'enable_learner_portal': True, + 'modified': str(datetime.now().replace(tzinfo=pytz.UTC)), + } + + main_catalog, catalog_queries, content_metadata, restricted_courses = test_utils.setup_scaffolding( + create_catalog_query, + create_content_metadata, + create_restricted_courses, + create_restricted_run_allowed_for_restricted_course, + ) + main_catalog.enterprise_uuid = self.enterprise_uuid + main_catalog.save() + + filtered_content_keys = ['course-v1:edX+course+run1', 'course-v1:edX+course+run2', ] + url = self._get_content_metadata_url(main_catalog) + for filter_content_key in filtered_content_keys: + url += f"&content_keys={filter_content_key}" + + response = self.client.get( + url, + {'content_keys': filtered_content_keys} + ) + + self.assertEqual(response.data.get('count'), 1) + results = response.data.get('results')[0] + self.assertEqual(results.get('key'), 'edX+course') + course_runs = results.get('course_runs') + # only one (unrestricted) run is returned + self.assertEqual(len(course_runs), 1) + # contains only the unrestricted run + self.assertEqual(course_runs[0].get('key'), 'course-v1:edX+course+run1') + + @mock.patch('enterprise_catalog.apps.api_client.enterprise_cache.EnterpriseApiClient') + @ddt.data( + # Create a course with both an unrestricted (run1) and restricted run (run2), and the + # restricted run is allowed by the CatalogQuery. But the course runs do not have + # the RESTRICTION_FOR_B2B restriction type set + { + 'create_catalog_query': { + '11111111-1111-1111-1111-111111111111': { + 'content_filter': { + 'restricted_runs_allowed': { + 'course:edX+course': [ + 'course-v1:edX+course+run2', + ], + }, + }, + }, + }, + 'create_content_metadata': { + 'edX+course': { + 'create_runs': { + 'course-v1:edX+course+run1': {'is_restricted': False}, + 'course-v1:edX+course+run2': {'is_restricted': True}, + }, + 'json_metadata': { + 'key': 'edX+course', + 'course_runs': [ + { + 'key': 'course-v1:edX+course+run1', + 'status': 'published', + 'is_enrollable': True, + 'is_marketable': False, + }, + ], + }, + 'associate_with_catalog_query': '11111111-1111-1111-1111-111111111111', + }, + }, + 'create_restricted_courses': { + 1: { + 'content_key': 'edX+course', + 'catalog_query': '11111111-1111-1111-1111-111111111111', + 'json_metadata': { + 'key': 'edX+course', + 'course_runs': [ + { + 'key': 'course-v1:edX+course+run1', + 'status': 'published', + 'is_enrollable': True, + 'is_marketable': False, + }, + { + 'key': 'course-v1:edX+course+run2', + 'status': 'published', + 'is_enrollable': True, + 'is_marketable': False, + }, + ], + }, + }, + }, + 'create_restricted_run_allowed_for_restricted_course': [ + {'course': 1, 'run': 'course-v1:edX+course+run2'}, + ], + }, + ) + @ddt.unpack + def test_get_content_metadata_with_no_restriction_type( + self, + mock_api_client, + create_catalog_query, + create_content_metadata=None, + create_restricted_courses=None, + create_restricted_run_allowed_for_restricted_course=None, + ): + """ + Test that the get_content_metadata view GET view will filter provided content_keys (up to a limit) + """ + mock_api_client.return_value.get_enterprise_customer.return_value = { + 'slug': self.enterprise_slug, + 'enable_learner_portal': True, + 'modified': str(datetime.now().replace(tzinfo=pytz.UTC)), + } + + main_catalog, catalog_queries, content_metadata, restricted_courses = test_utils.setup_scaffolding( + create_catalog_query, + create_content_metadata, + create_restricted_courses, + create_restricted_run_allowed_for_restricted_course, + ) + main_catalog.enterprise_uuid = self.enterprise_uuid + main_catalog.save() + + filtered_content_keys = ['course-v1:edX+course+run1', 'course-v1:edX+course+run2', ] + url = self._get_content_metadata_url(main_catalog) + for filter_content_key in filtered_content_keys: + url += f"&content_keys={filter_content_key}" + + response = self.client.get( + url, + {'content_keys': filtered_content_keys} + ) + + self.assertEqual(response.data.get('count'), 0) + + @mock.patch('enterprise_catalog.apps.api_client.enterprise_cache.EnterpriseApiClient') + @ddt.data( + # Create a course with both an unrestricted (run1) and restricted run (run2), and the restricted run is allowed + # by the CatalogQuery. + { + 'create_catalog_query': { + '11111111-1111-1111-1111-111111111111': { + 'content_filter': { + 'restricted_runs_allowed': { + 'course:edX+course': [ + 'course-v1:edX+course+run2', + ], + }, + }, + }, + }, + 'create_content_metadata': { + 'edX+course': { + 'create_runs': { + 'course-v1:edX+course+run1': {'is_restricted': False}, + 'course-v1:edX+course+run2': {'is_restricted': True}, + }, + 'json_metadata': { + 'key': 'edX+course', + 'course_runs': [ + { + 'key': 'course-v1:edX+course+run1', + 'status': 'published', + 'is_enrollable': True, + 'is_marketable': False, + COURSE_RUN_RESTRICTION_TYPE_KEY: RESTRICTION_FOR_B2B, + }, + ], + }, + 'associate_with_catalog_query': '11111111-1111-1111-1111-111111111111', + }, + }, + 'create_restricted_courses': { + 1: { + 'content_key': 'edX+course', + 'catalog_query': '11111111-1111-1111-1111-111111111111', + 'json_metadata': { + 'key': 'edX+course', + 'course_runs': [ + { + 'key': 'course-v1:edX+course+run1', + 'status': 'published', + 'is_enrollable': True, + 'is_marketable': False, + COURSE_RUN_RESTRICTION_TYPE_KEY: RESTRICTION_FOR_B2B, + }, + { + 'key': 'course-v1:edX+course+run2', + 'status': 'published', + 'is_enrollable': True, + 'is_marketable': False, + COURSE_RUN_RESTRICTION_TYPE_KEY: RESTRICTION_FOR_B2B, + }, + ], + }, + }, + }, + 'create_restricted_run_allowed_for_restricted_course': [ + {'course': 1, 'run': 'course-v1:edX+course+run2'}, + ], + }, + ) + @ddt.unpack + def test_get_content_metadata_no_content_filters( + + self, + mock_api_client, + create_catalog_query, + create_content_metadata=None, + create_restricted_courses=None, + create_restricted_run_allowed_for_restricted_course=None, + ): + """ + Test that the get_content_metadata view GET view will filter provided content_keys (up to a limit) + """ + mock_api_client.return_value.get_enterprise_customer.return_value = { + 'slug': self.enterprise_slug, + 'enable_learner_portal': True, + 'modified': str(datetime.now().replace(tzinfo=pytz.UTC)), + } + + main_catalog, catalog_queries, content_metadata, restricted_courses = test_utils.setup_scaffolding( + create_catalog_query, + create_content_metadata, + create_restricted_courses, + create_restricted_run_allowed_for_restricted_course, + ) + main_catalog.enterprise_uuid = self.enterprise_uuid + main_catalog.save() + + filtered_content_keys = ['course-v1:edX+course+run1', 'course-v1:edX+course+run2', ] + url = self._get_content_metadata_url(main_catalog) + + response = self.client.get(url) + + self.assertEqual(response.data.get('count'), 1) + results = response.data.get('results')[0] + self.assertEqual(results.get('key'), 'edX+course') + course_runs = results.get('course_runs') + + for course_run in course_runs: + self.assertIn(course_run.get('key'), filtered_content_keys) diff --git a/enterprise_catalog/apps/api/v2/urls.py b/enterprise_catalog/apps/api/v2/urls.py new file mode 100644 index 00000000..0a6350cd --- /dev/null +++ b/enterprise_catalog/apps/api/v2/urls.py @@ -0,0 +1,24 @@ +""" +URL definitions for enterprise catalog API version 2. +""" +from django.urls import re_path +from rest_framework.routers import DefaultRouter + +from enterprise_catalog.apps.api.v2.views.enterprise_catalog_get_content_metadata import ( + EnterpriseCatalogGetContentMetadataV2, +) + + +app_name = 'v2' + +router = DefaultRouter() + +urlpatterns = [ + re_path( + r'^enterprise-catalogs/(?P[\S]+)/get_content_metadata', + EnterpriseCatalogGetContentMetadataV2.as_view({'get': 'get'}), + name='get-content-metadata-v2' + ), +] + +urlpatterns += router.urls diff --git a/enterprise_catalog/apps/api/v2/utils.py b/enterprise_catalog/apps/api/v2/utils.py new file mode 100644 index 00000000..1e1e2601 --- /dev/null +++ b/enterprise_catalog/apps/api/v2/utils.py @@ -0,0 +1,53 @@ +import logging + +from enterprise_catalog.apps.catalog.constants import ( + COURSE_RUN_RESTRICTION_TYPE_KEY, + RESTRICTION_FOR_B2B, +) + + +logger = logging.getLogger(__name__) + + +def is_course_run_active(course_run): + """ + Checks whether a course run is active. That is, whether the course run is published, + enrollable, and either marketable, or has a b2b restriction type. To ellaborate on the latter: + + Restricted course run records will be set with `is_marketable: false` from the + upstream source-of-truth (course-discovery). But because our discovery <-> catalog + synchronization has business logic that filters course run json metadata (inside of courses) + to only the *allowed* restricted runs for a catalog, we can safely assume + when looking at a course run metadata record in the context of a catalog, + if that run has a non-null, B2B restriction type, then it is permitted to be + part of the catalog and should be considered active (as long as it is published and enrollable). + + Arguments: + course_run (dict): The metadata about a course run. + + Returns: + bool: True if the course run is "active" + """ + course_run_status = course_run.get('status') or '' + is_published = course_run_status.lower() == 'published' + is_enrollable = course_run.get('is_enrollable', False) + is_marketable = course_run.get('is_marketable', False) + is_restricted = course_run.get(COURSE_RUN_RESTRICTION_TYPE_KEY) == RESTRICTION_FOR_B2B + + return is_published and is_enrollable and (is_marketable or is_restricted) + + +def is_any_course_run_active(course_runs): + """ + Iterates over all course runs to check if there's any course run that is available for enrollment. + + Arguments: + course_runs (list): list of course runs + + Returns: + bool: True if active course run is found, else False + """ + for course_run in course_runs: + if is_course_run_active(course_run): + return True + return False diff --git a/enterprise_catalog/apps/api/v2/views/__init__.py b/enterprise_catalog/apps/api/v2/views/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/enterprise_catalog/apps/api/v2/views/enterprise_catalog_get_content_metadata.py b/enterprise_catalog/apps/api/v2/views/enterprise_catalog_get_content_metadata.py new file mode 100644 index 00000000..520c9b51 --- /dev/null +++ b/enterprise_catalog/apps/api/v2/views/enterprise_catalog_get_content_metadata.py @@ -0,0 +1,44 @@ +from asyncio.log import logger + +from enterprise_catalog.apps.api.v1.views.enterprise_catalog_get_content_metadata import ( + EnterpriseCatalogGetContentMetadata, +) +from enterprise_catalog.apps.api.v2.utils import is_any_course_run_active + + +class EnterpriseCatalogGetContentMetadataV2(EnterpriseCatalogGetContentMetadata): + """ + View for retrieving all the content metadata associated with a catalog. + """ + def get_queryset(self, **kwargs): + """ + Returns all the json of content metadata associated with the catalog. + """ + # Avoids ordering the content metadata by any field on that model to avoid using a temporary table / filesort + queryset = self.enterprise_catalog.content_metadata_with_restricted + content_filter = kwargs.get('content_keys_filter') + if content_filter: + queryset = self.enterprise_catalog.get_matching_content( + content_keys=content_filter, + include_restricted=True + ) + + return queryset.order_by('catalog_queries') + + def is_active(self, item): + """ + Determines if a content item is active. + Args: + item (ContentMetadata): The content metadata item to check. + Returns: + bool: True if the item is active, False otherwise. + For courses, checks if any course run is active. + For other content types, always returns True. + """ + if item.content_type == 'course': + active = is_any_course_run_active( + item.json_metadata.get('course_runs', [])) + if not active: + logger.debug(f'[get_content_metadata]: Content item {item.content_key} is not active.') + return active + return True diff --git a/enterprise_catalog/apps/catalog/tests/test_utils.py b/enterprise_catalog/apps/catalog/tests/test_utils.py new file mode 100644 index 00000000..1d9db07c --- /dev/null +++ b/enterprise_catalog/apps/catalog/tests/test_utils.py @@ -0,0 +1,97 @@ +""" Test Util for catalog models. """ + +from enterprise_catalog.apps.catalog.constants import COURSE, COURSE_RUN +from enterprise_catalog.apps.catalog.tests import factories + + +def setup_scaffolding( + create_catalog_query, + create_content_metadata=None, + create_restricted_courses=None, + create_restricted_run_allowed_for_restricted_course=None, +): + """ + Helper function to create an arbitrary number of CatalogQuery, ContentMetadata, + RestrictedCourseMetadata, and RestrictedRunAllowedForRestrictedCourse objects for testing + purposes. + Sample setup: + { + 'create_catalog_query': { + '11111111-1111-1111-1111-111111111111': { + 'content_filter': { + 'restricted_runs_allowed': { + 'course:edX+course': [ + 'course-v1:edX+course+run2', + ], + }, + }, + }, + }, + 'create_content_metadata': { + 'edX+course': { + 'create_runs': { + 'course-v1:edX+course+run1': {'is_restricted': False}, + 'course-v1:edX+course+run2': {'is_restricted': True}, + }, + 'json_metadata': {'foobar': 'base metadata'}, + 'associate_with_catalog_query': '11111111-1111-1111-1111-111111111111', + }, + }, + 'create_restricted_courses': { + 1: { + 'content_key': 'edX+course', + 'catalog_query': '11111111-1111-1111-1111-111111111111', + 'json_metadata': {'foobar': 'override metadata'}, + }, + }, + 'create_restricted_run_allowed_for_restricted_course': [ + {'course': 1, 'run': 'course-v1:edX+course+run2'}, + ], + }, + """ + catalog_queries = { + cq_uuid: factories.CatalogQueryFactory( + uuid=cq_uuid, + content_filter=cq_info['content_filter'] | {'force_unique': cq_uuid}, + ) for cq_uuid, cq_info in create_catalog_query.items() + } + content_metadata = {} + create_content_metadata = create_content_metadata or {} + for course_key, course_info in create_content_metadata.items(): + course = factories.ContentMetadataFactory( + content_key=course_key, + content_type=COURSE, + _json_metadata=course_info['json_metadata'], + ) + content_metadata.update({course_key: course}) + if cq_uuid := course_info['associate_with_catalog_query']: + course.catalog_queries.set([catalog_queries[cq_uuid]]) + for run_key, run_info in course_info['create_runs'].items(): + run = factories.ContentMetadataFactory( + content_key=run_key, + parent_content_key=course_key, + content_type=COURSE_RUN, + ) + if run_info['is_restricted']: + # pylint: disable=protected-access + run._json_metadata.update({'restriction_type': 'custom-b2b-enterprise'}) + run.save() + content_metadata.update({run_key: run}) + restricted_courses = { + id: factories.RestrictedCourseMetadataFactory( + id=id, + content_key=restricted_course_info['content_key'], + unrestricted_parent=content_metadata[restricted_course_info['content_key']], + catalog_query=catalog_queries[restricted_course_info['catalog_query']], + _json_metadata=restricted_course_info['json_metadata'], + ) for id, restricted_course_info in create_restricted_courses.items() + } if create_restricted_courses else {} + for mapping_info in create_restricted_run_allowed_for_restricted_course or []: + factories.RestrictedRunAllowedForRestrictedCourseFactory( + course=restricted_courses[mapping_info['course']], + run=content_metadata[mapping_info['run']], + ) + main_catalog = factories.EnterpriseCatalogFactory( + catalog_query=catalog_queries['11111111-1111-1111-1111-111111111111'], + ) + return main_catalog, catalog_queries, content_metadata, restricted_courses diff --git a/enterprise_catalog/settings/base.py b/enterprise_catalog/settings/base.py index 10b8d118..f7b1f2e1 100644 --- a/enterprise_catalog/settings/base.py +++ b/enterprise_catalog/settings/base.py @@ -428,6 +428,9 @@ # SHOULD_FETCH_RESTRICTED_COURSE_RUNS) during algolia indexing. SHOULD_INDEX_COURSES_WITH_RESTRICTED_RUNS = False +# Whether to enable v2 of the APIs that surface restricted course (+ runs) content +ENABLE_V2_API = False + # Set up system-to-feature roles mapping for edx-rbac SYSTEM_TO_FEATURE_ROLE_MAPPING = { # The enterprise catalog admin role is for users who need to perform state altering requests on catalogs