Remove credential listing from program list view
The new design for the program detail page introduced a sidebar which includes a program-specific listing of credentials. This is an improvement over the old list of credentials found on the program list page and is meant to replace it. The new detail page is stable, so the credential listing on the program list page can be removed. LEARNER-492
This commit is contained in:
@@ -4,7 +4,6 @@ Unit tests covering the program listing and detail pages.
|
||||
"""
|
||||
import json
|
||||
import re
|
||||
import unittest
|
||||
from urlparse import urljoin
|
||||
from uuid import uuid4
|
||||
|
||||
@@ -16,8 +15,6 @@ import mock
|
||||
|
||||
from openedx.core.djangoapps.catalog.tests.factories import ProgramFactory, CourseFactory, CourseRunFactory
|
||||
from openedx.core.djangoapps.catalog.tests.mixins import CatalogIntegrationMixin
|
||||
from openedx.core.djangoapps.credentials.tests.factories import UserCredential, ProgramCredential
|
||||
from openedx.core.djangoapps.credentials.tests.mixins import CredentialsApiConfigMixin
|
||||
from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin
|
||||
from openedx.core.djangolib.testing.utils import skip_unless_lms
|
||||
from student.tests.factories import UserFactory, CourseEnrollmentFactory
|
||||
@@ -25,15 +22,13 @@ from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory as ModuleStoreCourseFactory
|
||||
|
||||
|
||||
CATALOG_UTILS_MODULE = 'openedx.core.djangoapps.catalog.utils'
|
||||
CREDENTIALS_UTILS_MODULE = 'openedx.core.djangoapps.credentials.utils'
|
||||
PROGRAMS_UTILS_MODULE = 'openedx.core.djangoapps.programs.utils'
|
||||
|
||||
|
||||
@skip_unless_lms
|
||||
@override_settings(MKTG_URLS={'ROOT': 'https://www.example.com'})
|
||||
@mock.patch(PROGRAMS_UTILS_MODULE + '.get_programs')
|
||||
class TestProgramListing(ProgramsApiConfigMixin, CredentialsApiConfigMixin, SharedModuleStoreTestCase):
|
||||
class TestProgramListing(ProgramsApiConfigMixin, SharedModuleStoreTestCase):
|
||||
"""Unit tests for the program listing page."""
|
||||
maxDiff = None
|
||||
password = 'test'
|
||||
@@ -65,15 +60,6 @@ class TestProgramListing(ProgramsApiConfigMixin, CredentialsApiConfigMixin, Shar
|
||||
"""
|
||||
return program['title']
|
||||
|
||||
def credential_sort_key(self, credential):
|
||||
"""
|
||||
Helper function used to sort dictionaries representing credentials.
|
||||
"""
|
||||
try:
|
||||
return credential['certificate_url']
|
||||
except KeyError:
|
||||
return credential['credential_url']
|
||||
|
||||
def load_serialized_data(self, response, key):
|
||||
"""
|
||||
Extract and deserialize serialized data from the response.
|
||||
@@ -184,48 +170,6 @@ class TestProgramListing(ProgramsApiConfigMixin, CredentialsApiConfigMixin, Shar
|
||||
expected_url = reverse('program_details_view', kwargs={'program_uuid': expected_program['uuid']})
|
||||
self.assertEqual(actual_program['detail_url'], expected_url)
|
||||
|
||||
@mock.patch(CREDENTIALS_UTILS_MODULE + '.get_credentials')
|
||||
@mock.patch(CREDENTIALS_UTILS_MODULE + '.get_programs')
|
||||
def test_certificates_listed(self, mock_get_programs, mock_get_credentials, __):
|
||||
"""
|
||||
Verify that the response contains accurate certificate data when certificates are available.
|
||||
"""
|
||||
self.create_programs_config()
|
||||
self.create_credentials_config(is_learner_issuance_enabled=True)
|
||||
|
||||
mock_get_programs.return_value = self.data
|
||||
|
||||
first_credential = UserCredential(
|
||||
username=self.user.username,
|
||||
credential=ProgramCredential(
|
||||
program_uuid=self.first_program['uuid']
|
||||
)
|
||||
)
|
||||
second_credential = UserCredential(
|
||||
username=self.user.username,
|
||||
credential=ProgramCredential(
|
||||
program_uuid=self.second_program['uuid']
|
||||
)
|
||||
)
|
||||
|
||||
credentials_data = sorted([first_credential, second_credential], key=self.credential_sort_key)
|
||||
mock_get_credentials.return_value = credentials_data
|
||||
|
||||
response = self.client.get(self.url)
|
||||
actual = self.load_serialized_data(response, 'certificatesData')
|
||||
actual = sorted(actual, key=self.credential_sort_key)
|
||||
|
||||
self.assertEqual(len(actual), len(credentials_data))
|
||||
for index, actual_credential in enumerate(actual):
|
||||
expected_credential = credentials_data[index]
|
||||
|
||||
self.assertEqual(
|
||||
# TODO: certificate_url is needlessly transformed to credential_url. (╯°□°)╯︵ ┻━┻
|
||||
# Clean this up!
|
||||
actual_credential['credential_url'],
|
||||
expected_credential['certificate_url']
|
||||
)
|
||||
|
||||
|
||||
@skip_unless_lms
|
||||
@mock.patch(PROGRAMS_UTILS_MODULE + '.get_programs')
|
||||
|
||||
@@ -7,7 +7,6 @@ from django.views.decorators.http import require_GET
|
||||
from edxmako.shortcuts import render_to_response
|
||||
from lms.djangoapps.learner_dashboard.utils import strip_course_id, FAKE_COURSE_KEY
|
||||
from openedx.core.djangoapps.catalog.utils import get_programs
|
||||
from openedx.core.djangoapps.credentials.utils import get_programs_credentials
|
||||
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
|
||||
from openedx.core.djangoapps.programs.utils import (
|
||||
get_program_marketing_url,
|
||||
@@ -29,7 +28,6 @@ def program_listing(request):
|
||||
meter = ProgramProgressMeter(request.user)
|
||||
|
||||
context = {
|
||||
'credentials': get_programs_credentials(request.user),
|
||||
'disable_courseware_js': True,
|
||||
'marketing_url': get_program_marketing_url(programs_config),
|
||||
'nav_hidden': True,
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
(function(define) {
|
||||
'use strict';
|
||||
define(['backbone',
|
||||
'jquery',
|
||||
'underscore',
|
||||
'gettext',
|
||||
'text!../../../templates/learner_dashboard/certificate.underscore'
|
||||
],
|
||||
function(
|
||||
Backbone,
|
||||
$,
|
||||
_,
|
||||
gettext,
|
||||
certificateTpl
|
||||
) {
|
||||
return Backbone.View.extend({
|
||||
el: '.certificates-list',
|
||||
tpl: _.template(certificateTpl),
|
||||
initialize: function(data) {
|
||||
this.context = data.context;
|
||||
this.render();
|
||||
},
|
||||
render: function() {
|
||||
var certificatesData = this.context.certificatesData || [];
|
||||
|
||||
if (certificatesData.length) {
|
||||
this.$el.html(this.tpl(this.context));
|
||||
} else {
|
||||
/**
|
||||
* If not rendering remove el because
|
||||
* styles are applied to it
|
||||
*/
|
||||
this.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}).call(this, define || RequireJS.define);
|
||||
@@ -6,7 +6,6 @@
|
||||
'underscore',
|
||||
'gettext',
|
||||
'js/learner_dashboard/views/explore_new_programs_view',
|
||||
'js/learner_dashboard/views/certificate_view',
|
||||
'text!../../../templates/learner_dashboard/sidebar.underscore'
|
||||
],
|
||||
function(
|
||||
@@ -15,7 +14,6 @@
|
||||
_,
|
||||
gettext,
|
||||
NewProgramsView,
|
||||
CertificateView,
|
||||
sidebarTpl
|
||||
) {
|
||||
return Backbone.View.extend({
|
||||
@@ -36,10 +34,6 @@
|
||||
this.newProgramsView = new NewProgramsView({
|
||||
context: this.context
|
||||
});
|
||||
|
||||
this.newCertificateView = new CertificateView({
|
||||
context: this.context
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
define([
|
||||
'backbone',
|
||||
'jquery',
|
||||
'js/learner_dashboard/views/certificate_view'
|
||||
], function(Backbone, $, CertificateView) {
|
||||
'use strict';
|
||||
describe('Certificate View', function() {
|
||||
var view = null,
|
||||
data = {
|
||||
context: {
|
||||
certificatesData: [
|
||||
{
|
||||
'display_name': 'Testing',
|
||||
'credential_url': 'https://credentials.stage.edx.org/credentials/dummy-uuid-1/'
|
||||
},
|
||||
{
|
||||
'display_name': 'Testing2',
|
||||
'credential_url': 'https://credentials.stage.edx.org/credentials/dummy-uuid-2/'
|
||||
}
|
||||
],
|
||||
sampleCertImageSrc: '/images/programs/sample-cert.png'
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
setFixtures('<div class="certificates-list"></div>');
|
||||
view = new CertificateView(data);
|
||||
view.render();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
view.remove();
|
||||
});
|
||||
|
||||
it('should exist', function() {
|
||||
expect(view).toBeDefined();
|
||||
});
|
||||
|
||||
it('should load the certificates based on passed in certificates list', function() {
|
||||
var $certificates = view.$el.find('.certificate-link');
|
||||
expect($certificates.length).toBe(2);
|
||||
|
||||
$certificates.each(function(index, el) {
|
||||
expect($(el).html().trim()).toEqual(data.context.certificatesData[index].display_name);
|
||||
expect($(el).attr('href')).toEqual(data.context.certificatesData[index].credential_url);
|
||||
});
|
||||
expect(view.$el.find('.hd-6').html().trim()).toEqual('Program Certificates');
|
||||
expect(view.$el.find('img').attr('src')).toEqual(data.context.sampleCertImageSrc);
|
||||
});
|
||||
|
||||
it('should display no certificate box if certificates list is empty', function() {
|
||||
view.remove();
|
||||
setFixtures('<div class="certificates-list"></div>');
|
||||
view = new CertificateView({
|
||||
context: {certificatesData: []}
|
||||
});
|
||||
view.render();
|
||||
expect(view.$('.certificates-list').length).toBe(0);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -9,14 +9,7 @@ define([
|
||||
describe('Sidebar View', function() {
|
||||
var view = null,
|
||||
context = {
|
||||
marketingUrl: 'https://www.example.org/programs',
|
||||
certificatesData: [
|
||||
{
|
||||
'display_name': 'Testing',
|
||||
'credential_url': 'https://credentials.example.com/credentials/uuid/'
|
||||
}
|
||||
],
|
||||
sampleCertImageSrc: '/images/programs/sample-cert.png'
|
||||
marketingUrl: 'https://www.example.org/programs'
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
@@ -44,21 +37,16 @@ define([
|
||||
expect($sidebar.find('.program-advertise .ad-link a').attr('href')).toEqual(context.marketingUrl);
|
||||
});
|
||||
|
||||
it('should load the certificates based on passed in certificates list', function() {
|
||||
expect(view.$('.certificate-link').length).toBe(1);
|
||||
});
|
||||
|
||||
it('should not load the advertising panel if no marketing URL is provided', function() {
|
||||
var $ad;
|
||||
view.remove();
|
||||
view = new SidebarView({
|
||||
el: '.sidebar',
|
||||
context: {certificatesData: []}
|
||||
context: {}
|
||||
});
|
||||
view.render();
|
||||
$ad = view.$el.find('.program-advertise');
|
||||
expect($ad.length).toBe(0);
|
||||
expect(view.$('.certificate-link').length).toBe(0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -737,7 +737,6 @@
|
||||
'js/spec/instructor_dashboard/certificates_spec.js',
|
||||
'js/spec/instructor_dashboard/ecommerce_spec.js',
|
||||
'js/spec/instructor_dashboard/student_admin_spec.js',
|
||||
'js/spec/learner_dashboard/certificate_view_spec.js',
|
||||
'js/spec/learner_dashboard/collection_list_view_spec.js',
|
||||
'js/spec/learner_dashboard/program_card_view_spec.js',
|
||||
'js/spec/learner_dashboard/sidebar_view_spec.js',
|
||||
|
||||
@@ -43,27 +43,6 @@
|
||||
margin-bottom: $baseline;
|
||||
}
|
||||
}
|
||||
|
||||
.certificate-container {
|
||||
.hd-6 {
|
||||
color: palette(grayscale, dark);
|
||||
font-weight: font-weight(normal);
|
||||
margin-bottom: $baseline;
|
||||
}
|
||||
|
||||
.certificate-link {
|
||||
padding-top: $baseline;
|
||||
color: palette(primary, base);
|
||||
display: block;
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
&:hover {
|
||||
color: $link-hover;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-programs-message {
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
<div class="certificate-container">
|
||||
<h2 class="hd-6"><%- gettext('Program Certificates') %></h2>
|
||||
<img src="<%- sampleCertImageSrc %>" alt="">
|
||||
<% _.each(certificatesData, function(certificate){ %>
|
||||
<a class="certificate-link" href="<%- gettext(certificate.credential_url) %>"><%- gettext(certificate.display_name) %></a>
|
||||
<% }); %>
|
||||
</div>
|
||||
@@ -15,10 +15,8 @@ from openedx.core.djangolib.js_utils import (
|
||||
<%block name="js_extra">
|
||||
<%static:require_module module_name="js/learner_dashboard/program_list_factory" class_name="ProgramListFactory">
|
||||
ProgramListFactory({
|
||||
certificatesData: ${credentials | n, dump_js_escaped_json},
|
||||
marketingUrl: '${marketing_url | n, js_escaped_string}',
|
||||
programsData: ${programs | n, dump_js_escaped_json},
|
||||
sampleCertImageSrc: '${static.url('images/programs/sample-cert.png') | n, js_escaped_string}',
|
||||
userProgress: ${progress | n, dump_js_escaped_json}
|
||||
});
|
||||
</%static:require_module>
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
<aside class="aside program-advertise"></aside>
|
||||
<aside class="aside certificates-list"></aside>
|
||||
|
||||
@@ -35,6 +35,10 @@ def generate_zulu_datetime():
|
||||
|
||||
|
||||
class DictFactoryBase(factory.Factory):
|
||||
"""
|
||||
Subclass this to make factories that can be used to produce fake API response
|
||||
bodies for testing.
|
||||
"""
|
||||
class Meta(object):
|
||||
model = dict
|
||||
|
||||
|
||||
@@ -1,43 +1,31 @@
|
||||
"""Factories for generating fake credentials-related data."""
|
||||
import uuid
|
||||
# pylint: disable=missing-docstring, invalid-name
|
||||
from functools import partial
|
||||
|
||||
import factory
|
||||
from factory.fuzzy import FuzzyText
|
||||
|
||||
from openedx.core.djangoapps.catalog.tests.factories import (
|
||||
generate_instances,
|
||||
generate_course_run_key,
|
||||
DictFactoryBase,
|
||||
)
|
||||
|
||||
|
||||
class UserCredential(factory.Factory):
|
||||
"""Factory for stubbing user credentials resources from the User Credentials
|
||||
API (v1).
|
||||
"""
|
||||
class Meta(object):
|
||||
model = dict
|
||||
|
||||
id = factory.Sequence(lambda n: n) # pylint: disable=invalid-name
|
||||
username = FuzzyText(prefix='user_')
|
||||
status = 'awarded'
|
||||
uuid = FuzzyText(prefix='uuid_')
|
||||
certificate_url = FuzzyText(prefix='https://www.example.com/credentials/')
|
||||
credential = {}
|
||||
class ProgramCredential(DictFactoryBase):
|
||||
credential_id = factory.Faker('random_int')
|
||||
program_uuid = factory.Faker('uuid4')
|
||||
|
||||
|
||||
class ProgramCredential(factory.Factory):
|
||||
"""Factory for stubbing program credentials resources from the Program
|
||||
Credentials API (v1).
|
||||
"""
|
||||
class Meta(object):
|
||||
model = dict
|
||||
|
||||
credential_id = factory.Sequence(lambda n: n)
|
||||
program_uuid = factory.LazyAttribute(lambda obj: str(uuid.uuid4()))
|
||||
|
||||
|
||||
class CourseCredential(factory.Factory):
|
||||
"""Factory for stubbing course credentials resources from the Course
|
||||
Credentials API (v1).
|
||||
"""
|
||||
class Meta(object):
|
||||
model = dict
|
||||
|
||||
course_id = 'edx/test01/2015'
|
||||
credential_id = factory.Sequence(lambda n: n)
|
||||
class CourseCredential(DictFactoryBase):
|
||||
credential_id = factory.Faker('random_int')
|
||||
course_id = factory.LazyFunction(generate_course_run_key)
|
||||
certificate_type = 'verified'
|
||||
|
||||
|
||||
class UserCredential(DictFactoryBase):
|
||||
id = factory.Faker('random_int')
|
||||
username = factory.Faker('word')
|
||||
status = 'awarded'
|
||||
uuid = factory.Faker('uuid4')
|
||||
certificate_url = factory.Faker('url')
|
||||
credential = factory.LazyFunction(partial(generate_instances, ProgramCredential, count=1))
|
||||
|
||||
@@ -27,94 +27,3 @@ class CredentialsApiConfigMixin(object):
|
||||
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": [
|
||||
factories.UserCredential(
|
||||
id=1,
|
||||
username='test',
|
||||
credential=factories.ProgramCredential()
|
||||
),
|
||||
factories.UserCredential(
|
||||
id=2,
|
||||
username='test',
|
||||
credential=factories.ProgramCredential()
|
||||
),
|
||||
factories.UserCredential(
|
||||
id=3,
|
||||
status='revoked',
|
||||
username='test',
|
||||
credential=factories.ProgramCredential()
|
||||
),
|
||||
factories.UserCredential(
|
||||
id=4,
|
||||
username='test',
|
||||
credential=factories.CourseCredential(
|
||||
certificate_type='honor'
|
||||
)
|
||||
),
|
||||
factories.UserCredential(
|
||||
id=5,
|
||||
username='test',
|
||||
credential=factories.CourseCredential(
|
||||
course_id='edx/test02/2015'
|
||||
)
|
||||
),
|
||||
factories.UserCredential(
|
||||
id=6,
|
||||
username='test',
|
||||
credential=factories.CourseCredential(
|
||||
course_id='edx/test02/2015'
|
||||
)
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
CREDENTIALS_NEXT_API_RESPONSE = {
|
||||
"next": None,
|
||||
"results": [
|
||||
factories.UserCredential(
|
||||
id=7,
|
||||
username='test',
|
||||
credential=factories.ProgramCredential()
|
||||
),
|
||||
factories.UserCredential(
|
||||
id=8,
|
||||
username='test',
|
||||
credential=factories.ProgramCredential()
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
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 + '/credentials/?status=awarded&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_url = internal_api_url + '/credentials/?page=2&status=awarded&username=' + user.username
|
||||
self.CREDENTIALS_NEXT_API_RESPONSE['next'] = next_page_url
|
||||
next_page_body = json.dumps(self.CREDENTIALS_NEXT_API_RESPONSE)
|
||||
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
|
||||
)
|
||||
|
||||
@@ -1,218 +1,70 @@
|
||||
"""Tests covering Credentials utilities."""
|
||||
import uuid
|
||||
|
||||
from django.core.cache import cache
|
||||
from edx_oauth2_provider.tests.factories import ClientFactory
|
||||
import httpretty
|
||||
import mock
|
||||
from nose.plugins.attrib import attr
|
||||
from provider.constants import CONFIDENTIAL
|
||||
|
||||
from openedx.core.djangoapps.catalog.tests.factories import ProgramFactory
|
||||
from openedx.core.djangoapps.credentials.models import CredentialsApiConfig
|
||||
from openedx.core.djangoapps.credentials.tests.mixins import CredentialsApiConfigMixin, CredentialsDataMixin
|
||||
from openedx.core.djangoapps.credentials.utils import (
|
||||
get_credentials,
|
||||
get_user_program_credentials,
|
||||
get_programs_credentials,
|
||||
get_programs_for_credentials
|
||||
)
|
||||
from openedx.core.djangoapps.credentials.tests.mixins import CredentialsApiConfigMixin
|
||||
from openedx.core.djangoapps.credentials.utils import get_credentials
|
||||
from openedx.core.djangoapps.credentials.tests import factories
|
||||
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, skip_unless_lms
|
||||
from student.tests.factories import UserFactory
|
||||
|
||||
|
||||
UTILS_MODULE = 'openedx.core.djangoapps.credentials.utils'
|
||||
|
||||
|
||||
@skip_unless_lms
|
||||
@attr(shard=2)
|
||||
class TestCredentialsRetrieval(CredentialsApiConfigMixin, CredentialsDataMixin, CacheIsolationTestCase):
|
||||
""" Tests covering the retrieval of user credentials from the Credentials
|
||||
service.
|
||||
"""
|
||||
|
||||
@mock.patch(UTILS_MODULE + '.get_edx_api_data')
|
||||
class TestGetCredentials(CredentialsApiConfigMixin, CacheIsolationTestCase):
|
||||
ENABLED_CACHES = ['default']
|
||||
|
||||
def setUp(self):
|
||||
super(TestCredentialsRetrieval, self).setUp()
|
||||
super(TestGetCredentials, self).setUp()
|
||||
|
||||
ClientFactory(name=CredentialsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL)
|
||||
self.user = UserFactory()
|
||||
self.primary_uuid = str(uuid.uuid4())
|
||||
self.alternate_uuid = str(uuid.uuid4())
|
||||
|
||||
cache.clear()
|
||||
|
||||
def expected_credentials_display_data(self, programs):
|
||||
""" Returns expected credentials data to be represented. """
|
||||
return [
|
||||
{
|
||||
'display_name': programs[0]['title'],
|
||||
'subtitle': programs[0]['subtitle'],
|
||||
'credential_url': programs[0]['credential_url']
|
||||
},
|
||||
{
|
||||
'display_name': programs[1]['title'],
|
||||
'subtitle': programs[1]['subtitle'],
|
||||
'credential_url': programs[1]['credential_url']
|
||||
}
|
||||
]
|
||||
|
||||
@httpretty.activate
|
||||
def test_get_credentials(self):
|
||||
"""Verify user credentials data can be retrieve."""
|
||||
self.create_credentials_config()
|
||||
self.mock_credentials_api(self.user)
|
||||
self.user = UserFactory()
|
||||
|
||||
def test_get_many(self, mock_get_edx_api_data):
|
||||
expected = factories.UserCredential.create_batch(3)
|
||||
mock_get_edx_api_data.return_value = expected
|
||||
|
||||
actual = get_credentials(self.user)
|
||||
self.assertEqual(actual, self.CREDENTIALS_API_RESPONSE['results'])
|
||||
|
||||
@httpretty.activate
|
||||
def test_get_credentials_caching(self):
|
||||
"""Verify that when enabled, the cache is used for non-staff users."""
|
||||
self.create_credentials_config(cache_ttl=1)
|
||||
self.mock_credentials_api(self.user)
|
||||
mock_get_edx_api_data.assert_called_once()
|
||||
call = mock_get_edx_api_data.mock_calls[0]
|
||||
__, __, kwargs = call
|
||||
|
||||
# Warm up the cache.
|
||||
get_credentials(self.user)
|
||||
|
||||
# Hit the cache.
|
||||
get_credentials(self.user)
|
||||
|
||||
# Verify only one request was made.
|
||||
self.assertEqual(len(httpretty.httpretty.latest_requests), 1)
|
||||
|
||||
staff_user = UserFactory(is_staff=True)
|
||||
|
||||
# Hit the Credentials API twice.
|
||||
for _ in range(2):
|
||||
get_credentials(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_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_programs_credentials(self):
|
||||
"""Verify program credentials data can be retrieved and parsed correctly."""
|
||||
# create credentials and program configuration
|
||||
self.create_credentials_config()
|
||||
|
||||
# Mocking the API responses from programs and credentials
|
||||
primary_uuid, alternate_uuid = str(uuid.uuid4()), str(uuid.uuid4())
|
||||
credentials_api_response = {
|
||||
"next": None,
|
||||
"results": [
|
||||
factories.UserCredential(
|
||||
username='test',
|
||||
credential=factories.ProgramCredential(program_uuid=primary_uuid)
|
||||
),
|
||||
factories.UserCredential(
|
||||
username='test',
|
||||
credential=factories.ProgramCredential(program_uuid=alternate_uuid)
|
||||
)
|
||||
]
|
||||
querystring = {
|
||||
'username': self.user.username,
|
||||
'status': 'awarded',
|
||||
}
|
||||
self.mock_credentials_api(self.user, data=credentials_api_response, reset_url=False)
|
||||
programs = [
|
||||
ProgramFactory(uuid=primary_uuid), ProgramFactory(uuid=alternate_uuid)
|
||||
]
|
||||
self.assertEqual(kwargs['querystring'], querystring)
|
||||
|
||||
with mock.patch("openedx.core.djangoapps.credentials.utils.get_programs_for_credentials") as mock_get_programs:
|
||||
mock_get_programs.return_value = programs
|
||||
actual = get_user_program_credentials(self.user)
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
# checking response from API is as expected
|
||||
self.assertEqual(len(actual), 2)
|
||||
self.assertEqual(actual, programs)
|
||||
def test_get_one(self, mock_get_edx_api_data):
|
||||
expected = factories.UserCredential()
|
||||
mock_get_edx_api_data.return_value = expected
|
||||
|
||||
@httpretty.activate
|
||||
def test_get_programs_credentials(self):
|
||||
""" Verify that the program credentials data required for display can
|
||||
be retrieved.
|
||||
"""
|
||||
# create credentials and program configuration
|
||||
self.create_credentials_config()
|
||||
program_uuid = str(uuid.uuid4())
|
||||
actual = get_credentials(self.user, program_uuid=program_uuid)
|
||||
|
||||
# Mocking the API responses from programs and credentials
|
||||
primary_uuid, alternate_uuid = str(uuid.uuid4()), str(uuid.uuid4())
|
||||
credentials_api_response = {
|
||||
"next": None,
|
||||
"results": [
|
||||
factories.UserCredential(
|
||||
username='test',
|
||||
credential=factories.ProgramCredential(program_uuid=primary_uuid)
|
||||
),
|
||||
factories.UserCredential(
|
||||
username='test',
|
||||
credential=factories.ProgramCredential(program_uuid=alternate_uuid)
|
||||
)
|
||||
]
|
||||
mock_get_edx_api_data.assert_called_once()
|
||||
call = mock_get_edx_api_data.mock_calls[0]
|
||||
__, __, kwargs = call
|
||||
|
||||
querystring = {
|
||||
'username': self.user.username,
|
||||
'status': 'awarded',
|
||||
'program_uuid': program_uuid,
|
||||
}
|
||||
self.mock_credentials_api(self.user, data=credentials_api_response, reset_url=False)
|
||||
programs = [
|
||||
ProgramFactory(uuid=primary_uuid), ProgramFactory(uuid=alternate_uuid)
|
||||
]
|
||||
self.assertEqual(kwargs['querystring'], querystring)
|
||||
|
||||
with mock.patch("openedx.core.djangoapps.credentials.utils.get_programs") as mock_get_programs:
|
||||
mock_get_programs.return_value = programs
|
||||
actual = get_programs_credentials(self.user)
|
||||
expected = self.expected_credentials_display_data(programs)
|
||||
|
||||
# Checking result is as expected
|
||||
self.assertEqual(len(actual), 2)
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
def _expected_program_credentials_data(self):
|
||||
"""
|
||||
Dry method for getting expected program credentials response data.
|
||||
"""
|
||||
return [
|
||||
factories.UserCredential(
|
||||
username='test',
|
||||
credential=factories.ProgramCredential(
|
||||
program_uuid=self.primary_uuid
|
||||
)
|
||||
),
|
||||
factories.UserCredential(
|
||||
username='test',
|
||||
credential=factories.ProgramCredential(
|
||||
program_uuid=self.alternate_uuid
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
def test_get_program_for_certificates(self):
|
||||
"""Verify programs data can be retrieved and parsed correctly for certificates."""
|
||||
programs = [
|
||||
ProgramFactory(uuid=self.primary_uuid),
|
||||
ProgramFactory(uuid=self.alternate_uuid)
|
||||
]
|
||||
|
||||
program_credentials_data = self._expected_program_credentials_data()
|
||||
with mock.patch("openedx.core.djangoapps.credentials.utils.get_programs") as patched_get_programs:
|
||||
patched_get_programs.return_value = programs
|
||||
actual = get_programs_for_credentials(program_credentials_data)
|
||||
|
||||
self.assertEqual(len(actual), 2)
|
||||
self.assertEqual(actual, programs)
|
||||
|
||||
def test_get_program_for_certificates_no_data(self):
|
||||
"""Verify behavior when no programs data is found for the user."""
|
||||
program_credentials_data = self._expected_program_credentials_data()
|
||||
with mock.patch("openedx.core.djangoapps.credentials.utils.get_programs") as patched_get_programs:
|
||||
patched_get_programs.return_value = []
|
||||
actual = get_programs_for_credentials(program_credentials_data)
|
||||
|
||||
self.assertEqual(actual, [])
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
"""Helper functions for working with Credentials."""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from edx_rest_api_client.client import EdxRestApiClient
|
||||
|
||||
from openedx.core.djangoapps.catalog.utils import get_programs
|
||||
from openedx.core.djangoapps.credentials.models import CredentialsApiConfig
|
||||
from openedx.core.lib.edx_api_utils import get_edx_api_data
|
||||
from openedx.core.lib.token_utils import JwtBuilder
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_credentials_api_client(user):
|
||||
""" Returns an authenticated Credentials API client. """
|
||||
@@ -53,89 +48,3 @@ def get_credentials(user, program_uuid=None):
|
||||
return get_edx_api_data(
|
||||
credential_configuration, 'credentials', api=api, querystring=querystring, cache_key=cache_key
|
||||
)
|
||||
|
||||
|
||||
def get_programs_for_credentials(programs_credentials):
|
||||
""" Given a user and an iterable of credentials, get corresponding programs
|
||||
data and return it as a list of dictionaries.
|
||||
|
||||
Arguments:
|
||||
programs_credentials (list): List of credentials awarded to the user
|
||||
for completion of a program.
|
||||
|
||||
Returns:
|
||||
list, containing programs dictionaries.
|
||||
"""
|
||||
certified_programs = []
|
||||
programs = get_programs()
|
||||
for program in programs:
|
||||
for credential in programs_credentials:
|
||||
if program['uuid'] == credential['credential']['program_uuid']:
|
||||
program['credential_url'] = credential['certificate_url']
|
||||
certified_programs.append(program)
|
||||
|
||||
return certified_programs
|
||||
|
||||
|
||||
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_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_uuid' in credential['credential']:
|
||||
programs_credentials.append(credential)
|
||||
except KeyError:
|
||||
log.exception('Invalid credential structure: %r', credential)
|
||||
|
||||
if programs_credentials:
|
||||
programs_credentials_data = get_programs_for_credentials(programs_credentials)
|
||||
|
||||
return programs_credentials_data
|
||||
|
||||
|
||||
def get_programs_credentials(user):
|
||||
"""Return program credentials data required for display.
|
||||
|
||||
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:
|
||||
try:
|
||||
program_data = {
|
||||
'display_name': program['title'],
|
||||
'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
|
||||
|
||||
@@ -19,7 +19,6 @@ from django.test import RequestFactory, TestCase, override_settings
|
||||
from django.conf import settings
|
||||
from django.contrib import sites
|
||||
from nose.plugins import Plugin
|
||||
from waffle.models import Switch
|
||||
|
||||
from request_cache.middleware import RequestCache
|
||||
|
||||
|
||||
Reference in New Issue
Block a user