Commit 2955a6e6 by Renzo Lucioni

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
parent 78d88958
......@@ -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,
......
(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
});
}
});
}
......
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 {
......
<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>
......
<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
......
"""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(factory.Factory):
"""Factory for stubbing program credentials resources from the Program
Credentials API (v1).
"""
class Meta(object):
model = dict
class ProgramCredential(DictFactoryBase):
credential_id = factory.Faker('random_int')
program_uuid = factory.Faker('uuid4')
credential_id = factory.Sequence(lambda n: n)
program_uuid = factory.LazyAttribute(lambda obj: str(uuid.uuid4()))
class CourseCredential(DictFactoryBase):
credential_id = factory.Faker('random_int')
course_id = factory.LazyFunction(generate_course_run_key)
certificate_type = 'verified'
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)
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
)
"""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
......
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