Skip to content

Commit

Permalink
feat: adding v2 of catalog-based get_content_metadata endpoint
Browse files Browse the repository at this point in the history
ENT-9406
  • Loading branch information
pmakwana93 authored and iloveagent57 committed Oct 28, 2024
1 parent 0eb18b0 commit 3cd7e19
Show file tree
Hide file tree
Showing 9 changed files with 708 additions and 0 deletions.
2 changes: 2 additions & 0 deletions enterprise_catalog/apps/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
]
Empty file.
485 changes: 485 additions & 0 deletions enterprise_catalog/apps/api/v2/tests/test_views.py

Large diffs are not rendered by default.

24 changes: 24 additions & 0 deletions enterprise_catalog/apps/api/v2/urls.py
Original file line number Diff line number Diff line change
@@ -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<uuid>[\S]+)/get_content_metadata',
EnterpriseCatalogGetContentMetadataV2.as_view({'get': 'get'}),
name='get-content-metadata-v2'
),
]

urlpatterns += router.urls
53 changes: 53 additions & 0 deletions enterprise_catalog/apps/api/v2/utils.py
Original file line number Diff line number Diff line change
@@ -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
Empty file.
Original file line number Diff line number Diff line change
@@ -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
97 changes: 97 additions & 0 deletions enterprise_catalog/apps/catalog/tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions enterprise_catalog/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 3cd7e19

Please sign in to comment.