Commit 2b4fb4fd by Afzal Wali Naushahi Committed by GitHub

Merge pull request #15180 from edx/clintonb/multi-tenant-catalog-integration

Updated CatalogIntegration to use a site-specific URL
parents dd1f8346 8173182c
...@@ -9,6 +9,7 @@ from datetime import datetime ...@@ -9,6 +9,7 @@ from datetime import datetime
import ddt import ddt
import freezegun import freezegun
import httpretty import httpretty
import waffle
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from mock import patch from mock import patch
...@@ -157,6 +158,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest ...@@ -157,6 +158,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
self.assertEquals(response.status_code, 200) self.assertEquals(response.status_code, 200)
@httpretty.activate @httpretty.activate
@waffle.testutils.override_switch("populate-multitenant-programs", True)
def test_enterprise_learner_context(self): def test_enterprise_learner_context(self):
""" """
Test: Track selection page should show the enterprise context message if user belongs to the Enterprise. Test: Track selection page should show the enterprise context message if user belongs to the Enterprise.
...@@ -177,6 +179,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest ...@@ -177,6 +179,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
) )
@httpretty.activate @httpretty.activate
@waffle.testutils.override_switch("populate-multitenant-programs", True)
def test_enterprise_learner_context_with_multiple_organizations(self): def test_enterprise_learner_context_with_multiple_organizations(self):
""" """
Test: Track selection page should show the enterprise context message with multiple organization names Test: Track selection page should show the enterprise context message with multiple organization names
...@@ -209,6 +212,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest ...@@ -209,6 +212,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
) )
@httpretty.activate @httpretty.activate
@waffle.testutils.override_switch("populate-multitenant-programs", True)
def test_enterprise_learner_context_audit_disabled(self): def test_enterprise_learner_context_audit_disabled(self):
""" """
Track selection page should hide the audit choice by default in an Enterprise Customer/Learner context Track selection page should hide the audit choice by default in an Enterprise Customer/Learner context
......
...@@ -4,15 +4,18 @@ import json ...@@ -4,15 +4,18 @@ import json
import ddt import ddt
import httpretty import httpretty
import waffle
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.test import TestCase from django.test import TestCase
from django.test.utils import override_settings from django.test.utils import override_settings
from oauth2_provider.models import get_application_model from oauth2_provider.models import get_application_model
from openedx.core.djangoapps.api_admin.models import ApiAccessRequest, ApiAccessConfig from openedx.core.djangoapps.api_admin.models import ApiAccessConfig, ApiAccessRequest
from openedx.core.djangoapps.api_admin.tests.factories import ( from openedx.core.djangoapps.api_admin.tests.factories import (
ApiAccessRequestFactory, ApplicationFactory, CatalogFactory ApiAccessRequestFactory,
ApplicationFactory,
CatalogFactory
) )
from openedx.core.djangoapps.api_admin.tests.utils import VALID_DATA from openedx.core.djangoapps.api_admin.tests.utils import VALID_DATA
from openedx.core.djangolib.testing.utils import skip_unless_lms from openedx.core.djangolib.testing.utils import skip_unless_lms
...@@ -263,6 +266,7 @@ class CatalogSearchViewTest(CatalogTest): ...@@ -263,6 +266,7 @@ class CatalogSearchViewTest(CatalogTest):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@httpretty.activate @httpretty.activate
@waffle.testutils.override_switch("populate-multitenant-programs", True)
def test_post(self): def test_post(self):
catalog_user = UserFactory() catalog_user = UserFactory()
self.mock_catalog_endpoint({'results': []}) self.mock_catalog_endpoint({'results': []})
...@@ -285,6 +289,7 @@ class CatalogListViewTest(CatalogTest): ...@@ -285,6 +289,7 @@ class CatalogListViewTest(CatalogTest):
self.url = reverse('api_admin:catalog-list', kwargs={'username': self.catalog_user.username}) self.url = reverse('api_admin:catalog-list', kwargs={'username': self.catalog_user.username})
@httpretty.activate @httpretty.activate
@waffle.testutils.override_switch("populate-multitenant-programs", True)
def test_get(self): def test_get(self):
catalog = CatalogFactory(viewers=[self.catalog_user.username]) catalog = CatalogFactory(viewers=[self.catalog_user.username])
self.mock_catalog_endpoint({'results': [catalog.attributes]}) self.mock_catalog_endpoint({'results': [catalog.attributes]})
...@@ -293,6 +298,7 @@ class CatalogListViewTest(CatalogTest): ...@@ -293,6 +298,7 @@ class CatalogListViewTest(CatalogTest):
self.assertIn(catalog.name, response.content.decode('utf-8')) self.assertIn(catalog.name, response.content.decode('utf-8'))
@httpretty.activate @httpretty.activate
@waffle.testutils.override_switch("populate-multitenant-programs", True)
def test_get_no_catalogs(self): def test_get_no_catalogs(self):
"""Verify that the view works when no catalogs are set up.""" """Verify that the view works when no catalogs are set up."""
self.mock_catalog_endpoint({}, status_code=404) self.mock_catalog_endpoint({}, status_code=404)
...@@ -300,6 +306,7 @@ class CatalogListViewTest(CatalogTest): ...@@ -300,6 +306,7 @@ class CatalogListViewTest(CatalogTest):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@httpretty.activate @httpretty.activate
@waffle.testutils.override_switch("populate-multitenant-programs", True)
def test_post(self): def test_post(self):
catalog_data = { catalog_data = {
'name': 'test-catalog', 'name': 'test-catalog',
...@@ -314,6 +321,7 @@ class CatalogListViewTest(CatalogTest): ...@@ -314,6 +321,7 @@ class CatalogListViewTest(CatalogTest):
self.assertRedirects(response, reverse('api_admin:catalog-edit', kwargs={'catalog_id': catalog_id})) self.assertRedirects(response, reverse('api_admin:catalog-edit', kwargs={'catalog_id': catalog_id}))
@httpretty.activate @httpretty.activate
@waffle.testutils.override_switch("populate-multitenant-programs", True)
def test_post_invalid(self): def test_post_invalid(self):
catalog = CatalogFactory(viewers=[self.catalog_user.username]) catalog = CatalogFactory(viewers=[self.catalog_user.username])
self.mock_catalog_endpoint({'results': [catalog.attributes]}) self.mock_catalog_endpoint({'results': [catalog.attributes]})
...@@ -339,6 +347,7 @@ class CatalogEditViewTest(CatalogTest): ...@@ -339,6 +347,7 @@ class CatalogEditViewTest(CatalogTest):
self.url = reverse('api_admin:catalog-edit', kwargs={'catalog_id': self.catalog.id}) self.url = reverse('api_admin:catalog-edit', kwargs={'catalog_id': self.catalog.id})
@httpretty.activate @httpretty.activate
@waffle.testutils.override_switch("populate-multitenant-programs", True)
def test_get(self): def test_get(self):
self.mock_catalog_endpoint(self.catalog.attributes, catalog_id=self.catalog.id) self.mock_catalog_endpoint(self.catalog.attributes, catalog_id=self.catalog.id)
response = self.client.get(self.url) response = self.client.get(self.url)
...@@ -346,6 +355,7 @@ class CatalogEditViewTest(CatalogTest): ...@@ -346,6 +355,7 @@ class CatalogEditViewTest(CatalogTest):
self.assertIn(self.catalog.name, response.content.decode('utf-8')) self.assertIn(self.catalog.name, response.content.decode('utf-8'))
@httpretty.activate @httpretty.activate
@waffle.testutils.override_switch("populate-multitenant-programs", True)
def test_delete(self): def test_delete(self):
self.mock_catalog_endpoint( self.mock_catalog_endpoint(
self.catalog.attributes, self.catalog.attributes,
...@@ -362,6 +372,7 @@ class CatalogEditViewTest(CatalogTest): ...@@ -362,6 +372,7 @@ class CatalogEditViewTest(CatalogTest):
self.assertEqual(len(httpretty.httpretty.latest_requests), 1) self.assertEqual(len(httpretty.httpretty.latest_requests), 1)
@httpretty.activate @httpretty.activate
@waffle.testutils.override_switch("populate-multitenant-programs", True)
def test_edit(self): def test_edit(self):
self.mock_catalog_endpoint(self.catalog.attributes, method=httpretty.PATCH, catalog_id=self.catalog.id) self.mock_catalog_endpoint(self.catalog.attributes, method=httpretty.PATCH, catalog_id=self.catalog.id)
new_attributes = dict(self.catalog.attributes, **{'delete-catalog': 'off', 'name': 'changed'}) new_attributes = dict(self.catalog.attributes, **{'delete-catalog': 'off', 'name': 'changed'})
...@@ -370,6 +381,7 @@ class CatalogEditViewTest(CatalogTest): ...@@ -370,6 +381,7 @@ class CatalogEditViewTest(CatalogTest):
self.assertRedirects(response, reverse('api_admin:catalog-edit', kwargs={'catalog_id': self.catalog.id})) self.assertRedirects(response, reverse('api_admin:catalog-edit', kwargs={'catalog_id': self.catalog.id}))
@httpretty.activate @httpretty.activate
@waffle.testutils.override_switch("populate-multitenant-programs", True)
def test_edit_invalid(self): def test_edit_invalid(self):
self.mock_catalog_endpoint(self.catalog.attributes, catalog_id=self.catalog.id) self.mock_catalog_endpoint(self.catalog.attributes, catalog_id=self.catalog.id)
new_attributes = dict(self.catalog.attributes, **{'delete-catalog': 'off', 'name': ''}) new_attributes = dict(self.catalog.attributes, **{'delete-catalog': 'off', 'name': ''})
...@@ -389,6 +401,7 @@ class CatalogPreviewViewTest(CatalogTest): ...@@ -389,6 +401,7 @@ class CatalogPreviewViewTest(CatalogTest):
self.url = reverse('api_admin:catalog-preview') self.url = reverse('api_admin:catalog-preview')
@httpretty.activate @httpretty.activate
@waffle.testutils.override_switch("populate-multitenant-programs", True)
def test_get(self): def test_get(self):
data = {'count': 1, 'results': ['test data'], 'next': None, 'prev': None} data = {'count': 1, 'results': ['test data'], 'next': None, 'prev': None}
httpretty.register_uri( httpretty.register_uri(
...@@ -401,6 +414,7 @@ class CatalogPreviewViewTest(CatalogTest): ...@@ -401,6 +414,7 @@ class CatalogPreviewViewTest(CatalogTest):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(json.loads(response.content), data) self.assertEqual(json.loads(response.content), data)
@waffle.testutils.override_switch("populate-multitenant-programs", True)
def test_get_without_query(self): def test_get_without_query(self):
response = self.client.get(self.url) response = self.client.get(self.url)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
......
""" Course Discovery API Service. """
from django.conf import settings
from edx_rest_api_client.client import EdxRestApiClient
from openedx.core.lib.token_utils import JwtBuilder
def course_discovery_api_client(user):
""" Returns a Course Discovery API client setup with authentication for the specified user. """
scopes = ['email', 'profile']
expires_in = settings.OAUTH_ID_TOKEN_EXPIRATION
jwt = JwtBuilder(user).build_token(scopes, expires_in)
return EdxRestApiClient(settings.COURSE_CATALOG_API_URL, jwt=jwt)
...@@ -18,7 +18,7 @@ from edxmako.shortcuts import render_to_response ...@@ -18,7 +18,7 @@ from edxmako.shortcuts import render_to_response
from openedx.core.djangoapps.api_admin.decorators import require_api_access from openedx.core.djangoapps.api_admin.decorators import require_api_access
from openedx.core.djangoapps.api_admin.forms import ApiAccessRequestForm, CatalogForm from openedx.core.djangoapps.api_admin.forms import ApiAccessRequestForm, CatalogForm
from openedx.core.djangoapps.api_admin.models import ApiAccessRequest, Catalog from openedx.core.djangoapps.api_admin.models import ApiAccessRequest, Catalog
from openedx.core.djangoapps.api_admin.utils import course_discovery_api_client from openedx.core.djangoapps.catalog.utils import create_catalog_api_client
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -119,6 +119,11 @@ class ApiTosView(TemplateView): ...@@ -119,6 +119,11 @@ class ApiTosView(TemplateView):
template_name = 'api_admin/terms_of_service.html' template_name = 'api_admin/terms_of_service.html'
class CatalogApiMixin(object):
def get_catalog_api_client(self, user):
return create_catalog_api_client(user)
class CatalogSearchView(View): class CatalogSearchView(View):
"""View to search for catalogs belonging to a user.""" """View to search for catalogs belonging to a user."""
...@@ -135,7 +140,7 @@ class CatalogSearchView(View): ...@@ -135,7 +140,7 @@ class CatalogSearchView(View):
return redirect(reverse('api_admin:catalog-list', kwargs={'username': username})) return redirect(reverse('api_admin:catalog-list', kwargs={'username': username}))
class CatalogListView(View): class CatalogListView(CatalogApiMixin, View):
"""View to list existing catalogs and create new ones.""" """View to list existing catalogs and create new ones."""
template = 'api_admin/catalogs/list.html' template = 'api_admin/catalogs/list.html'
...@@ -162,14 +167,14 @@ class CatalogListView(View): ...@@ -162,14 +167,14 @@ class CatalogListView(View):
def get(self, request, username): def get(self, request, username):
"""Display a list of a user's catalogs.""" """Display a list of a user's catalogs."""
client = course_discovery_api_client(request.user) client = self.get_catalog_api_client(request.user)
form = CatalogForm(initial={'viewers': [username]}) form = CatalogForm(initial={'viewers': [username]})
return render_to_response(self.template, self.get_context_data(client, username, form)) return render_to_response(self.template, self.get_context_data(client, username, form))
def post(self, request, username): def post(self, request, username):
"""Create a new catalog for a user.""" """Create a new catalog for a user."""
form = CatalogForm(request.POST) form = CatalogForm(request.POST)
client = course_discovery_api_client(request.user) client = self.get_catalog_api_client(request.user)
if not form.is_valid(): if not form.is_valid():
return render_to_response(self.template, self.get_context_data(client, username, form), status=400) return render_to_response(self.template, self.get_context_data(client, username, form), status=400)
...@@ -178,7 +183,7 @@ class CatalogListView(View): ...@@ -178,7 +183,7 @@ class CatalogListView(View):
return redirect(reverse('api_admin:catalog-edit', kwargs={'catalog_id': catalog['id']})) return redirect(reverse('api_admin:catalog-edit', kwargs={'catalog_id': catalog['id']}))
class CatalogEditView(View): class CatalogEditView(CatalogApiMixin, View):
"""View to edit an individual catalog.""" """View to edit an individual catalog."""
template_name = 'api_admin/catalogs/edit.html' template_name = 'api_admin/catalogs/edit.html'
...@@ -196,7 +201,7 @@ class CatalogEditView(View): ...@@ -196,7 +201,7 @@ class CatalogEditView(View):
def get(self, request, catalog_id): def get(self, request, catalog_id):
"""Display a form to edit this catalog.""" """Display a form to edit this catalog."""
client = course_discovery_api_client(request.user) client = self.get_catalog_api_client(request.user)
response = client.catalogs(catalog_id).get() response = client.catalogs(catalog_id).get()
catalog = Catalog(attributes=response) catalog = Catalog(attributes=response)
form = CatalogForm(instance=catalog) form = CatalogForm(instance=catalog)
...@@ -204,7 +209,7 @@ class CatalogEditView(View): ...@@ -204,7 +209,7 @@ class CatalogEditView(View):
def post(self, request, catalog_id): def post(self, request, catalog_id):
"""Update or delete this catalog.""" """Update or delete this catalog."""
client = course_discovery_api_client(request.user) client = self.get_catalog_api_client(request.user)
if request.POST.get('delete-catalog') == 'on': if request.POST.get('delete-catalog') == 'on':
client.catalogs(catalog_id).delete() client.catalogs(catalog_id).delete()
return redirect(reverse('api_admin:catalog-search')) return redirect(reverse('api_admin:catalog-search'))
...@@ -217,7 +222,7 @@ class CatalogEditView(View): ...@@ -217,7 +222,7 @@ class CatalogEditView(View):
return redirect(reverse('api_admin:catalog-edit', kwargs={'catalog_id': catalog['id']})) return redirect(reverse('api_admin:catalog-edit', kwargs={'catalog_id': catalog['id']}))
class CatalogPreviewView(View): class CatalogPreviewView(CatalogApiMixin, View):
"""Endpoint to preview courses for a query.""" """Endpoint to preview courses for a query."""
def get(self, request): def get(self, request):
...@@ -225,7 +230,7 @@ class CatalogPreviewView(View): ...@@ -225,7 +230,7 @@ class CatalogPreviewView(View):
Return the results of a query against the course catalog API. If no Return the results of a query against the course catalog API. If no
query parameter is given, returns an empty result set. query parameter is given, returns an empty result set.
""" """
client = course_discovery_api_client(request.user) client = self.get_catalog_api_client(request.user)
# Just pass along the request params including limit/offset pagination # Just pass along the request params including limit/offset pagination
if 'q' in request.GET: if 'q' in request.GET:
results = client.courses.get(**request.GET) results = client.courses.get(**request.GET)
......
...@@ -9,7 +9,6 @@ from openedx.core.djangoapps.catalog.cache import PROGRAM_CACHE_KEY_TPL, PROGRAM ...@@ -9,7 +9,6 @@ from openedx.core.djangoapps.catalog.cache import PROGRAM_CACHE_KEY_TPL, PROGRAM
from openedx.core.djangoapps.catalog.models import CatalogIntegration from openedx.core.djangoapps.catalog.models import CatalogIntegration
from openedx.core.djangoapps.catalog.utils import create_catalog_api_client from openedx.core.djangoapps.catalog.utils import create_catalog_api_client
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
User = get_user_model() # pylint: disable=invalid-name User = get_user_model() # pylint: disable=invalid-name
...@@ -30,7 +29,7 @@ class Command(BaseCommand): ...@@ -30,7 +29,7 @@ class Command(BaseCommand):
try: try:
user = User.objects.get(username=username) user = User.objects.get(username=username)
client = create_catalog_api_client(user, catalog_integration) client = create_catalog_api_client(user)
except User.DoesNotExist: except User.DoesNotExist:
logger.error( logger.error(
'Failed to create API client. Service user {username} does not exist.'.format(username) 'Failed to create API client. Service user {username} does not exist.'.format(username)
......
...@@ -21,7 +21,7 @@ class TestCachePrograms(CatalogIntegrationMixin, CacheIsolationTestCase): ...@@ -21,7 +21,7 @@ class TestCachePrograms(CatalogIntegrationMixin, CacheIsolationTestCase):
self.catalog_integration = self.create_catalog_integration() self.catalog_integration = self.create_catalog_integration()
self.list_url = self.catalog_integration.internal_api_url.rstrip('/') + '/programs/' self.list_url = self.catalog_integration.get_internal_api_url().rstrip('/') + '/programs/'
self.detail_tpl = self.list_url.rstrip('/') + '/{uuid}/' self.detail_tpl = self.list_url.rstrip('/') + '/{uuid}/'
self.programs = ProgramFactory.create_batch(3) self.programs = ProgramFactory.create_batch(3)
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('catalog', '0003_catalogintegration_page_size'),
]
operations = [
migrations.AlterField(
model_name='catalogintegration',
name='internal_api_url',
field=models.URLField(help_text='DEPRECATED: Use the setting COURSE_CATALOG_API_URL.', verbose_name='Internal API URL'),
),
]
"""Models governing integration with the catalog service.""" """Models governing integration with the catalog service."""
import waffle
from config_models.models import ConfigurationModel from config_models.models import ConfigurationModel
from django.conf import settings
from django.contrib.auth import get_user_model
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from openedx.core.djangoapps.site_configuration import helpers
class CatalogIntegration(ConfigurationModel): class CatalogIntegration(ConfigurationModel):
"""Manages configuration for connecting to the catalog service and using its API.""" """Manages configuration for connecting to the catalog service and using its API."""
API_NAME = 'catalog' API_NAME = 'catalog'
CACHE_KEY = 'catalog.api.data' CACHE_KEY = 'catalog.api.data'
# TODO Replace all usages of this field with a call to get_internal_api_url().
internal_api_url = models.URLField( internal_api_url = models.URLField(
verbose_name=_('Internal API URL'), verbose_name=_('Internal API URL'),
help_text=_( help_text=_(
'API root to be used for server-to-server requests (e.g., https://catalog-internal.example.com/api/v1/).' 'DEPRECATED: Use the setting COURSE_CATALOG_API_URL.'
) )
) )
...@@ -47,5 +53,15 @@ class CatalogIntegration(ConfigurationModel): ...@@ -47,5 +53,15 @@ class CatalogIntegration(ConfigurationModel):
"""Whether responses from the catalog API will be cached.""" """Whether responses from the catalog API will be cached."""
return self.cache_ttl > 0 return self.cache_ttl > 0
def __unicode__(self): def get_internal_api_url(self):
return self.internal_api_url """ Returns the internal Catalog API URL associated with the request's site. """
if waffle.switch_is_active("populate-multitenant-programs"):
return helpers.get_value('COURSE_CATALOG_API_URL', settings.COURSE_CATALOG_API_URL)
else:
return self.internal_api_url
def get_service_user(self):
# NOTE: We load the user model here to avoid issues at startup time that result from the hacks
# in lms/startup.py.
User = get_user_model() # pylint: disable=invalid-name
return User.objects.get(username=self.service_username)
"""Catalog model tests.""" """Catalog model tests."""
import ddt import ddt
from django.test import TestCase
import mock import mock
import waffle
from django.test import TestCase, override_settings
from openedx.core.djangoapps.catalog.tests import mixins from openedx.core.djangoapps.catalog.tests import mixins
from openedx.core.djangoapps.site_configuration.tests.test_util import with_site_configuration
COURSE_CATALOG_API_URL = 'https://api.example.com/v1/'
@ddt.ddt @ddt.ddt
...@@ -12,6 +16,11 @@ from openedx.core.djangoapps.catalog.tests import mixins ...@@ -12,6 +16,11 @@ from openedx.core.djangoapps.catalog.tests import mixins
class TestCatalogIntegration(mixins.CatalogIntegrationMixin, TestCase): class TestCatalogIntegration(mixins.CatalogIntegrationMixin, TestCase):
"""Tests covering the CatalogIntegration model.""" """Tests covering the CatalogIntegration model."""
def assert_get_internal_api_url_value(self, expected):
""" Asserts the value of get_internal_api_url matches the expected value. """
catalog_integration = self.create_catalog_integration()
self.assertEqual(catalog_integration.get_internal_api_url(), expected)
@ddt.data( @ddt.data(
(0, False), (0, False),
(1, True), (1, True),
...@@ -21,3 +30,27 @@ class TestCatalogIntegration(mixins.CatalogIntegrationMixin, TestCase): ...@@ -21,3 +30,27 @@ class TestCatalogIntegration(mixins.CatalogIntegrationMixin, TestCase):
"""Test the behavior of the property controlling whether API responses are cached.""" """Test the behavior of the property controlling whether API responses are cached."""
catalog_integration = self.create_catalog_integration(cache_ttl=cache_ttl) catalog_integration = self.create_catalog_integration(cache_ttl=cache_ttl)
self.assertEqual(catalog_integration.is_cache_enabled, is_cache_enabled) self.assertEqual(catalog_integration.is_cache_enabled, is_cache_enabled)
@override_settings(COURSE_CATALOG_API_URL=COURSE_CATALOG_API_URL)
@waffle.testutils.override_switch("populate-multitenant-programs", True)
def test_get_internal_api_url(self, _mock_cache):
""" Requests made without a microsite should return the value from settings. """
self.assert_get_internal_api_url_value(COURSE_CATALOG_API_URL)
catalog_integration = self.create_catalog_integration()
self.assertEqual(catalog_integration.get_internal_api_url(), COURSE_CATALOG_API_URL)
@override_settings(COURSE_CATALOG_API_URL=COURSE_CATALOG_API_URL)
@with_site_configuration(configuration={})
@waffle.testutils.override_switch("populate-multitenant-programs", True)
def test_get_internal_api_url_without_microsite_override(self, _mock_cache):
""" Requests made to microsites that do not have COURSE_CATALOG_API_URL overridden should
return the default value from settings. """
self.assert_get_internal_api_url_value(COURSE_CATALOG_API_URL)
@override_settings(COURSE_CATALOG_API_URL=COURSE_CATALOG_API_URL)
@with_site_configuration(configuration={'COURSE_CATALOG_API_URL': 'foo'})
@waffle.testutils.override_switch("populate-multitenant-programs", True)
def test_get_internal_api_url_with_microsite_override(self, _mock_cache):
""" If a microsite has overridden the value of COURSE_CATALOG_API_URL, the overridden
value should be returned. """
self.assert_get_internal_api_url_value('foo')
...@@ -7,7 +7,7 @@ import ddt ...@@ -7,7 +7,7 @@ import ddt
import mock import mock
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core.cache import cache from django.core.cache import cache
from django.test import TestCase from django.test import TestCase, override_settings
from openedx.core.djangoapps.catalog.cache import PROGRAM_CACHE_KEY_TPL, PROGRAM_UUIDS_CACHE_KEY from openedx.core.djangoapps.catalog.cache import PROGRAM_CACHE_KEY_TPL, PROGRAM_UUIDS_CACHE_KEY
from openedx.core.djangoapps.catalog.models import CatalogIntegration from openedx.core.djangoapps.catalog.models import CatalogIntegration
...@@ -209,6 +209,7 @@ class TestGetProgramsWithType(TestCase): ...@@ -209,6 +209,7 @@ class TestGetProgramsWithType(TestCase):
@mock.patch(UTILS_MODULE + '.get_edx_api_data') @mock.patch(UTILS_MODULE + '.get_edx_api_data')
class TestGetProgramTypes(CatalogIntegrationMixin, TestCase): class TestGetProgramTypes(CatalogIntegrationMixin, TestCase):
"""Tests covering retrieval of program types from the catalog service.""" """Tests covering retrieval of program types from the catalog service."""
@override_settings(COURSE_CATALOG_API_URL='https://api.example.com/v1/')
def test_get_program_types(self, mock_get_edx_api_data): def test_get_program_types(self, mock_get_edx_api_data):
"""Verify get_program_types returns the expected list of program types.""" """Verify get_program_types returns the expected list of program types."""
program_types = ProgramTypeFactory.create_batch(3) program_types = ProgramTypeFactory.create_batch(3)
...@@ -249,7 +250,7 @@ class TestGetCourseRuns(CatalogIntegrationMixin, TestCase): ...@@ -249,7 +250,7 @@ class TestGetCourseRuns(CatalogIntegrationMixin, TestCase):
for arg in (self.catalog_integration, 'course_runs'): for arg in (self.catalog_integration, 'course_runs'):
self.assertIn(arg, args) self.assertIn(arg, args)
self.assertEqual(kwargs['api']._store['base_url'], self.catalog_integration.internal_api_url) # pylint: disable=protected-access self.assertEqual(kwargs['api']._store['base_url'], self.catalog_integration.get_internal_api_url()) # pylint: disable=protected-access
querystring = { querystring = {
'page_size': 20, 'page_size': 20,
......
...@@ -3,8 +3,8 @@ import copy ...@@ -3,8 +3,8 @@ import copy
import logging import logging
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.cache import cache from django.core.cache import cache
from django.core.exceptions import ObjectDoesNotExist
from edx_rest_api_client.client import EdxRestApiClient from edx_rest_api_client.client import EdxRestApiClient
from openedx.core.djangoapps.catalog.cache import PROGRAM_CACHE_KEY_TPL, PROGRAM_UUIDS_CACHE_KEY from openedx.core.djangoapps.catalog.cache import PROGRAM_CACHE_KEY_TPL, PROGRAM_UUIDS_CACHE_KEY
...@@ -13,16 +13,16 @@ from openedx.core.lib.edx_api_utils import get_edx_api_data ...@@ -13,16 +13,16 @@ from openedx.core.lib.edx_api_utils import get_edx_api_data
from openedx.core.lib.token_utils import JwtBuilder from openedx.core.lib.token_utils import JwtBuilder
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
User = get_user_model() # pylint: disable=invalid-name
def create_catalog_api_client(user, catalog_integration): def create_catalog_api_client(user):
"""Returns an API client which can be used to make catalog API requests.""" """Returns an API client which can be used to make Catalog API requests."""
scopes = ['email', 'profile'] scopes = ['email', 'profile']
expires_in = settings.OAUTH_ID_TOKEN_EXPIRATION expires_in = settings.OAUTH_ID_TOKEN_EXPIRATION
jwt = JwtBuilder(user).build_token(scopes, expires_in) jwt = JwtBuilder(user).build_token(scopes, expires_in)
return EdxRestApiClient(catalog_integration.internal_api_url, jwt=jwt) url = CatalogIntegration.current().get_internal_api_url()
return EdxRestApiClient(url, jwt=jwt)
def get_programs(uuid=None): def get_programs(uuid=None):
...@@ -90,11 +90,11 @@ def get_program_types(name=None): ...@@ -90,11 +90,11 @@ def get_program_types(name=None):
catalog_integration = CatalogIntegration.current() catalog_integration = CatalogIntegration.current()
if catalog_integration.enabled: if catalog_integration.enabled:
try: try:
user = User.objects.get(username=catalog_integration.service_username) user = catalog_integration.get_service_user()
except User.DoesNotExist: except ObjectDoesNotExist:
return [] return []
api = create_catalog_api_client(user, catalog_integration) api = create_catalog_api_client(user)
cache_key = '{base}.program_types'.format(base=catalog_integration.CACHE_KEY) cache_key = '{base}.program_types'.format(base=catalog_integration.CACHE_KEY)
data = get_edx_api_data(catalog_integration, 'program_types', api=api, data = get_edx_api_data(catalog_integration, 'program_types', api=api,
...@@ -161,15 +161,15 @@ def get_course_runs(): ...@@ -161,15 +161,15 @@ def get_course_runs():
course_runs = [] course_runs = []
if catalog_integration.enabled: if catalog_integration.enabled:
try: try:
user = User.objects.get(username=catalog_integration.service_username) user = catalog_integration.get_service_user()
except User.DoesNotExist: except ObjectDoesNotExist:
logger.error( logger.error(
'Catalog service user with username [%s] does not exist. Course runs will not be retrieved.', 'Catalog service user with username [%s] does not exist. Course runs will not be retrieved.',
catalog_integration.service_username, catalog_integration.service_username,
) )
return course_runs return course_runs
api = create_catalog_api_client(user, catalog_integration) api = create_catalog_api_client(user)
querystring = { querystring = {
'page_size': catalog_integration.page_size, 'page_size': catalog_integration.page_size,
......
...@@ -4,6 +4,7 @@ import json ...@@ -4,6 +4,7 @@ import json
import httpretty import httpretty
import mock import mock
import waffle
from django.core.cache import cache from django.core.cache import cache
from django.test.utils import override_settings from django.test.utils import override_settings
from edx_oauth2_provider.tests.factories import ClientFactory from edx_oauth2_provider.tests.factories import ClientFactory
...@@ -40,7 +41,7 @@ class TestGetEdxApiData(CatalogIntegrationMixin, CredentialsApiConfigMixin, Cach ...@@ -40,7 +41,7 @@ class TestGetEdxApiData(CatalogIntegrationMixin, CredentialsApiConfigMixin, Cach
def _mock_catalog_api(self, responses, url=None): def _mock_catalog_api(self, responses, url=None):
self.assertTrue(httpretty.is_enabled(), msg='httpretty must be enabled to mock Catalog API calls.') self.assertTrue(httpretty.is_enabled(), msg='httpretty must be enabled to mock Catalog API calls.')
url = url if url else CatalogIntegration.current().internal_api_url.strip('/') + '/programs/' url = url if url else CatalogIntegration.current().get_internal_api_url().strip('/') + '/programs/'
httpretty.register_uri(httpretty.GET, url, responses=responses) httpretty.register_uri(httpretty.GET, url, responses=responses)
...@@ -51,7 +52,7 @@ class TestGetEdxApiData(CatalogIntegrationMixin, CredentialsApiConfigMixin, Cach ...@@ -51,7 +52,7 @@ class TestGetEdxApiData(CatalogIntegrationMixin, CredentialsApiConfigMixin, Cach
def test_get_unpaginated_data(self): def test_get_unpaginated_data(self):
"""Verify that unpaginated data can be retrieved.""" """Verify that unpaginated data can be retrieved."""
catalog_integration = self.create_catalog_integration() catalog_integration = self.create_catalog_integration()
api = create_catalog_api_client(self.user, catalog_integration) api = create_catalog_api_client(self.user)
expected_collection = ['some', 'test', 'data'] expected_collection = ['some', 'test', 'data']
data = { data = {
...@@ -73,13 +74,14 @@ class TestGetEdxApiData(CatalogIntegrationMixin, CredentialsApiConfigMixin, Cach ...@@ -73,13 +74,14 @@ class TestGetEdxApiData(CatalogIntegrationMixin, CredentialsApiConfigMixin, Cach
# Verify the API was actually hit (not the cache) # Verify the API was actually hit (not the cache)
self._assert_num_requests(1) self._assert_num_requests(1)
@waffle.testutils.override_switch("populate-multitenant-programs", True)
def test_get_paginated_data(self): def test_get_paginated_data(self):
"""Verify that paginated data can be retrieved.""" """Verify that paginated data can be retrieved."""
catalog_integration = self.create_catalog_integration() catalog_integration = self.create_catalog_integration()
api = create_catalog_api_client(self.user, catalog_integration) api = create_catalog_api_client(self.user)
expected_collection = ['some', 'test', 'data'] expected_collection = ['some', 'test', 'data']
url = CatalogIntegration.current().internal_api_url.strip('/') + '/programs/?page={}' url = CatalogIntegration.current().get_internal_api_url().strip('/') + '/programs/?page={}'
responses = [] responses = []
for page, record in enumerate(expected_collection, start=1): for page, record in enumerate(expected_collection, start=1):
...@@ -100,14 +102,15 @@ class TestGetEdxApiData(CatalogIntegrationMixin, CredentialsApiConfigMixin, Cach ...@@ -100,14 +102,15 @@ class TestGetEdxApiData(CatalogIntegrationMixin, CredentialsApiConfigMixin, Cach
self._assert_num_requests(len(expected_collection)) self._assert_num_requests(len(expected_collection))
@waffle.testutils.override_switch("populate-multitenant-programs", True)
def test_get_paginated_data_do_not_traverse_pagination(self): def test_get_paginated_data_do_not_traverse_pagination(self):
""" """
Verify that pagination is not traversed if traverse_pagination=False is passed as argument. Verify that pagination is not traversed if traverse_pagination=False is passed as argument.
""" """
catalog_integration = self.create_catalog_integration() catalog_integration = self.create_catalog_integration()
api = create_catalog_api_client(self.user, catalog_integration) api = create_catalog_api_client(self.user)
url = CatalogIntegration.current().internal_api_url.strip('/') + '/programs/?page={}' url = CatalogIntegration.current().get_internal_api_url().strip('/') + '/programs/?page={}'
responses = [ responses = [
{ {
'next': url.format(2), 'next': url.format(2),
...@@ -128,14 +131,15 @@ class TestGetEdxApiData(CatalogIntegrationMixin, CredentialsApiConfigMixin, Cach ...@@ -128,14 +131,15 @@ class TestGetEdxApiData(CatalogIntegrationMixin, CredentialsApiConfigMixin, Cach
self.assertEqual(actual_collection, expected_response) self.assertEqual(actual_collection, expected_response)
self._assert_num_requests(1) self._assert_num_requests(1)
@waffle.testutils.override_switch("populate-multitenant-programs", True)
def test_get_specific_resource(self): def test_get_specific_resource(self):
"""Verify that a specific resource can be retrieved.""" """Verify that a specific resource can be retrieved."""
catalog_integration = self.create_catalog_integration() catalog_integration = self.create_catalog_integration()
api = create_catalog_api_client(self.user, catalog_integration) api = create_catalog_api_client(self.user)
resource_id = 1 resource_id = 1
url = '{api_root}/programs/{resource_id}/'.format( url = '{api_root}/programs/{resource_id}/'.format(
api_root=CatalogIntegration.current().internal_api_url.strip('/'), api_root=CatalogIntegration.current().get_internal_api_url().strip('/'),
resource_id=resource_id, resource_id=resource_id,
) )
...@@ -151,6 +155,7 @@ class TestGetEdxApiData(CatalogIntegrationMixin, CredentialsApiConfigMixin, Cach ...@@ -151,6 +155,7 @@ class TestGetEdxApiData(CatalogIntegrationMixin, CredentialsApiConfigMixin, Cach
self._assert_num_requests(1) self._assert_num_requests(1)
@waffle.testutils.override_switch("populate-multitenant-programs", True)
def test_get_specific_resource_with_falsey_id(self): def test_get_specific_resource_with_falsey_id(self):
""" """
Verify that a specific resource can be retrieved, and pagination parsing is Verify that a specific resource can be retrieved, and pagination parsing is
...@@ -160,11 +165,11 @@ class TestGetEdxApiData(CatalogIntegrationMixin, CredentialsApiConfigMixin, Cach ...@@ -160,11 +165,11 @@ class TestGetEdxApiData(CatalogIntegrationMixin, CredentialsApiConfigMixin, Cach
return the value of that "results" key. return the value of that "results" key.
""" """
catalog_integration = self.create_catalog_integration() catalog_integration = self.create_catalog_integration()
api = create_catalog_api_client(self.user, catalog_integration) api = create_catalog_api_client(self.user)
resource_id = 0 resource_id = 0
url = '{api_root}/programs/{resource_id}/'.format( url = '{api_root}/programs/{resource_id}/'.format(
api_root=CatalogIntegration.current().internal_api_url.strip('/'), api_root=CatalogIntegration.current().get_internal_api_url().strip('/'),
resource_id=resource_id, resource_id=resource_id,
) )
...@@ -180,10 +185,11 @@ class TestGetEdxApiData(CatalogIntegrationMixin, CredentialsApiConfigMixin, Cach ...@@ -180,10 +185,11 @@ class TestGetEdxApiData(CatalogIntegrationMixin, CredentialsApiConfigMixin, Cach
self._assert_num_requests(1) self._assert_num_requests(1)
@waffle.testutils.override_switch("populate-multitenant-programs", True)
def test_cache_utilization(self): def test_cache_utilization(self):
"""Verify that when enabled, the cache is used.""" """Verify that when enabled, the cache is used."""
catalog_integration = self.create_catalog_integration(cache_ttl=5) catalog_integration = self.create_catalog_integration(cache_ttl=5)
api = create_catalog_api_client(self.user, catalog_integration) api = create_catalog_api_client(self.user)
expected_collection = ['some', 'test', 'data'] expected_collection = ['some', 'test', 'data']
data = { data = {
...@@ -197,7 +203,7 @@ class TestGetEdxApiData(CatalogIntegrationMixin, CredentialsApiConfigMixin, Cach ...@@ -197,7 +203,7 @@ class TestGetEdxApiData(CatalogIntegrationMixin, CredentialsApiConfigMixin, Cach
resource_id = 1 resource_id = 1
url = '{api_root}/programs/{resource_id}/'.format( url = '{api_root}/programs/{resource_id}/'.format(
api_root=CatalogIntegration.current().internal_api_url.strip('/'), api_root=CatalogIntegration.current().get_internal_api_url().strip('/'),
resource_id=resource_id, resource_id=resource_id,
) )
...@@ -240,7 +246,7 @@ class TestGetEdxApiData(CatalogIntegrationMixin, CredentialsApiConfigMixin, Cach ...@@ -240,7 +246,7 @@ class TestGetEdxApiData(CatalogIntegrationMixin, CredentialsApiConfigMixin, Cach
def test_data_retrieval_failure(self, mock_exception): def test_data_retrieval_failure(self, mock_exception):
"""Verify that an exception is logged when data can't be retrieved.""" """Verify that an exception is logged when data can't be retrieved."""
catalog_integration = self.create_catalog_integration() catalog_integration = self.create_catalog_integration()
api = create_catalog_api_client(self.user, catalog_integration) api = create_catalog_api_client(self.user)
self._mock_catalog_api( self._mock_catalog_api(
[httpretty.Response(body='clunk', content_type='application/json', status_code=500)] [httpretty.Response(body='clunk', content_type='application/json', status_code=500)]
...@@ -271,7 +277,7 @@ class TestGetEdxApiData(CatalogIntegrationMixin, CredentialsApiConfigMixin, Cach ...@@ -271,7 +277,7 @@ class TestGetEdxApiData(CatalogIntegrationMixin, CredentialsApiConfigMixin, Cach
def test_data_retrieval_failure_with_id(self, mock_exception): def test_data_retrieval_failure_with_id(self, mock_exception):
"""Verify that an exception is logged when data can't be retrieved.""" """Verify that an exception is logged when data can't be retrieved."""
catalog_integration = self.create_catalog_integration() catalog_integration = self.create_catalog_integration()
api = create_catalog_api_client(self.user, catalog_integration) api = create_catalog_api_client(self.user)
self._mock_catalog_api( self._mock_catalog_api(
[httpretty.Response(body='clunk', content_type='application/json', status_code=500)] [httpretty.Response(body='clunk', content_type='application/json', status_code=500)]
......
...@@ -18,8 +18,8 @@ from edx_rest_api_client.client import EdxRestApiClient ...@@ -18,8 +18,8 @@ from edx_rest_api_client.client import EdxRestApiClient
from requests.exceptions import ConnectionError, Timeout from requests.exceptions import ConnectionError, Timeout
from slumber.exceptions import HttpClientError, HttpServerError, SlumberBaseException from slumber.exceptions import HttpClientError, HttpServerError, SlumberBaseException
from openedx.core.djangoapps.api_admin.utils import course_discovery_api_client
from openedx.core.djangoapps.catalog.models import CatalogIntegration from openedx.core.djangoapps.catalog.models import CatalogIntegration
from openedx.core.djangoapps.catalog.utils import create_catalog_api_client
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.lib.token_utils import JwtBuilder from openedx.core.lib.token_utils import JwtBuilder
...@@ -31,7 +31,6 @@ try: ...@@ -31,7 +31,6 @@ try:
except ImportError: except ImportError:
pass pass
CONSENT_FAILED_PARAMETER = 'consent_failed' CONSENT_FAILED_PARAMETER = 'consent_failed'
LOGGER = logging.getLogger("edx.enterprise_helpers") LOGGER = logging.getLogger("edx.enterprise_helpers")
...@@ -201,6 +200,7 @@ def data_sharing_consent_required(view_func): ...@@ -201,6 +200,7 @@ def data_sharing_consent_required(view_func):
After granting consent, the user will be redirected back to the original request.path. After granting consent, the user will be redirected back to the original request.path.
""" """
@wraps(view_func) @wraps(view_func)
def inner(request, course_id, *args, **kwargs): def inner(request, course_id, *args, **kwargs):
""" """
...@@ -462,7 +462,7 @@ def is_course_in_enterprise_catalog(site, course_id, enterprise_catalog_id): ...@@ -462,7 +462,7 @@ def is_course_in_enterprise_catalog(site, course_id, enterprise_catalog_id):
try: try:
# GET: /api/v1/catalogs/{catalog_id}/contains?course_run_id={course_run_ids} # GET: /api/v1/catalogs/{catalog_id}/contains?course_run_id={course_run_ids}
response = course_discovery_api_client(user=user).catalogs(enterprise_catalog_id).contains.get( response = create_catalog_api_client(user=user).catalogs(enterprise_catalog_id).contains.get(
course_run_id=course_id course_run_id=course_id
) )
cache.set(cache_key, response, settings.COURSES_API_CACHE_TIMEOUT) cache.set(cache_key, response, settings.COURSES_API_CACHE_TIMEOUT)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment