Commit 51815136 by Ahsan Ulhaq

Show message for earned programs credentials

ECOM-3015
parent 8a280559
......@@ -21,6 +21,8 @@ class TestProgramListing(ProgramsApiConfigMixin, ProgramsDataMixin, SharedModule
ClientFactory(name=ProgramsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL)
self.create_programs_config()
self.staff = UserFactory(is_staff=True)
self.client.login(username=self.staff.username, password='test')
......@@ -29,7 +31,7 @@ class TestProgramListing(ProgramsApiConfigMixin, ProgramsDataMixin, SharedModule
@httpretty.activate
def test_programs_config_disabled(self):
"""Verify that the programs tab and creation button aren't rendered when config is disabled."""
self.create_config(enable_studio_tab=False)
self.create_programs_config(enable_studio_tab=False)
self.mock_programs_api()
response = self.client.get(self.studio_home)
......@@ -48,7 +50,6 @@ class TestProgramListing(ProgramsApiConfigMixin, ProgramsDataMixin, SharedModule
student = UserFactory(is_staff=False)
self.client.login(username=student.username, password='test')
self.create_config()
self.mock_programs_api()
response = self.client.get(self.studio_home)
......@@ -57,7 +58,6 @@ class TestProgramListing(ProgramsApiConfigMixin, ProgramsDataMixin, SharedModule
@httpretty.activate
def test_programs_displayed(self):
"""Verify that the programs tab and creation button can be rendered when config is enabled."""
self.create_config()
# When no data is provided, expect creation prompt.
self.mock_programs_api(data={'results': []})
......@@ -102,7 +102,7 @@ class TestProgramAuthoringView(ProgramsApiConfigMixin, SharedModuleStoreTestCase
def test_authoring_header(self):
"""Verify that the header contains the expected text."""
self.client.login(username=self.staff.username, password='test')
self.create_config()
self.create_programs_config()
response = self._assert_status(200)
self.assertIn("Program Administration", response.content)
......@@ -116,7 +116,7 @@ class TestProgramAuthoringView(ProgramsApiConfigMixin, SharedModuleStoreTestCase
self._assert_status(404)
# Enable Programs authoring interface
self.create_config()
self.create_programs_config()
student = UserFactory(is_staff=False)
self.client.login(username=student.username, password='test')
......@@ -134,13 +134,13 @@ class TestProgramsIdTokenView(ProgramsApiConfigMixin, SharedModuleStoreTestCase)
def test_config_disabled(self):
"""Ensure the endpoint returns 404 when Programs authoring is disabled."""
self.create_config(enable_studio_tab=False)
self.create_programs_config(enable_studio_tab=False)
response = self.client.get(self.path)
self.assertEqual(response.status_code, 404)
def test_not_logged_in(self):
"""Ensure the endpoint denies access to unauthenticated users."""
self.create_config()
self.create_programs_config()
self.client.logout()
response = self.client.get(self.path)
self.assertEqual(response.status_code, 302)
......@@ -152,7 +152,7 @@ class TestProgramsIdTokenView(ProgramsApiConfigMixin, SharedModuleStoreTestCase)
Ensure the endpoint responds with a valid JSON payload when authoring
is enabled.
"""
self.create_config()
self.create_programs_config()
response = self.client.get(self.path)
self.assertEqual(response.status_code, 200)
payload = json.loads(response.content)
......
......@@ -1035,7 +1035,7 @@ class DashboardTestXSeriesPrograms(ModuleStoreTestCase, ProgramsApiConfigMixin):
CourseEnrollment.enroll(self.user, self.course_2.id, mode=course_mode)
self.client.login(username="jack", password="test")
self.create_config()
self.create_programs_config()
with patch('student.views.get_programs_for_dashboard') as mock_data:
mock_data.return_value = self._create_program_data(
......@@ -1068,7 +1068,7 @@ class DashboardTestXSeriesPrograms(ModuleStoreTestCase, ProgramsApiConfigMixin):
CourseEnrollment.enroll(self.user, self.course_1.id, mode='verified')
self.client.login(username="jack", password="test")
self.create_config()
self.create_programs_config()
with patch(
'student.views.get_programs_for_dashboard',
......@@ -1098,7 +1098,7 @@ class DashboardTestXSeriesPrograms(ModuleStoreTestCase, ProgramsApiConfigMixin):
CourseEnrollment.enroll(self.user, self.course_3.id, mode='honor')
self.client.login(username="jack", password="test")
self.create_config()
self.create_programs_config()
with patch('student.views.get_programs_for_dashboard') as mock_data:
mock_data.return_value = self._create_program_data(
......@@ -1119,7 +1119,7 @@ class DashboardTestXSeriesPrograms(ModuleStoreTestCase, ProgramsApiConfigMixin):
CourseEnrollment.enroll(self.user, self.course_1.id)
self.client.login(username="jack", password="test")
self.create_config()
self.create_programs_config()
program_data = self._create_program_data([(self.course_1.id, 'active')])
if key_remove and key_remove in program_data[unicode(self.course_1.id)]:
......
......@@ -123,6 +123,7 @@ from eventtracking import tracker
from notification_prefs.views import enable_notifications
# Note that this lives in openedx, so this dependency should be refactored.
from openedx.core.djangoapps.credentials.utils import get_user_program_credentials
from openedx.core.djangoapps.user_api.preferences import api as preferences_api
from openedx.core.djangoapps.programs.utils import get_programs_for_dashboard
......@@ -609,6 +610,7 @@ def dashboard(request):
# This is passed along in the template context to allow rendering of
# program-related information on the dashboard.
course_programs = _get_course_programs(user, [enrollment.course_id for enrollment in course_enrollments])
xseries_credentials = _get_xseries_credentials(user)
# Construct a dictionary of course mode information
# used to render the course list. We re-use the course modes dict
......@@ -732,6 +734,7 @@ def dashboard(request):
'nav_hidden': True,
'course_programs': course_programs,
'disable_courseware_js': True,
'xseries_credentials': xseries_credentials,
}
return render_to_response('dashboard.html', context)
......@@ -2410,3 +2413,34 @@ def _get_course_programs(user, user_enrolled_courses): # pylint: disable=invali
log.warning('Program structure is invalid, skipping display: %r', program)
return programs_data
def _get_xseries_credentials(user):
"""Return program credentials data required for display on
the learner dashboard.
Given a user, find all programs for which certificates have been earned
and return list of dictionaries of required program data.
Arguments:
user (User): user object for getting programs credentials.
Returns:
list of dict, containing data corresponding to the programs for which
the user has been awarded a credential.
"""
programs_credentials = get_user_program_credentials(user)
credentials_data = []
for program in programs_credentials:
if program.get('status') == 'active':
try:
program_data = {
'display_name': program['name'],
'subtitle': program['subtitle'],
'credential_url': program['credential_url'],
}
credentials_data.append(program_data)
except KeyError:
log.warning('Program structure is invalid: %r', program)
return credentials_data
......@@ -32,6 +32,32 @@
}
}
.wrapper-xseries-certificates{
@include float(right);
@include margin-left(flex-gutter());
width: flex-grid(3);
.title{
@extend %t-title7;
@extend %t-weight4;
}
ul{
@include padding-left(0);
margin-top: ($baseline/2);
}
li{
@include line-height(20);
list-style-type: none;
}
.copy {
@extend %t-copy-sub1;
margin-top: ($baseline/2);
}
}
.profile-sidebar {
background: transparent;
@include float(right);
......
......@@ -172,6 +172,19 @@ import json
</ul>
</section>
</section>
% if xseries_credentials:
<div class="wrapper-xseries-certificates">
<p class="title">${_("XSeries Program Certificates")}</p>
<p class="copy">${_("You have received a certificate for the following XSeries programs:")}</p>
<ul>
% for xseries_credential in xseries_credentials:
<li>
<a class="copy" href="${xseries_credential['credential_url']}">${xseries_credential['display_name']}</a>
</li>
% endfor
</ul>
</div>
% endif
</section>
<section id="email-settings-modal" class="modal" aria-hidden="true">
......
......@@ -15,6 +15,9 @@ class CredentialsApiConfig(ConfigurationModel):
Manages configuration for connecting to the Credential service and using its
API.
"""
OAUTH2_CLIENT_NAME = 'credentials'
API_NAME = 'credentials'
internal_service_url = models.URLField(verbose_name=_("Internal Service URL"))
public_service_url = models.URLField(verbose_name=_("Public Service URL"))
......
"""Mixins for use during testing."""
import json
import httpretty
from openedx.core.djangoapps.credentials.models import CredentialsApiConfig
......@@ -6,7 +9,7 @@ from openedx.core.djangoapps.credentials.models import CredentialsApiConfig
class CredentialsApiConfigMixin(object):
""" Utilities for working with Credentials configuration during testing."""
DEFAULTS = {
CREDENTIALS_DEFAULTS = {
'enabled': True,
'internal_service_url': 'http://internal.credentials.org/',
'public_service_url': 'http://public.credentials.org/',
......@@ -14,11 +17,139 @@ class CredentialsApiConfigMixin(object):
'enable_studio_authoring': True,
}
def create_config(self, **kwargs):
def create_credentials_config(self, **kwargs):
""" Creates a new CredentialsApiConfig with DEFAULTS, updated with any
provided overrides.
"""
fields = dict(self.DEFAULTS, **kwargs)
fields = dict(self.CREDENTIALS_DEFAULTS, **kwargs)
CredentialsApiConfig(**fields).save()
return CredentialsApiConfig.current()
class CredentialsDataMixin(object):
"""Mixin mocking Credentials API URLs and providing fake data for testing."""
CREDENTIALS_API_RESPONSE = {
"next": None,
"results": [
{
"id": 1,
"username": "test",
"credential": {
"credential_id": 1,
"program_id": 1
},
"status": "awarded",
"uuid": "dummy-uuid-1"
},
{
"id": 2,
"username": "test",
"credential": {
"credential_id": 2,
"program_id": 2
},
"status": "awarded",
"uuid": "dummy-uuid-2"
},
{
"id": 3,
"username": "test",
"credential": {
"credential_id": 3,
"program_id": 3
},
"status": "revoked",
"uuid": "dummy-uuid-3"
},
{
"id": 4,
"username": "test",
"credential": {
"course_id": "edx/test01/2015",
"credential_id": 4,
"certificate_type": "honor"
},
"status": "awarded",
"uuid": "dummy-uuid-4"
},
{
"id": 5,
"username": "test",
"credential": {
"course_id": "edx/test02/2015",
"credential_id": 5,
"certificate_type": "verified"
},
"status": "awarded",
"uuid": "dummy-uuid-5"
},
{
"id": 6,
"username": "test",
"credential": {
"course_id": "edx/test03/2015",
"credential_id": 6,
"certificate_type": "honor"
},
"status": "revoked",
"uuid": "dummy-uuid-6"
}
]
}
CREDENTIALS_NEXT_API_RESPONSE = {
"next": 'next_page_url',
"results": [
{
"id": 7,
"username": "test",
"credential": {
"credential_id": 7,
"program_id": 7
},
"status": "awarded",
"uuid": "dummy-uuid-7"
},
{
"id": 8,
"username": "test",
"credential": {
"credential_id": 8,
"program_id": 8
},
"status": "awarded",
"uuid": "dummy-uuid-8"
}
]
}
def mock_credentials_api(self, user, data=None, status_code=200, reset_url=True, is_next_page=False):
"""Utility for mocking out Credentials API URLs."""
self.assertTrue(httpretty.is_enabled(), msg='httpretty must be enabled to mock Credentials API calls.')
internal_api_url = CredentialsApiConfig.current().internal_api_url.strip('/')
url = internal_api_url + '/user_credentials/?username=' + user.username
if reset_url:
httpretty.reset()
if data is None:
data = self.CREDENTIALS_API_RESPONSE
body = json.dumps(data)
if is_next_page:
next_page_data = self.CREDENTIALS_NEXT_API_RESPONSE
next_page_body = json.dumps(next_page_data)
next_page_url = internal_api_url + '/user_credentials/?page=2&username=' + user.username
httpretty.register_uri(
httpretty.GET, next_page_url, body=body, content_type='application/json', status=status_code
)
httpretty.register_uri(
httpretty.GET, url, body=next_page_body, content_type='application/json', status=status_code
)
else:
httpretty.register_uri(
httpretty.GET, url, body=body, content_type='application/json', status=status_code
)
......@@ -8,7 +8,7 @@ class TestCredentialsApiConfig(CredentialsApiConfigMixin, TestCase):
"""Tests covering the CredentialsApiConfig model."""
def test_url_construction(self):
"""Verify that URLs returned by the model are constructed correctly."""
credentials_config = self.create_config()
credentials_config = self.create_credentials_config()
self.assertEqual(
credentials_config.internal_api_url,
......@@ -23,13 +23,13 @@ class TestCredentialsApiConfig(CredentialsApiConfigMixin, TestCase):
Verify that the property controlling display on the student dashboard is only True
when configuration is enabled and all required configuration is provided.
"""
credentials_config = self.create_config(enabled=False)
credentials_config = self.create_credentials_config(enabled=False)
self.assertFalse(credentials_config.is_learner_issuance_enabled)
credentials_config = self.create_config(enable_learner_issuance=False)
credentials_config = self.create_credentials_config(enable_learner_issuance=False)
self.assertFalse(credentials_config.is_learner_issuance_enabled)
credentials_config = self.create_config()
credentials_config = self.create_credentials_config()
self.assertTrue(credentials_config.is_learner_issuance_enabled)
def test_is_studio_authoring_enabled(self):
......@@ -37,11 +37,11 @@ class TestCredentialsApiConfig(CredentialsApiConfigMixin, TestCase):
Verify that the property controlling display in the Studio authoring is only True
when configuration is enabled and all required configuration is provided.
"""
credentials_config = self.create_config(enabled=False)
credentials_config = self.create_credentials_config(enabled=False)
self.assertFalse(credentials_config.is_studio_authoring_enabled)
credentials_config = self.create_config(enable_studio_authoring=False)
credentials_config = self.create_credentials_config(enable_studio_authoring=False)
self.assertFalse(credentials_config.is_studio_authoring_enabled)
credentials_config = self.create_config()
credentials_config = self.create_credentials_config()
self.assertTrue(credentials_config.is_studio_authoring_enabled)
"""Tests covering Credentials utilities."""
from django.test import TestCase
import httpretty
from oauth2_provider.tests.factories import ClientFactory
from provider.constants import CONFIDENTIAL
from openedx.core.djangoapps.credentials.tests.mixins import CredentialsApiConfigMixin, CredentialsDataMixin
from openedx.core.djangoapps.credentials.models import CredentialsApiConfig
from openedx.core.djangoapps.credentials.utils import (
get_user_credentials, get_user_program_credentials
)
from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin, ProgramsDataMixin
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from student.tests.factories import UserFactory
class TestCredentialsRetrieval(ProgramsApiConfigMixin, CredentialsApiConfigMixin, CredentialsDataMixin,
ProgramsDataMixin, TestCase):
""" Tests covering the retrieval of user credentials from the Credentials
service.
"""
def setUp(self):
super(TestCredentialsRetrieval, self).setUp()
ClientFactory(name=CredentialsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL)
ClientFactory(name=ProgramsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL)
self.user = UserFactory()
@httpretty.activate
def test_get_user_credentials(self):
"""Verify user credentials data can be retrieve."""
self.create_credentials_config()
self.mock_credentials_api(self.user)
actual = get_user_credentials(self.user)
self.assertEqual(actual, self.CREDENTIALS_API_RESPONSE['results'])
def test_get_user_program_credentials_issuance_disable(self):
"""Verify that user program credentials cannot be retrieved if issuance is disabled."""
self.create_credentials_config(enable_learner_issuance=False)
actual = get_user_program_credentials(self.user)
self.assertEqual(actual, [])
@httpretty.activate
def test_get_user_program_credentials_no_credential(self):
"""Verify behavior if no credential exist."""
self.create_credentials_config()
self.mock_credentials_api(self.user, data={'results': []})
actual = get_user_program_credentials(self.user)
self.assertEqual(actual, [])
@httpretty.activate
def test_get_user_program_credentials_revoked(self):
"""Verify behavior if credential revoked."""
self.create_credentials_config()
credential_data = {"results": [
{
"id": 1,
"username": "test",
"credential": {
"credential_id": 1,
"program_id": 1
},
"status": "revoked",
"uuid": "dummy-uuid-1"
}
]}
self.mock_credentials_api(self.user, data=credential_data)
actual = get_user_program_credentials(self.user)
self.assertEqual(actual, [])
@httpretty.activate
def test_get_user_programs_credentials(self):
"""Verify program credentials data can be retrieved and parsed correctly."""
credentials_config = self.create_credentials_config()
self.create_programs_config()
self.mock_programs_api()
self.mock_credentials_api(self.user, reset_url=False)
actual = get_user_program_credentials(self.user)
expected = self.PROGRAMS_API_RESPONSE['results']
expected[0]['credential_url'] = \
credentials_config.public_service_url + 'credentials/' + self.PROGRAMS_CREDENTIALS_DATA[0]['uuid']
expected[1]['credential_url'] = \
credentials_config.public_service_url + 'credentials/' + self.PROGRAMS_CREDENTIALS_DATA[1]['uuid']
self.assertEqual(len(actual), 2)
self.assertEqual(actual, expected)
httpretty.reset()
"""Helper functions for working with Credentials."""
from __future__ import unicode_literals
import logging
from openedx.core.djangoapps.credentials.models import CredentialsApiConfig
from openedx.core.djangoapps.programs.utils import get_programs_for_credentials
from openedx.core.lib.api_utils import get_api_data
log = logging.getLogger(__name__)
def get_user_credentials(user):
"""Given a user, get credentials earned from the Credentials service.
Arguments:
user (User): The user to authenticate as when requesting credentials.
Returns:
list of dict, representing credentials returned by the Credentials
service.
"""
credential_configuration = CredentialsApiConfig.current()
user_query = {'username': user.username}
credentials = get_api_data(
credential_configuration, user, credential_configuration.API_NAME, 'user_credentials', querystring=user_query
)
return credentials
def get_user_program_credentials(user):
"""Given a user, get the list of all program credentials earned and returns
list of dictionaries containing related programs data.
Arguments:
user (User): The user object for getting programs credentials.
Returns:
list, containing programs dictionaries.
"""
programs_credentials_data = []
credential_configuration = CredentialsApiConfig.current()
if not credential_configuration.is_learner_issuance_enabled:
log.debug('Display of certificates for programs is disabled.')
return programs_credentials_data
credentials = get_user_credentials(user)
if not credentials:
log.info('No credential earned by the given user.')
return programs_credentials_data
programs_credentials = []
for credential in credentials:
try:
if 'program_id' in credential['credential'] and credential['status'] == 'awarded':
programs_credentials.append(credential)
except KeyError:
log.exception('Invalid credential structure: %r', credential)
if programs_credentials:
programs_credentials_data = get_programs_for_credentials(user, programs_credentials)
return programs_credentials_data
......@@ -18,6 +18,7 @@ class ProgramsApiConfig(ConfigurationModel):
"""
OAUTH2_CLIENT_NAME = 'programs'
CACHE_KEY = 'programs.api.data'
API_NAME = 'programs'
api_version_number = models.IntegerField(verbose_name=_("API Version"))
......
......@@ -22,7 +22,7 @@ class ProgramsApiConfigMixin(object):
'enable_certification': True,
}
def create_config(self, **kwargs):
def create_programs_config(self, **kwargs):
"""Creates a new ProgramsApiConfig with DEFAULTS, updated with any provided overrides."""
fields = dict(self.DEFAULTS, **kwargs)
ProgramsApiConfig(**fields).save()
......@@ -185,6 +185,29 @@ class ProgramsDataMixin(object):
]
}
PROGRAMS_CREDENTIALS_DATA = [
{
"id": 1,
"username": "test",
"credential": {
"credential_id": 1,
"program_id": 1
},
"status": "awarded",
"uuid": "dummy-uuid-1"
},
{
"id": 2,
"username": "test",
"credential": {
"credential_id": 2,
"program_id": 2
},
"status": "awarded",
"uuid": "dummy-uuid-2"
}
]
def mock_programs_api(self, data=None, status_code=200):
"""Utility for mocking out Programs API URLs."""
self.assertTrue(httpretty.is_enabled(), msg='httpretty must be enabled to mock Programs API calls.')
......
......@@ -14,7 +14,7 @@ class TestProgramsApiConfig(ProgramsApiConfigMixin, TestCase):
"""Tests covering the ProgramsApiConfig model."""
def test_url_construction(self, _mock_cache):
"""Verify that URLs returned by the model are constructed correctly."""
programs_config = self.create_config()
programs_config = self.create_programs_config()
self.assertEqual(
programs_config.internal_api_url,
......@@ -43,7 +43,7 @@ class TestProgramsApiConfig(ProgramsApiConfigMixin, TestCase):
@ddt.unpack
def test_cache_control(self, cache_ttl, is_cache_enabled, _mock_cache):
"""Verify the behavior of the property controlling whether API responses are cached."""
programs_config = self.create_config(cache_ttl=cache_ttl)
programs_config = self.create_programs_config(cache_ttl=cache_ttl)
self.assertEqual(programs_config.is_cache_enabled, is_cache_enabled)
def test_is_student_dashboard_enabled(self, _mock_cache):
......@@ -51,13 +51,13 @@ class TestProgramsApiConfig(ProgramsApiConfigMixin, TestCase):
Verify that the property controlling display on the student dashboard is only True
when configuration is enabled and all required configuration is provided.
"""
programs_config = self.create_config(enabled=False)
programs_config = self.create_programs_config(enabled=False)
self.assertFalse(programs_config.is_student_dashboard_enabled)
programs_config = self.create_config(enable_student_dashboard=False)
programs_config = self.create_programs_config(enable_student_dashboard=False)
self.assertFalse(programs_config.is_student_dashboard_enabled)
programs_config = self.create_config()
programs_config = self.create_programs_config()
self.assertTrue(programs_config.is_student_dashboard_enabled)
def test_is_studio_tab_enabled(self, _mock_cache):
......@@ -65,16 +65,16 @@ class TestProgramsApiConfig(ProgramsApiConfigMixin, TestCase):
Verify that the property controlling display of the Studio tab is only True
when configuration is enabled and all required configuration is provided.
"""
programs_config = self.create_config(enabled=False)
programs_config = self.create_programs_config(enabled=False)
self.assertFalse(programs_config.is_studio_tab_enabled)
programs_config = self.create_config(enable_studio_tab=False)
programs_config = self.create_programs_config(enable_studio_tab=False)
self.assertFalse(programs_config.is_studio_tab_enabled)
programs_config = self.create_config(authoring_app_js_path='', authoring_app_css_path='')
programs_config = self.create_programs_config(authoring_app_js_path='', authoring_app_css_path='')
self.assertFalse(programs_config.is_studio_tab_enabled)
programs_config = self.create_config()
programs_config = self.create_programs_config()
self.assertTrue(programs_config.is_studio_tab_enabled)
def test_is_certification_enabled(self, _mock_cache):
......
......@@ -2,17 +2,20 @@
from django.core.cache import cache
from django.test import TestCase
import httpretty
import mock
from oauth2_provider.tests.factories import ClientFactory
from provider.constants import CONFIDENTIAL
from openedx.core.djangoapps.credentials.tests.mixins import CredentialsApiConfigMixin
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin, ProgramsDataMixin
from openedx.core.djangoapps.programs.utils import get_programs, get_programs_for_dashboard
from openedx.core.djangoapps.programs.utils import (
get_programs, get_programs_for_credentials, get_programs_for_dashboard
)
from student.tests.factories import UserFactory
class TestProgramRetrieval(ProgramsApiConfigMixin, ProgramsDataMixin, TestCase):
class TestProgramRetrieval(ProgramsApiConfigMixin, ProgramsDataMixin,
CredentialsApiConfigMixin, TestCase):
"""Tests covering the retrieval of programs from the Programs service."""
def setUp(self):
super(TestProgramRetrieval, self).setUp()
......@@ -25,7 +28,7 @@ class TestProgramRetrieval(ProgramsApiConfigMixin, ProgramsDataMixin, TestCase):
@httpretty.activate
def test_get_programs(self):
"""Verify programs data can be retrieved."""
self.create_config()
self.create_programs_config()
self.mock_programs_api()
actual = get_programs(self.user)
......@@ -38,59 +41,9 @@ class TestProgramRetrieval(ProgramsApiConfigMixin, ProgramsDataMixin, TestCase):
self.assertEqual(len(httpretty.httpretty.latest_requests), 1)
@httpretty.activate
def test_get_programs_caching(self):
"""Verify that when enabled, the cache is used for non-staff users."""
self.create_config(cache_ttl=1)
self.mock_programs_api()
# Warm up the cache.
get_programs(self.user)
# Hit the cache.
get_programs(self.user)
# Verify only one request was made.
self.assertEqual(len(httpretty.httpretty.latest_requests), 1)
staff_user = UserFactory(is_staff=True)
# Hit the Programs API twice.
for _ in range(2):
get_programs(staff_user)
# Verify that three requests have been made (one for student, two for staff).
self.assertEqual(len(httpretty.httpretty.latest_requests), 3)
def test_get_programs_programs_disabled(self):
"""Verify behavior when programs is disabled."""
self.create_config(enabled=False)
actual = get_programs(self.user)
self.assertEqual(actual, [])
@mock.patch('edx_rest_api_client.client.EdxRestApiClient.__init__')
def test_get_programs_client_initialization_failure(self, mock_init):
"""Verify behavior when API client fails to initialize."""
self.create_config()
mock_init.side_effect = Exception
actual = get_programs(self.user)
self.assertEqual(actual, [])
self.assertTrue(mock_init.called)
@httpretty.activate
def test_get_programs_data_retrieval_failure(self):
"""Verify behavior when data can't be retrieved from Programs."""
self.create_config()
self.mock_programs_api(status_code=500)
actual = get_programs(self.user)
self.assertEqual(actual, [])
@httpretty.activate
def test_get_programs_for_dashboard(self):
"""Verify programs data can be retrieved and parsed correctly."""
self.create_config()
self.create_programs_config()
self.mock_programs_api()
actual = get_programs_for_dashboard(self.user, self.COURSE_KEYS)
......@@ -105,7 +58,7 @@ class TestProgramRetrieval(ProgramsApiConfigMixin, ProgramsDataMixin, TestCase):
def test_get_programs_for_dashboard_dashboard_display_disabled(self):
"""Verify behavior when student dashboard display is disabled."""
self.create_config(enable_student_dashboard=False)
self.create_programs_config(enable_student_dashboard=False)
actual = get_programs_for_dashboard(self.user, self.COURSE_KEYS)
self.assertEqual(actual, {})
......@@ -113,7 +66,7 @@ class TestProgramRetrieval(ProgramsApiConfigMixin, ProgramsDataMixin, TestCase):
@httpretty.activate
def test_get_programs_for_dashboard_no_data(self):
"""Verify behavior when no programs data is found for the user."""
self.create_config()
self.create_programs_config()
self.mock_programs_api(data={'results': []})
actual = get_programs_for_dashboard(self.user, self.COURSE_KEYS)
......@@ -122,10 +75,58 @@ class TestProgramRetrieval(ProgramsApiConfigMixin, ProgramsDataMixin, TestCase):
@httpretty.activate
def test_get_programs_for_dashboard_invalid_data(self):
"""Verify behavior when the Programs API returns invalid data and parsing fails."""
self.create_config()
self.create_programs_config()
invalid_program = {'invalid_key': 'invalid_data'}
self.mock_programs_api(data={'results': [invalid_program]})
actual = get_programs_for_dashboard(self.user, self.COURSE_KEYS)
self.assertEqual(actual, {})
@httpretty.activate
def test_get_program_for_certificates(self):
"""Verify programs data can be retrieved and parsed correctly for certificates."""
self.create_programs_config()
credentials_config = self.create_credentials_config()
self.mock_programs_api()
actual = get_programs_for_credentials(self.user, self.PROGRAMS_CREDENTIALS_DATA)
expected = self.PROGRAMS_API_RESPONSE['results']
expected[0]['credential_url'] = \
credentials_config.public_service_url + 'credentials/' + self.PROGRAMS_CREDENTIALS_DATA[0]['uuid']
expected[1]['credential_url'] = \
credentials_config.public_service_url + 'credentials/' + self.PROGRAMS_CREDENTIALS_DATA[1]['uuid']
self.assertEqual(len(actual), 2)
self.assertEqual(actual, expected)
@httpretty.activate
def test_get_program_for_certificates_no_data(self):
"""Verify behavior when no programs data is found for the user."""
self.create_programs_config()
self.create_credentials_config()
self.mock_programs_api(data={'results': []})
actual = get_programs_for_credentials(self.user, self.PROGRAMS_CREDENTIALS_DATA)
self.assertEqual(actual, [])
@httpretty.activate
def test_get_program_for_certificates_id_not_exist(self):
"""Verify behavior when no program with the given program_id in
credentials exists.
"""
self.create_programs_config()
self.create_credentials_config()
self.mock_programs_api()
credential_data = [
{
"id": 1,
"username": "test",
"credential": {
"credential_id": 1,
"program_id": 100
},
"status": "awarded",
"credential_url": "www.example.com"
}
]
actual = get_programs_for_credentials(self.user, credential_data)
self.assertEqual(actual, [])
"""Helper functions for working with Programs."""
import logging
from urlparse import urljoin
from django.core.cache import cache
from edx_rest_api_client.client import EdxRestApiClient
from openedx.core.djangoapps.credentials.models import CredentialsApiConfig
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.lib.token_utils import get_id_token
from openedx.core.lib.api_utils import get_api_data
log = logging.getLogger(__name__)
......@@ -13,7 +12,6 @@ log = logging.getLogger(__name__)
def get_programs(user):
"""Given a user, get programs from the Programs service.
Returned value is cached depending on user permissions. Staff users making requests
against Programs will receive unpublished programs, while regular users will only receive
published programs.
......@@ -25,39 +23,10 @@ def get_programs(user):
list of dict, representing programs returned by the Programs service.
"""
programs_config = ProgramsApiConfig.current()
no_programs = []
# Bypass caching for staff users, who may be creating Programs and want to see them displayed immediately.
use_cache = programs_config.is_cache_enabled and not user.is_staff
if not programs_config.enabled:
log.warning('Programs configuration is disabled.')
return no_programs
if use_cache:
cached = cache.get(programs_config.CACHE_KEY)
if cached is not None:
return cached
try:
jwt = get_id_token(user, programs_config.OAUTH2_CLIENT_NAME)
api = EdxRestApiClient(programs_config.internal_api_url, jwt=jwt)
except Exception: # pylint: disable=broad-except
log.exception('Failed to initialize the Programs API client.')
return no_programs
try:
response = api.programs.get()
except Exception: # pylint: disable=broad-except
log.exception('Failed to retrieve programs from the Programs API.')
return no_programs
results = response.get('results', no_programs)
if use_cache:
cache.set(programs_config.CACHE_KEY, results, programs_config.cache_ttl)
return results
return get_api_data(programs_config, user, programs_config.API_NAME, 'programs', use_cache=use_cache)
def get_programs_for_dashboard(user, course_keys):
......@@ -105,3 +74,34 @@ def get_programs_for_dashboard(user, course_keys):
log.exception('Unable to parse Programs API response: %r', program)
return course_programs
def get_programs_for_credentials(user, programs_credentials):
""" Given a user and an iterable of credentials, get corresponding programs
data and return it as a list of dictionaries.
Arguments:
user (User): The user to authenticate as for requesting programs.
programs_credentials (list): List of credentials awarded to the user
for completion of a program.
Returns:
list, containing programs dictionaries.
"""
ProgramsApiConfig.current()
certificate_programs = []
programs = get_programs(user)
if not programs:
log.debug('No programs found for the user with ID %d.', user.id)
return certificate_programs
credential_configuration = CredentialsApiConfig.current()
for program in programs:
for credential in programs_credentials:
if program['id'] == credential['credential']['program_id']:
credentials_url = 'credentials/' + credential['uuid']
program['credential_url'] = urljoin(credential_configuration.public_service_url, credentials_url)
certificate_programs.append(program)
return certificate_programs
"""Helper functions to get data from APIs"""
from __future__ import unicode_literals
import logging
from django.core.cache import cache
from edx_rest_api_client.client import EdxRestApiClient
from openedx.core.lib.token_utils import get_id_token
log = logging.getLogger(__name__)
def get_api_data(api_config, user, api_name, resource, querystring=None, use_cache=False):
"""Fetch the data from the API using provided API Configuration and
resource.
Arguments:
api_config: The configuration which will be user for requesting data.
user (User): The user to authenticate as when requesting data.
api_name: Name fo the api to be use for logging.
resource: API resource to from where data will be requested.
querystring: Querystring parameters that might be required to request
data.
use_cache: Will be used to decide whether to cache the response data
or not.
Returns:
list of dict, representing data returned by the API.
"""
no_data = []
if not api_config.enabled:
log.warning('%s configuration is disabled.', api_name)
return no_data
if use_cache:
if api_config.CACHE_KEY:
cached = cache.get(api_config.CACHE_KEY)
if cached is not None:
return cached
else:
log.warning('No cache key available for %s configuration.', api_name)
return no_data
try:
jwt = get_id_token(user, api_config.OAUTH2_CLIENT_NAME)
api = EdxRestApiClient(api_config.internal_api_url, jwt=jwt)
except Exception: # pylint: disable=broad-except
log.exception('Failed to initialize the %s API client.', api_name)
return no_data
try:
querystring = {} if not querystring else querystring
response = getattr(api, resource).get(**querystring)
results = response.get('results', no_data)
page = 1
next_page = response.get('next', None)
while next_page:
page += 1
querystring['page'] = page
response = getattr(api, resource).get(**querystring)
results += response.get('results', no_data)
next_page = response.get('next', None)
except Exception: # pylint: disable=broad-except
log.exception('Failed to retrieve data from the %s API.', api_name)
return no_data
if use_cache:
cache.set(api_config.CACHE_KEY, results, api_config.cache_ttl)
return results
"""Tests covering Api utils."""
from django.core.cache import cache
from django.test import TestCase
import httpretty
import mock
from oauth2_provider.tests.factories import ClientFactory
from provider.constants import CONFIDENTIAL
from openedx.core.djangoapps.credentials.models import CredentialsApiConfig
from openedx.core.djangoapps.credentials.tests.mixins import CredentialsApiConfigMixin, CredentialsDataMixin
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin, ProgramsDataMixin
from openedx.core.lib.api_utils import get_api_data
from student.tests.factories import UserFactory
class TestApiDataRetrieval(CredentialsApiConfigMixin, CredentialsDataMixin, ProgramsApiConfigMixin, ProgramsDataMixin,
TestCase):
"""Test data retrieval from the api util function."""
def setUp(self):
super(TestApiDataRetrieval, self).setUp()
ClientFactory(name=CredentialsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL)
ClientFactory(name=ProgramsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL)
self.user = UserFactory()
cache.clear()
@httpretty.activate
def test_get_api_data_programs(self):
"""Verify programs data can be retrieve using get_api_data."""
program_config = self.create_programs_config()
self.mock_programs_api()
actual = get_api_data(program_config, self.user, 'programs', 'programs')
self.assertEqual(
actual,
self.PROGRAMS_API_RESPONSE['results']
)
# Verify the API was actually hit (not the cache).
self.assertEqual(len(httpretty.httpretty.latest_requests), 1)
@httpretty.activate
def test_get_api_data_credentials(self):
"""Verify credentials data can be retrieve using get_api_data."""
credentials_config = self.create_credentials_config()
self.mock_credentials_api(self.user)
querystring = {'username': self.user.username}
actual = get_api_data(credentials_config, self.user, 'credentials', 'user_credentials', querystring=querystring)
self.assertEqual(
actual,
self.CREDENTIALS_API_RESPONSE['results']
)
def test_get_api_data_disable_config(self):
"""Verify no data is retrieve if configuration is disabled."""
program_config = self.create_programs_config(enabled=False)
actual = get_api_data(program_config, self.user, 'programs', 'programs')
self.assertEqual(actual, [])
@httpretty.activate
def test_get_api_data_cache(self):
"""Verify that when enabled, the cache is used."""
program_config = self.create_programs_config(cache_ttl=1)
self.mock_programs_api()
# Warm up the cache.
get_api_data(program_config, self.user, 'programs', 'programs', use_cache=True)
# Hit the cache.
get_api_data(program_config, self.user, 'programs', 'programs', use_cache=True)
# Verify only one request was made.
self.assertEqual(len(httpretty.httpretty.latest_requests), 1)
def test_get_api_data_without_cache_key(self):
"""Verify that when cache enabled without cache key then no data is retrieved."""
ProgramsApiConfig.CACHE_KEY = None
program_config = self.create_programs_config(cache_ttl=1)
actual = get_api_data(program_config, self.user, 'programs', 'programs', use_cache=True)
self.assertEqual(actual, [])
@mock.patch('edx_rest_api_client.client.EdxRestApiClient.__init__')
def test_get_api_data_client_initialization_failure(self, mock_init):
"""Verify behavior when API client fails to initialize."""
program_config = self.create_programs_config()
mock_init.side_effect = Exception
actual = get_api_data(program_config, self.user, 'programs', 'programs')
self.assertEqual(actual, [])
self.assertTrue(mock_init.called)
@httpretty.activate
def test_get_api_data_retrieval_failure(self):
"""Verify behavior when data can't be retrieved from API."""
program_config = self.create_programs_config()
self.mock_programs_api(status_code=500)
actual = get_api_data(program_config, self.user, 'programs', 'programs')
self.assertEqual(actual, [])
@httpretty.activate
def test_get_api_data_multiple_page(self):
"""Verify that all data is retrieve for multiple page response."""
credentials_config = self.create_credentials_config()
self.mock_credentials_api(self.user, is_next_page=True)
querystring = {'username': self.user.username}
actual = get_api_data(credentials_config, self.user, 'credentials', 'user_credentials', querystring=querystring)
expected_data = self.CREDENTIALS_NEXT_API_RESPONSE['results'] + self.CREDENTIALS_API_RESPONSE['results']
self.assertEqual(actual, expected_data)
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