Merge pull request #18851 from edx/bbaker/reveal-industry-pathways
Reveal industry pathways in program details sidebar
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
<% } %>
|
||||
|
||||
8
openedx/core/djangoapps/catalog/constants.py
Normal file
8
openedx/core/djangoapps/catalog/constants.py
Normal 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'
|
||||
@@ -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(
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user