From c52717cb7e0223932e1ab7cea2ce7f7d03f3073e Mon Sep 17 00:00:00 2001 From: Alexander Dusenbery Date: Mon, 28 Oct 2024 15:51:28 -0400 Subject: [PATCH] feat: v2 customer content-metadata endpoint ENT-9407 --- enterprise_catalog/apps/api/base/__init__.py | 0 .../apps/api/base/tests/__init__.py | 0 .../base/tests/enterprise_customer_views.py | 73 +++++ .../tests/test_enterprise_customer_views.py | 207 +------------- .../apps/api/v1/views/enterprise_customer.py | 29 +- .../tests/test_enterprise_customer_views.py | 267 ++++++++++++++++++ enterprise_catalog/apps/api/v2/urls.py | 12 +- ...enterprise_catalog_get_content_metadata.py | 5 +- .../apps/api/v2/views/enterprise_customer.py | 25 ++ 9 files changed, 402 insertions(+), 216 deletions(-) create mode 100644 enterprise_catalog/apps/api/base/__init__.py create mode 100644 enterprise_catalog/apps/api/base/tests/__init__.py create mode 100644 enterprise_catalog/apps/api/base/tests/enterprise_customer_views.py create mode 100644 enterprise_catalog/apps/api/v2/tests/test_enterprise_customer_views.py create mode 100644 enterprise_catalog/apps/api/v2/views/enterprise_customer.py diff --git a/enterprise_catalog/apps/api/base/__init__.py b/enterprise_catalog/apps/api/base/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/enterprise_catalog/apps/api/base/tests/__init__.py b/enterprise_catalog/apps/api/base/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/enterprise_catalog/apps/api/base/tests/enterprise_customer_views.py b/enterprise_catalog/apps/api/base/tests/enterprise_customer_views.py new file mode 100644 index 000000000..c14a14c96 --- /dev/null +++ b/enterprise_catalog/apps/api/base/tests/enterprise_customer_views.py @@ -0,0 +1,73 @@ +from rest_framework.reverse import reverse + +from enterprise_catalog.apps.api.v1.tests.mixins import APITestMixin +from enterprise_catalog.apps.catalog.models import ( + CatalogQuery, + ContentMetadata, + EnterpriseCatalog, +) +from enterprise_catalog.apps.catalog.tests.factories import ( + EnterpriseCatalogFactory, +) + + +class BaseEnterpriseCustomerViewSetTests(APITestMixin): + """ + Tests for the EnterpriseCustomerViewSet + """ + VERSION = 'v1' + + def setUp(self): + super().setUp() + # clean up any stale test objects + CatalogQuery.objects.all().delete() + ContentMetadata.objects.all().delete() + EnterpriseCatalog.objects.all().delete() + + self.enterprise_catalog = EnterpriseCatalogFactory(enterprise_uuid=self.enterprise_uuid) + + # Set up catalog.has_learner_access permissions + self.set_up_catalog_learner() + + def tearDown(self): + super().tearDown() + # clean up any stale test objects + CatalogQuery.objects.all().delete() + ContentMetadata.objects.all().delete() + EnterpriseCatalog.objects.all().delete() + + def _get_contains_content_base_url(self, enterprise_uuid=None): + """ + Helper to construct the base url for the contains_content_items endpoint + """ + return reverse( + f'api:{self.VERSION}:enterprise-customer-contains-content-items', + kwargs={'enterprise_uuid': enterprise_uuid or self.enterprise_uuid}, + ) + + def _get_filter_content_base_url(self, enterprise_uuid=None): + """ + Helper to construct the base url for the filter_content_items endpoint + """ + return reverse( + f'api:{self.VERSION}:enterprise-customer-filter-content-items', + kwargs={'enterprise_uuid': enterprise_uuid or self.enterprise_uuid}, + ) + + def _get_generate_diff_base_url(self, enterprise_catalog_uuid=None): + """ + Helper to construct the base url for the catalog `generate_diff` endpoint + """ + return reverse( + f'api:{self.VERSION}:generate-catalog-diff', + kwargs={'uuid': enterprise_catalog_uuid or self.enterprise_catalog.uuid}, + ) + + def _get_content_metadata_base_url(self, enterprise_uuid, content_identifier): + return reverse( + f'api:{self.VERSION}:customer-content-metadata-retrieve', + kwargs={ + 'enterprise_uuid': enterprise_uuid, + 'content_identifier': content_identifier, + }, + ) diff --git a/enterprise_catalog/apps/api/v1/tests/test_enterprise_customer_views.py b/enterprise_catalog/apps/api/v1/tests/test_enterprise_customer_views.py index de738e474..20d00ebab 100644 --- a/enterprise_catalog/apps/api/v1/tests/test_enterprise_customer_views.py +++ b/enterprise_catalog/apps/api/v1/tests/test_enterprise_customer_views.py @@ -2,19 +2,11 @@ from datetime import datetime, timedelta from unittest import mock -import pytest 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 ( - RESTRICTED_RUNS_ALLOWED_KEY, -) -from enterprise_catalog.apps.catalog.models import ( - CatalogQuery, - ContentMetadata, - EnterpriseCatalog, +from enterprise_catalog.apps.api.base.tests.enterprise_customer_views import ( + BaseEnterpriseCustomerViewSetTests, ) from enterprise_catalog.apps.catalog.tests.factories import ( ContentMetadataFactory, @@ -22,57 +14,10 @@ ) -class EnterpriseCustomerViewSetTests(APITestMixin): +class EnterpriseCustomerViewSetTests(BaseEnterpriseCustomerViewSetTests): """ Tests for the EnterpriseCustomerViewSet """ - - def setUp(self): - super().setUp() - # clean up any stale test objects - CatalogQuery.objects.all().delete() - ContentMetadata.objects.all().delete() - EnterpriseCatalog.objects.all().delete() - - self.enterprise_catalog = EnterpriseCatalogFactory(enterprise_uuid=self.enterprise_uuid) - - # Set up catalog.has_learner_access permissions - self.set_up_catalog_learner() - - def tearDown(self): - super().tearDown() - # clean up any stale test objects - CatalogQuery.objects.all().delete() - ContentMetadata.objects.all().delete() - EnterpriseCatalog.objects.all().delete() - - def _get_contains_content_base_url(self, enterprise_uuid=None): - """ - Helper to construct the base url for the contains_content_items endpoint - """ - return reverse( - 'api:v1:enterprise-customer-contains-content-items', - kwargs={'enterprise_uuid': enterprise_uuid or self.enterprise_uuid}, - ) - - def _get_filter_content_base_url(self, enterprise_uuid=None): - """ - Helper to construct the base url for the filter_content_items endpoint - """ - return reverse( - 'api:v1:enterprise-customer-filter-content-items', - kwargs={'enterprise_uuid': enterprise_uuid or self.enterprise_uuid}, - ) - - def _get_generate_diff_base_url(self, enterprise_catalog_uuid=None): - """ - Helper to construct the base url for the catalog `generate_diff` endpoint - """ - return reverse( - 'api:v1:generate-catalog-diff', - kwargs={'uuid': enterprise_catalog_uuid or self.enterprise_catalog.uuid}, - ) - def test_generate_diff_unauthorized_non_catalog_learner(self): """ Verify the generate_diff endpoint rejects users that are not catalog learners @@ -357,152 +302,6 @@ def test_contains_catalog_list_with_catalog_list_param(self): catalog_list = response.json()['catalog_list'] assert set(catalog_list) == {str(second_catalog.uuid)} - @pytest.mark.skip(reason="We need a version of this test for the v2 API.") - def test_contains_catalog_list_with_content_ids_param(self): - """ - Verify the contains_content_items endpoint returns a list of catalogs the course is in if the correct - parameter is passed - """ - content_metadata = ContentMetadataFactory() - self.add_metadata_to_catalog(self.enterprise_catalog, [content_metadata]) - - # Create a two catalogs that have the content we're looking for - content_key = 'fake-key+101x' - second_catalog = EnterpriseCatalogFactory(enterprise_uuid=self.enterprise_uuid) - - relevant_content = ContentMetadataFactory(content_key=content_key) - self.add_metadata_to_catalog(second_catalog, [relevant_content]) - url = self._get_contains_content_base_url() + '?course_run_ids=' + content_key + \ - '&get_catalogs_containing_specified_content_ids=True' - self.assert_correct_contains_response(url, True) - - response = self.client.get(url) - response_payload = response.json() - catalog_list = response_payload['catalog_list'] - assert set(catalog_list) == {str(second_catalog.uuid)} - self.assertIsNone(response_payload['restricted_runs_allowed']) - - @pytest.mark.skip(reason="We need a version of this test for the v2 API.") - def test_contains_catalog_key_restricted_runs_allowed(self): - """ - Tests that, when a course key is requested, we also get a response - describing any child restricted runs that are allowed under that course key - for the customer. - """ - catalog = EnterpriseCatalogFactory(enterprise_uuid=self.enterprise_uuid) - catalog.catalog_query.content_filter[RESTRICTED_RUNS_ALLOWED_KEY] = { - 'org+key1': ['course-v1:org+key1+restrictedrun'] - } - catalog.catalog_query.save() - content_one = ContentMetadataFactory(content_key='org+key1') - content_two = ContentMetadataFactory(content_key='org+key2') - self.add_metadata_to_catalog(catalog, [content_one, content_two]) - - other_catalog = EnterpriseCatalogFactory(enterprise_uuid=self.enterprise_uuid) - other_catalog.catalog_query.content_filter[RESTRICTED_RUNS_ALLOWED_KEY] = { - 'course:org+key3': ['course-v1:org+key3+restrictedrun'], - 'course:org+key4': ['course-v1:org+key4+restrictedrun'] - } - other_catalog.catalog_query.save() - content_three = ContentMetadataFactory(content_key='org+key3') - # created a content record that has a restricted run, - # but which we won't make a request for. - content_four = ContentMetadataFactory(content_key='org+key4') - content_five = ContentMetadataFactory(content_key='org+key5') - self.add_metadata_to_catalog(other_catalog, [content_three, content_four, content_five]) - - # make sure to also request a course key that has no restricted runs, - # and then assert that it is *not* included in the response payload. - url = self._get_contains_content_base_url() + \ - '?course_run_ids=org+key1&course_run_ids=org+key2&course_run_ids=org+key3&get_catalog_list=true' - - response = self.client.get(url) - response_payload = response.json() - - self.assertTrue(response_payload.get('contains_content_items')) - self.assertEqual( - set(response_payload['catalog_list']), - set([str(catalog.uuid), str(other_catalog.uuid)]) - ) - self.assertEqual( - response_payload['restricted_runs_allowed'], - { - 'org+key1': { - 'course-v1:org+key1+restrictedrun': { - 'catalog_uuids': [str(catalog.uuid)] - }, - }, - 'org+key3': { - 'course-v1:org+key3+restrictedrun': { - 'catalog_uuids': [str(other_catalog.uuid)] - }, - }, - } - ) - - @pytest.mark.skip(reason="We need a version of this test for the v2 API.") - def test_restricted_course_disallowed_if_course_not_in_catalog(self): - """ - Tests that a requested course with restricted runs is "disallowed" - if the course is not part of a customer's catalog. - """ - catalog = EnterpriseCatalogFactory(enterprise_uuid=self.enterprise_uuid) - catalog.catalog_query.content_filter[RESTRICTED_RUNS_ALLOWED_KEY] = { - 'org+key1': ['course-v1:org+key1+restrictedrun'] - } - catalog.catalog_query.save() - ContentMetadataFactory(content_key='org+key1') - # don't add this content to the catalog - - url = self._get_contains_content_base_url() + '?course_run_ids=org+key1' - - response = self.client.get(url) - response_payload = response.json() - - self.assertFalse(response_payload.get('contains_content_items')) - self.assertIsNone(response_payload['restricted_runs_allowed']) - - @pytest.mark.skip(reason="We need a version of this test for the v2 API.") - def test_restricted_course_run_allowed_even_if_course_not_in_catalog(self): - """ - Tests that a requested restricted course run is "allowed" - even if the course is not part of a customer's catalog. This is necessary - because typically restricted runs will not have corresponding - `ContentMetadata` records present in the DB, so a lookup via only - `EnterpriseCatalog.contains_content_keys` will fail. We rely - on the restricted run mapping to ascertain the *implicit* inclusion - of a restricted course run in a catalog. - """ - catalog = EnterpriseCatalogFactory(enterprise_uuid=self.enterprise_uuid) - catalog.catalog_query.content_filter[RESTRICTED_RUNS_ALLOWED_KEY] = { - 'org+key1': ['course-v1:org+key1+restrictedrun'] - } - catalog.catalog_query.save() - ContentMetadataFactory(content_key='org+key1') - # don't add this content to the catalog - - url = self._get_contains_content_base_url() + \ - '?course_run_ids=course-v1:org+key1+restrictedrun&get_catalog_list=true' - - response = self.client.get(url) - response_payload = response.json() - - self.assertTrue(response_payload.get('contains_content_items')) - self.assertEqual( - response_payload['catalog_list'], - [str(catalog.uuid)], - ) - self.assertEqual( - response_payload['restricted_runs_allowed'], - { - 'org+key1': { - 'course-v1:org+key1+restrictedrun': { - 'catalog_uuids': [str(catalog.uuid)] - }, - }, - } - ) - def test_contains_catalog_list_parent_key(self): """ Verify the contains_content_items endpoint returns a list of catalogs the course is in diff --git a/enterprise_catalog/apps/api/v1/views/enterprise_customer.py b/enterprise_catalog/apps/api/v1/views/enterprise_customer.py index 1cf6602e2..a36bc89c5 100644 --- a/enterprise_catalog/apps/api/v1/views/enterprise_customer.py +++ b/enterprise_catalog/apps/api/v1/views/enterprise_customer.py @@ -64,6 +64,18 @@ def get_permission_object(self): """ return self.kwargs.get('enterprise_uuid') + def filter_content_keys(self, catalog, content_keys): + return catalog.filter_content_keys(content_keys) + + def contains_content_keys(self, catalog, content_keys): + return catalog.contains_content_keys(content_keys) + + def get_metadata_by_uuid(self, catalog, content_uuid): + return catalog.content_metadata.filter(content_uuid=content_uuid).first() + + def get_metadata_by_content_key(self, catalog, content_key): + return catalog.get_matching_content(content_keys=[content_key]).first() + @method_decorator(require_at_least_one_query_parameter('course_run_ids', 'program_uuids')) @action(detail=True) def contains_content_items(self, request, enterprise_uuid, course_run_ids, program_uuids, **kwargs): @@ -105,9 +117,9 @@ def contains_content_items(self, request, enterprise_uuid, course_run_ids, progr any_catalog_contains_content_items = False catalogs_that_contain_course = [] + content_keys = requested_course_or_run_keys + program_uuids for catalog in customer_catalogs: - contains_content_items = catalog.contains_content_keys(requested_course_or_run_keys + program_uuids) - if contains_content_items: + if self.contains_content_keys(catalog, content_keys): any_catalog_contains_content_items = True if not (get_catalogs_containing_specified_content_ids or get_catalog_list): # Break as soon as we find a catalog that contains the specified content @@ -136,8 +148,7 @@ def filter_content_items(self, request, enterprise_uuid, **kwargs): filtered_content_keys = set() for catalog in customer_catalogs: - items_included = catalog.filter_content_keys(content_keys) - if items_included: + if items_included := self.filter_content_keys(catalog, content_keys): filtered_content_keys = filtered_content_keys.union(items_included) response_data = { @@ -164,19 +175,17 @@ def get_metadata_item_serializer(self): # identifier is a valid UUID. content_uuid = uuid.UUID(content_identifier) for catalog in enterprise_catalogs: - content_with_uuid = catalog.content_metadata.filter(content_uuid=content_uuid) - if content_with_uuid: + if content_with_uuid := self.get_metadata_by_uuid(catalog, content_uuid): return ContentMetadataSerializer( - content_with_uuid.first(), + content_with_uuid, context={'enterprise_catalog': catalog, **serializer_context}, ) except ValueError: # Otherwise, search for matching metadata as a content key for catalog in enterprise_catalogs: - content_with_key = catalog.get_matching_content(content_keys=[content_identifier]) - if content_with_key: + if content_with_key := self.get_metadata_by_content_key(catalog, content_identifier): return ContentMetadataSerializer( - content_with_key.first(), + content_with_key, context={'enterprise_catalog': catalog, **serializer_context}, ) # If we've made it here without finding a matching ContentMetadata record, diff --git a/enterprise_catalog/apps/api/v2/tests/test_enterprise_customer_views.py b/enterprise_catalog/apps/api/v2/tests/test_enterprise_customer_views.py new file mode 100644 index 000000000..52ae8031f --- /dev/null +++ b/enterprise_catalog/apps/api/v2/tests/test_enterprise_customer_views.py @@ -0,0 +1,267 @@ +import uuid +from datetime import datetime, timedelta +from unittest import mock + +import ddt +import pytest +import pytz +from rest_framework import status + +from enterprise_catalog.apps.api.base.tests.enterprise_customer_views import ( + BaseEnterpriseCustomerViewSetTests, +) +from enterprise_catalog.apps.catalog.constants import ( + COURSE, + COURSE_RUN, + RESTRICTED_RUNS_ALLOWED_KEY, +) +from enterprise_catalog.apps.catalog.tests.factories import ( + ContentMetadataFactory, + EnterpriseCatalogFactory, + RestrictedCourseMetadataFactory, + RestrictedRunAllowedForRestrictedCourseFactory, +) +from enterprise_catalog.apps.catalog.utils import localized_utcnow + + +@ddt.ddt +class EnterpriseCustomerViewSetTests(BaseEnterpriseCustomerViewSetTests): + """ + Tests for the EnterpriseCustomerViewSetV2, which is permissive of restricted course/run metadata. + """ + VERSION = 'v2' + + def setUp(self): + super().setUp() + + self.customer_details_patcher = mock.patch( + 'enterprise_catalog.apps.catalog.models.EnterpriseCustomerDetails' + ) + self.mock_customer_details = self.customer_details_patcher.start() + self.NOW = localized_utcnow() + self.mock_customer_details.return_value.last_modified_date = self.NOW + + self.addCleanup(self.customer_details_patcher.stop) + + def test_contains_content_items_unauthorized_non_catalog_learner(self): + """ + Verify the contains_content_items endpoint rejects users that are not catalog learners + """ + self.set_up_invalid_jwt_role() + self.remove_role_assignments() + url = self._get_contains_content_base_url() + '?course_run_ids=fakeX' + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_contains_content_items_unauthorized_incorrect_jwt_context(self): + """ + Verify the contains_content_items endpoint rejects users that are catalog learners + with an incorrect JWT context (i.e., enterprise uuid) + """ + self.remove_role_assignments() + base_url = self._get_contains_content_base_url(enterprise_uuid=uuid.uuid4()) + url = base_url + '?course_run_ids=fakeX' + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_contains_content_items_implicit_access(self): + """ + Verify the contains_content_items endpoint responds with 200 OK for + user with implicit JWT access + """ + self.remove_role_assignments() + url = self._get_contains_content_base_url() + '?program_uuids=fakeX' + self.assert_correct_contains_response(url, False) + + def test_contains_content_items_no_params(self): + """ + Verify the contains_content_items endpoint errors if no parameters are provided + """ + response = self.client.get(self._get_contains_content_base_url()) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_contains_content_items_not_in_catalogs(self): + """ + Verify the contains_content_items endpoint returns False if the content is not in any associated catalog + """ + self.add_metadata_to_catalog(self.enterprise_catalog, [ContentMetadataFactory()]) + + url = self._get_contains_content_base_url() + '?program_uuids=this-is-not-the-uuid-youre-looking-for' + self.assert_correct_contains_response(url, False) + + def test_contains_content_items_in_catalogs(self): + """ + Verify the contains_content_items endpoint returns True if the content is in any associated catalog + """ + content_metadata = ContentMetadataFactory() + self.add_metadata_to_catalog(self.enterprise_catalog, [content_metadata]) + + # Create a second catalog that has the content we're looking for + content_key = 'fake-key+101x' + second_catalog = EnterpriseCatalogFactory(enterprise_uuid=self.enterprise_uuid) + relevant_content = ContentMetadataFactory(content_key=content_key) + self.add_metadata_to_catalog(second_catalog, [relevant_content]) + + url = self._get_contains_content_base_url() + '?course_run_ids=' + content_key + self.assert_correct_contains_response(url, True) + + def _create_restricted_course_and_run(self, catalog): + """ + Helper to setup restricted course and run. + """ + content_one = ContentMetadataFactory(content_key='org+key1', content_type=COURSE) + restricted_course = RestrictedCourseMetadataFactory.create( + content_key='org+key1', + content_type=COURSE, + unrestricted_parent=content_one, + catalog_query=catalog.catalog_query, + ) + restricted_run = ContentMetadataFactory.create( + content_key='course-v1:org+key1+restrictedrun', + content_type=COURSE_RUN, + ) + restricted_course.restricted_run_allowed_for_restricted_course.set( + [restricted_run], clear=True, + ) + return content_one, restricted_course, restricted_run + + def test_contains_catalog_key_restricted_runs_allowed(self): + """ + Tests that a customer is considered to contain a restricted run. + """ + catalog = EnterpriseCatalogFactory(enterprise_uuid=self.enterprise_uuid) + catalog_b = EnterpriseCatalogFactory(enterprise_uuid=self.enterprise_uuid) + + content_one, _, restricted_run = self._create_restricted_course_and_run(catalog) + + self.add_metadata_to_catalog(catalog, [content_one, restricted_run]) + self.add_metadata_to_catalog(catalog_b, [content_one]) + + url = self._get_contains_content_base_url() + \ + f'?course_run_ids={restricted_run.content_key}&get_catalogs_containing_specified_content_ids=true' + + response = self.client.get(url) + response_payload = response.json() + + self.assertTrue(response_payload.get('contains_content_items')) + # catalog_b doesn't contain the restricted run + self.assertEqual(response_payload['catalog_list'], [str(catalog.uuid)]) + + def test_contains_catalog_key_restricted_run_present_but_not_associated_with_catalog(self): + """ + Tests that a customer is not considered to contain a restricted run if the + run exists in the database but is not explicitly linked to the customer's catalog + (and even if the parent course *is* linked to the catalog). + """ + catalog = EnterpriseCatalogFactory(enterprise_uuid=self.enterprise_uuid) + another_customers_catalog = EnterpriseCatalogFactory(enterprise_uuid=str(uuid.uuid4())) + + content_one, _, restricted_run = self._create_restricted_course_and_run(another_customers_catalog) + + self.add_metadata_to_catalog(catalog, [content_one]) + + url = self._get_contains_content_base_url() + \ + f'?course_run_ids={restricted_run.content_key}&get_catalogs_containing_specified_content_ids=true' + + response = self.client.get(url) + response_payload = response.json() + + self.assertFalse(response_payload.get('contains_content_items')) + self.assertEqual(response_payload['catalog_list'], []) + + def test_filter_content_items_restricted_runs_allowed(self): + """ + Tests that restricted runs are filtered in/out. + """ + catalog = EnterpriseCatalogFactory(enterprise_uuid=self.enterprise_uuid) + catalog_b = EnterpriseCatalogFactory(enterprise_uuid=self.enterprise_uuid) + + content_one, _, restricted_run = self._create_restricted_course_and_run(catalog) + + self.add_metadata_to_catalog(catalog, [content_one, restricted_run]) + + # add only the top-level course to catalog B + self.add_metadata_to_catalog(catalog_b, [content_one]) + + url = self._get_filter_content_base_url() + + response = self.client.post(url, data={'content_keys': [restricted_run.content_key]}) + response_payload = response.json() + + self.assertEqual(response_payload['filtered_content_keys'], [restricted_run.content_key]) + + # filtering against only catalog B will have no results + response = self.client.post( + url, + data={ + 'content_keys': [restricted_run.content_key, content_one.content_key], + 'catalog_uuids': [str(catalog_b.uuid)], + }, + ) + response_payload = response.json() + + self.assertEqual( + response_payload['filtered_content_keys'], + [str(content_one.content_key)], + ) + + def test_get_content_metadata_restricted_runs(self): + """ + Tests that we can retrieve restricted content metadata for a customer. + """ + catalog = EnterpriseCatalogFactory(enterprise_uuid=self.enterprise_uuid) + catalog_b = EnterpriseCatalogFactory(enterprise_uuid=self.enterprise_uuid) + + content_one, _, restricted_run = self._create_restricted_course_and_run(catalog) + + self.add_metadata_to_catalog(catalog, [content_one, restricted_run]) + + # add only the top-level course to catalog B + self.add_metadata_to_catalog(catalog_b, [content_one]) + + # Test that we can retrieve the course record + url = self._get_content_metadata_base_url(self.enterprise_uuid, content_one.content_key) + + response_payload = self.client.get(url).json() + self.assertEqual(response_payload['key'], content_one.content_key) + self.assertEqual(response_payload['content_type'], COURSE) + + # Test that we can retrieve the restricted run by key + url = self._get_content_metadata_base_url(self.enterprise_uuid, restricted_run.content_key) + + response_payload = self.client.get(url).json() + self.assertEqual(response_payload['key'], restricted_run.content_key) + self.assertEqual(response_payload['content_type'], COURSE_RUN) + + # Test that we can retrieve the restricted run by uuid + url = self._get_content_metadata_base_url(self.enterprise_uuid, restricted_run.content_uuid) + + response_payload = self.client.get(url).json() + self.assertEqual(response_payload['uuid'], str(restricted_run.content_uuid)) + self.assertEqual(response_payload['content_type'], COURSE_RUN) + + def test_get_content_metadata_restricted_runs_not_found(self): + """ + Tests that when restricted runs are not explicitly linked to a customer's catalog, + they cannot be retrieved. + """ + catalog = EnterpriseCatalogFactory(enterprise_uuid=self.enterprise_uuid) + + content_one, _, restricted_run = self._create_restricted_course_and_run(catalog) + + # don't add the restricted run to the catalog, just the plain, top-level course + self.add_metadata_to_catalog(catalog, [content_one]) + + # Test that we can retrieve the course record + url = self._get_content_metadata_base_url(self.enterprise_uuid, content_one.content_key) + + response_payload = self.client.get(url).json() + self.assertEqual(response_payload['key'], content_one.content_key) + self.assertEqual(response_payload['content_type'], COURSE) + + # Test that we cannot retrieve the restricted run record + url = self._get_content_metadata_base_url(self.enterprise_uuid, restricted_run.content_key) + + response = self.client.get(url) + + self.assertEqual(status.HTTP_404_NOT_FOUND, response.status_code) diff --git a/enterprise_catalog/apps/api/v2/urls.py b/enterprise_catalog/apps/api/v2/urls.py index 0a6350cdb..ab3b841ca 100644 --- a/enterprise_catalog/apps/api/v2/urls.py +++ b/enterprise_catalog/apps/api/v2/urls.py @@ -1,24 +1,34 @@ """ URL definitions for enterprise catalog API version 2. """ -from django.urls import re_path +from django.urls import path, re_path from rest_framework.routers import DefaultRouter from enterprise_catalog.apps.api.v2.views.enterprise_catalog_get_content_metadata import ( EnterpriseCatalogGetContentMetadataV2, ) +from enterprise_catalog.apps.api.v2.views.enterprise_customer import ( + EnterpriseCustomerViewSetV2, +) app_name = 'v2' router = DefaultRouter() +router.register(r'enterprise-customer', EnterpriseCustomerViewSetV2, basename='enterprise-customer') + urlpatterns = [ re_path( r'^enterprise-catalogs/(?P[\S]+)/get_content_metadata', EnterpriseCatalogGetContentMetadataV2.as_view({'get': 'get'}), name='get-content-metadata-v2' ), + path( + 'enterprise-customer//content-metadata//', + EnterpriseCustomerViewSetV2.as_view({'get': 'content_metadata'}), + name='customer-content-metadata-retrieve' + ), ] urlpatterns += router.urls 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 index 520c9b514..5803ca990 100644 --- 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 @@ -1,4 +1,4 @@ -from asyncio.log import logger +import logging from enterprise_catalog.apps.api.v1.views.enterprise_catalog_get_content_metadata import ( EnterpriseCatalogGetContentMetadata, @@ -6,6 +6,9 @@ from enterprise_catalog.apps.api.v2.utils import is_any_course_run_active +logger = logging.getLogger(__name__) + + class EnterpriseCatalogGetContentMetadataV2(EnterpriseCatalogGetContentMetadata): """ View for retrieving all the content metadata associated with a catalog. diff --git a/enterprise_catalog/apps/api/v2/views/enterprise_customer.py b/enterprise_catalog/apps/api/v2/views/enterprise_customer.py new file mode 100644 index 000000000..1f3f27f4d --- /dev/null +++ b/enterprise_catalog/apps/api/v2/views/enterprise_customer.py @@ -0,0 +1,25 @@ +import logging + +from enterprise_catalog.apps.api.v1.views.enterprise_customer import ( + EnterpriseCustomerViewSet, +) + + +logger = logging.getLogger(__name__) + + +class EnterpriseCustomerViewSetV2(EnterpriseCustomerViewSet): + """ + V2 views for content metadata and catalog-content inclusion for retrieving. + """ + def get_metadata_by_uuid(self, catalog, content_uuid): + return catalog.content_metadata_with_restricted.filter(content_uuid=content_uuid).first() + + def get_metadata_by_content_key(self, catalog, content_key): + return catalog.get_matching_content(content_keys=[content_key], include_restricted=True).first() + + def filter_content_keys(self, catalog, content_keys): + return catalog.filter_content_keys(content_keys, include_restricted=True) + + def contains_content_keys(self, catalog, content_keys): + return catalog.contains_content_keys(content_keys, include_restricted=True)