-
Notifications
You must be signed in to change notification settings - Fork 19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: v2 catalog contains_content_items view #994
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
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 BaseEnterpriseCatalogViewSetTests(APITestMixin): | ||
""" | ||
Base tests for EnterpriseCatalog view sets. | ||
""" | ||
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, catalog_uuid=None): | ||
""" | ||
Helper to construct the base url for the catalog contains_content_items endpoint | ||
""" | ||
return reverse( | ||
f'api:{self.VERSION}:enterprise-catalog-content-contains-content-items', | ||
kwargs={'uuid': catalog_uuid or self.enterprise_catalog.uuid}, | ||
) |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,157 @@ | ||||||||||||||||||||||||||||||||||||||||||||
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_catalog_views import ( | ||||||||||||||||||||||||||||||||||||||||||||
BaseEnterpriseCatalogViewSetTests, | ||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||
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 EnterpriseCatalogContainsContentItemsTests(BaseEnterpriseCatalogViewSetTests): | ||||||||||||||||||||||||||||||||||||||||||||
""" | ||||||||||||||||||||||||||||||||||||||||||||
Tests for the EnterpriseCatalogViewSetV2, 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) | ||||||||||||||||||||||||||||||||||||||||||||
""" | ||||||||||||||||||||||||||||||||||||||||||||
other_customer_catalog = EnterpriseCatalogFactory(enterprise_uuid=uuid.uuid4()) | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
base_url = self._get_contains_content_base_url(other_customer_catalog.uuid) | ||||||||||||||||||||||||||||||||||||||||||||
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_key = 'fake-key+101x' | ||||||||||||||||||||||||||||||||||||||||||||
relevant_content = ContentMetadataFactory(content_key=content_key) | ||||||||||||||||||||||||||||||||||||||||||||
self.add_metadata_to_catalog(self.enterprise_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, | ||||||||||||||||||||||||||||||||||||||||||||
_json_metadata=content_one.json_metadata, | ||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||
restricted_run = ContentMetadataFactory.create( | ||||||||||||||||||||||||||||||||||||||||||||
content_key='course-v1:org+key1+restrictedrun', | ||||||||||||||||||||||||||||||||||||||||||||
parent_content_key=restricted_course.content_key, | ||||||||||||||||||||||||||||||||||||||||||||
content_type=COURSE_RUN, | ||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||
restricted_course.restricted_run_allowed_for_restricted_course.set( | ||||||||||||||||||||||||||||||||||||||||||||
[restricted_run], clear=True, | ||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||
catalog.catalog_query.content_filter[RESTRICTED_RUNS_ALLOWED_KEY] = { | ||||||||||||||||||||||||||||||||||||||||||||
content_one.content_key: [restricted_run.content_key], | ||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: the top-level key under the restricted_runs_allowed key should actually be an aggregation key, which means you should prefix this with "course:". It's merely a formality for this test suite, but I think important for people who might ever need to touch this test or refer to it as a code reference. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It can actually be a regular course key or an aggregation key, the code is flexible about it: enterprise-catalog/enterprise_catalog/apps/catalog/models.py Lines 117 to 137 in 5425510
|
||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||
catalog.catalog_query.save() | ||||||||||||||||||||||||||||||||||||||||||||
return content_one, restricted_course, restricted_run | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
def test_contains_catalog_key_restricted_runs_allowed(self): | ||||||||||||||||||||||||||||||||||||||||||||
""" | ||||||||||||||||||||||||||||||||||||||||||||
Tests that a catalog is considered to contain a restricted run, | ||||||||||||||||||||||||||||||||||||||||||||
and that a different catalog that does *not* allow the restricted run | ||||||||||||||||||||||||||||||||||||||||||||
is not considered to contain it. | ||||||||||||||||||||||||||||||||||||||||||||
""" | ||||||||||||||||||||||||||||||||||||||||||||
other_catalog = EnterpriseCatalogFactory(enterprise_uuid=self.enterprise_uuid) | ||||||||||||||||||||||||||||||||||||||||||||
content_one, _, restricted_run = self._create_restricted_course_and_run(other_catalog) | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
self.add_metadata_to_catalog(self.enterprise_catalog, [content_one]) | ||||||||||||||||||||||||||||||||||||||||||||
self.add_metadata_to_catalog(other_catalog, [content_one]) | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
url = self._get_contains_content_base_url(other_catalog.uuid) + \ | ||||||||||||||||||||||||||||||||||||||||||||
f'?course_run_ids={restricted_run.content_key}' | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
response = self.client.get(url) | ||||||||||||||||||||||||||||||||||||||||||||
response_payload = response.json() | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
self.assertTrue(response_payload.get('contains_content_items')) | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
# self.enterprise_catalog does not contain the restricted run. | ||||||||||||||||||||||||||||||||||||||||||||
url = self._get_contains_content_base_url(self.enterprise_catalog) + \ | ||||||||||||||||||||||||||||||||||||||||||||
f'?course_run_ids={restricted_run.content_key}' | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
response = self.client.get(url) | ||||||||||||||||||||||||||||||||||||||||||||
response_payload = response.json() | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
self.assertFalse(response_payload.get('contains_content_items')) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import logging | ||
|
||
from enterprise_catalog.apps.api.v1.views.enterprise_catalog_contains_content_items import ( | ||
EnterpriseCatalogContainsContentItems, | ||
) | ||
|
||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
class EnterpriseCatalogContainsContentItemsV2(EnterpriseCatalogContainsContentItems): | ||
""" | ||
Viewset to indicate if given content keys are contained by a catalog, with | ||
restricted content taken into account. | ||
""" | ||
def catalog_contains_content_items(self, content_keys): | ||
""" | ||
Returns a boolean indicating whether all of the provided content_keys | ||
are contained by the catalog record associated with the current request. | ||
Takes restricted content into account. | ||
""" | ||
enterprise_catalog = self.get_object() | ||
return enterprise_catalog.contains_content_keys(content_keys, include_restricted=True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice refactor 👍