Merge pull request #18851 from edx/bbaker/reveal-industry-pathways

Reveal industry pathways in program details sidebar
This commit is contained in:
Brandon Baker
2018-09-04 09:30:13 -04:00
committed by GitHub
12 changed files with 123 additions and 30 deletions

View File

@@ -12,6 +12,7 @@ from web_fragments.fragment import Fragment
from lms.djangoapps.commerce.utils import EcommerceService
from lms.djangoapps.learner_dashboard.utils import FAKE_COURSE_KEY, strip_course_id
from openedx.core.djangoapps.catalog.constants import PathwayType
from openedx.core.djangoapps.catalog.utils import get_pathways
from openedx.core.djangoapps.credentials.utils import get_credentials_records_url
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
@@ -107,12 +108,16 @@ class ProgramDetailsFragmentView(EdxFragmentView):
if not certificate_data:
program_record_url = None
pathways = []
industry_pathways = []
credit_pathways = []
try:
for pathway_id in program_data['pathway_ids']:
pathway = get_pathways(request.site, pathway_id)
if pathway and pathway['email']:
pathways.append(pathway)
if pathway['pathway_type'] == PathwayType.CREDIT.value:
credit_pathways.append(pathway)
elif pathway['pathway_type'] == PathwayType.INDUSTRY.value:
industry_pathways.append(pathway)
# if pathway caching did not complete fully (no pathway_ids)
except KeyError:
pass
@@ -133,7 +138,8 @@ class ProgramDetailsFragmentView(EdxFragmentView):
'program_data': program_data,
'course_data': course_data,
'certificate_data': certificate_data,
'pathways': pathways,
'industry_pathways': industry_pathways,
'credit_pathways': credit_pathways,
}
html = render_to_string('learner_dashboard/program_details_fragment.html', context)

View File

@@ -14,6 +14,7 @@ from django.urls import reverse, reverse_lazy
from django.test import override_settings
from lms.envs.test import CREDENTIALS_PUBLIC_SERVICE_URL
from openedx.core.djangoapps.catalog.constants import PathwayType
from openedx.core.djangoapps.catalog.tests.factories import (
PathwayFactory,
CourseFactory,
@@ -32,6 +33,17 @@ PROGRAMS_UTILS_MODULE = 'openedx.core.djangoapps.programs.utils'
PROGRAMS_MODULE = 'lms.djangoapps.learner_dashboard.programs'
def load_serialized_data(response, key):
"""
Extract and deserialize serialized data from the response.
"""
pattern = re.compile(r'{key}: (?P<data>\[.*\])'.format(key=key))
match = pattern.search(response.content)
serialized = match.group('data')
return json.loads(serialized)
@skip_unless_lms
@override_settings(MKTG_URLS={'ROOT': 'https://www.example.com'})
@mock.patch(PROGRAMS_UTILS_MODULE + '.get_programs')
@@ -68,16 +80,6 @@ class TestProgramListing(ProgramsApiConfigMixin, SharedModuleStoreTestCase):
"""
return program['title']
def load_serialized_data(self, response, key):
"""
Extract and deserialize serialized data from the response.
"""
pattern = re.compile(r'{key}: (?P<data>\[.*\])'.format(key=key))
match = pattern.search(response.content)
serialized = match.group('data')
return json.loads(serialized)
def assert_dict_contains_subset(self, superset, subset):
"""
Verify that the dict superset contains the dict subset.
@@ -140,7 +142,7 @@ class TestProgramListing(ProgramsApiConfigMixin, SharedModuleStoreTestCase):
CourseEnrollmentFactory(user=self.user, course_id=self.course.id)
response = self.client.get(self.url)
actual = self.load_serialized_data(response, 'programsData')
actual = load_serialized_data(response, 'programsData')
actual = sorted(actual, key=self.program_sort_key)
for index, actual_program in enumerate(actual):
@@ -169,7 +171,7 @@ class TestProgramListing(ProgramsApiConfigMixin, SharedModuleStoreTestCase):
CourseEnrollmentFactory(user=self.user, course_id=self.course.id)
response = self.client.get(self.url)
actual = self.load_serialized_data(response, 'programsData')
actual = load_serialized_data(response, 'programsData')
actual = sorted(actual, key=self.program_sort_key)
for index, actual_program in enumerate(actual):
@@ -227,8 +229,20 @@ class TestProgramDetails(ProgramsApiConfigMixin, CatalogIntegrationMixin, Shared
)
def assert_pathway_data_present(self, response):
""" Verify that the correct pathway data is present. """
self.assertContains(response, 'industryPathways')
self.assertContains(response, 'creditPathways')
self.assertContains(response, self.pathway_data['name'])
industry_pathways = load_serialized_data(response, 'industryPathways')
credit_pathways = load_serialized_data(response, 'creditPathways')
if self.pathway_data['pathway_type'] == PathwayType.CREDIT.value:
credit_pathway, = credit_pathways # Verify that there is only one credit pathway
self.assertEqual(self.pathway_data, credit_pathway)
self.assertEqual([], industry_pathways)
elif self.pathway_data['pathway_type'] == PathwayType.INDUSTRY.value:
industry_pathway, = industry_pathways # Verify that there is only one industry pathway
self.assertEqual(self.pathway_data, industry_pathway)
self.assertEqual([], credit_pathways)
def test_login_required(self, mock_get_programs, mock_get_pathways):
"""

File diff suppressed because one or more lines are too long

View File

@@ -485,6 +485,17 @@ describe('Program Details Header View', () => {
destination_url: 'edx.org',
},
],
industryPathways: [
{
org_name: 'Test Org Name',
email: 'test@test.com',
name: 'Name of Test Pathway',
program_uuids: ['0ffff5d6-0177-4690-9a48-aa2fecf94610'],
description: 'Test industry pathway description',
id: 3,
destination_url: 'industry.com',
},
],
};
const data = options.programData;

View File

@@ -14,7 +14,7 @@ class ProgramDetailsSidebarView extends Backbone.View {
constructor(options) {
const defaults = {
events: {
'click .sidebar-button': 'trackPathwayClicked',
'click .pathway-button': 'trackPathwayClicked',
},
};
super(Object.assign({}, defaults, options));
@@ -26,6 +26,7 @@ class ProgramDetailsSidebarView extends Backbone.View {
this.certificateCollection = options.certificateCollection || [];
this.programCertificate = this.getProgramCertificate();
this.programRecordUrl = options.programRecordUrl;
this.industryPathways = options.industryPathways;
this.creditPathways = options.creditPathways;
this.programModel = options.model;
this.render();
@@ -36,6 +37,7 @@ class ProgramDetailsSidebarView extends Backbone.View {
programCertificate: this.programCertificate ?
this.programCertificate.toJSON() : {},
programRecordUrl: this.programRecordUrl,
industryPathways: this.industryPathways,
creditPathways: this.creditPathways,
});
@@ -94,6 +96,7 @@ class ProgramDetailsSidebarView extends Backbone.View {
trackPathwayClicked(event) {
var button = event.currentTarget;
window.analytics.track('edx.bi.dashboard.program.pathway.clicked', {
category: 'pathways',
// Credentials uses the uuid without dashes so we are converting here for consistency

View File

@@ -113,6 +113,7 @@ class ProgramDetailsView extends Backbone.View {
courseModel: this.courseData,
certificateCollection: this.certificateCollection,
programRecordUrl: this.options.urls.program_record_url,
industryPathways: this.options.industryPathways,
creditPathways: this.options.creditPathways,
});
}

View File

@@ -677,6 +677,10 @@ $btn-color-primary: palette(primary, dark);
}
}
.program-credit-pathways {
padding-bottom: 2em;
}
.pathway-wrapper {
@include margin-left($baseline*0.75);
@@ -698,6 +702,10 @@ $btn-color-primary: palette(primary, dark);
}
}
.pathway-wrapper:last-child {
border-bottom: none;
}
@media (min-width: $bp-screen-md) {
@include float(right);

View File

@@ -18,7 +18,8 @@ ProgramDetailsFactory({
certificateData: ${certificate_data | n, dump_js_escaped_json},
urls: ${urls | n, dump_js_escaped_json},
userPreferences: ${user_preferences | n, dump_js_escaped_json},
creditPathways: ${pathways | n, dump_js_escaped_json},
industryPathways: ${industry_pathways | n, dump_js_escaped_json},
creditPathways: ${credit_pathways | n, dump_js_escaped_json},
});
</%static:webpack>
</%block>

View File

@@ -22,8 +22,8 @@
<% } %>
</aside>
<% if (creditPathways.length > 0) { %>
<aside class="aside js-program-pathways program-pathways">
<h2 class = "divider-heading"><%- gettext('Additional Learning Opportunities') %></h2>
<aside class="aside js-program-pathways program-credit-pathways">
<h2 class = "divider-heading"><%- gettext('Additional Credit Opportunities') %></h2>
<% for (var i = 0; i < creditPathways.length; i++) {
var pathway = creditPathways[i];
@@ -37,7 +37,7 @@
<% if (pathway.destination_url) { %>
<div class="sidebar-button-wrapper">
<a href="<%- pathway.destination_url %>" class="pathway-link">
<button class="btn sidebar-button" data-pathway-id="<%- pathway.id %>" data-pathway-name="<%- pathway.name %>"><%- gettext('Learn More') %></button>
<button class="btn pathway-button sidebar-button" data-pathway-id="<%- pathway.id %>" data-pathway-name="<%- pathway.name %>"><%- gettext('Learn More') %></button>
</a>
</div>
<% } %>
@@ -47,3 +47,28 @@
</aside>
<% } %>
<% if (industryPathways.length > 0) { %>
<aside class="aside js-program-pathways program-industry-pathways">
<h2 class = "divider-heading"><%- gettext('Additional Professional Opportunities') %></h2>
<% for (var i = 0; i < industryPathways.length; i++) {
var pathway = industryPathways[i];
%>
<div class="pathway-wrapper">
<div class = "pathway-info">
<h2 class="pathway-heading"> <%- pathway.name %> </h2>
<% if (pathway.description) { %>
<p> <%- pathway.description %> </p>
<% } %>
<% if (pathway.destination_url) { %>
<div class="sidebar-button-wrapper">
<a href="<%- pathway.destination_url %>" class="pathway-link">
<button class="btn pathway-button sidebar-button" data-pathway-id="<%- pathway.id %>" data-pathway-name="<%- pathway.name %>"><%- gettext('Learn More') %></button>
</a>
</div>
<% } %>
</div>
</div>
<% } %>
</aside>
<% } %>

View File

@@ -0,0 +1,8 @@
""" Constants associated with catalog """
from enum import Enum
class PathwayType(Enum):
""" Allowed values for pathway_type. """
CREDIT = 'credit'
INDUSTRY = 'industry'

View File

@@ -74,7 +74,7 @@ class Command(BaseCommand):
cache.set(SITE_PROGRAM_UUIDS_CACHE_KEY_TPL.format(domain=site.domain), uuids, None)
pathway_ids = new_pathways.keys()
logger.info('Caching ids for {total} credit pathways for site {site_name}.'.format(
logger.info('Caching ids for {total} pathways for site {site_name}.'.format(
total=len(pathway_ids),
site_name=site.domain,
))
@@ -86,7 +86,7 @@ class Command(BaseCommand):
cache.set_many(programs, None)
successful_pathways = len(pathways)
logger.info('Caching details for {successful_pathways} credit pathways.'.format(
logger.info('Caching details for {successful_pathways} pathways.'.format(
successful_pathways=successful_pathways))
cache.set_many(pathways, None)
@@ -149,7 +149,7 @@ class Command(BaseCommand):
next_page = next_page + 1 if new_pathways['next'] else None
except: # pylint: disable=bare-except
logger.error('Failed to retrieve credit pathways for site: {domain}.'.format(domain=site.domain))
logger.error('Failed to retrieve pathways for site: {domain}.'.format(domain=site.domain))
failure = True
logger.info('Received {total} pathways for site {domain}'.format(

View File

@@ -4,8 +4,11 @@ from functools import partial
import factory
import uuid
from factory.fuzzy import FuzzyChoice
from faker import Faker
from openedx.core.djangoapps.catalog.constants import PathwayType
fake = Faker()
VERIFIED_MODE = 'verified'
@@ -221,3 +224,4 @@ class PathwayFactory(DictFactoryBase):
name = factory.Faker('sentence')
org_name = factory.Faker('company')
programs = factory.LazyFunction(partial(generate_instances, ProgramFactory))
pathway_type = FuzzyChoice((path_type.value for path_type in PathwayType))