Add some links to student records

Add some waffle-guarded connection points to the Credentials service
to start filling out the user flow for Student Records.

Specifically, add a button to the Program Progress Details page if
a certificate exists, and add a link in the Learner Profile page.

Both only appear if the 'student_records' waffle switch is active.

LEARNER-4701
This commit is contained in:
Michael Terry
2018-04-26 13:48:59 -04:00
committed by Michael Terry
parent 4297c999c4
commit 502287b07e
13 changed files with 82 additions and 2 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.credentials.utils import get_credentials_records_url
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.djangoapps.programs.utils import (
@@ -97,13 +98,22 @@ class ProgramDetailsFragmentView(EdxFragmentView):
skus = program_data.get('skus')
ecommerce_service = EcommerceService()
# TODO: Don't have business logic of course-certificate==record-available here in LMS.
# Eventually, the UI should ask Credentials if there is a record available and get a URL from it.
# But this is here for now so that we can gate this URL behind both this business logic and
# a waffle flag. This feature is in active developoment.
program_record_url = get_credentials_records_url(program_uuid=program_uuid)
if not certificate_data:
program_record_url = None
urls = {
'program_listing_url': reverse('program_listing_view'),
'track_selection_url': strip_course_id(
reverse('course_modes_choose', kwargs={'course_id': FAKE_COURSE_KEY})
),
'commerce_api_url': reverse('commerce_api:v0:baskets:create'),
'buy_button_url': ecommerce_service.get_checkout_page_url(*skus)
'buy_button_url': ecommerce_service.get_checkout_page_url(*skus),
'program_record_url': program_record_url,
}
context = {

View File

@@ -12,7 +12,9 @@ from bs4 import BeautifulSoup
from django.conf import settings
from django.core.urlresolvers import reverse, reverse_lazy
from django.test import override_settings
from waffle.testutils import override_switch
from lms.envs.test import CREDENTIALS_PUBLIC_SERVICE_URL
from openedx.core.djangoapps.catalog.tests.factories import CourseFactory, CourseRunFactory, ProgramFactory
from openedx.core.djangoapps.catalog.tests.mixins import CatalogIntegrationMixin
from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin
@@ -172,6 +174,7 @@ class TestProgramListing(ProgramsApiConfigMixin, SharedModuleStoreTestCase):
@skip_unless_lms
@mock.patch(PROGRAMS_UTILS_MODULE + '.get_programs')
@override_switch('student_records', True)
class TestProgramDetails(ProgramsApiConfigMixin, CatalogIntegrationMixin, SharedModuleStoreTestCase):
"""Unit tests for the program details page."""
program_uuid = str(uuid4())
@@ -198,6 +201,8 @@ class TestProgramDetails(ProgramsApiConfigMixin, CatalogIntegrationMixin, Shared
"""Verify that program data is present."""
self.assertContains(response, 'programData')
self.assertContains(response, 'urls')
self.assertContains(response,
'"program_record_url": "{}/records/programs/'.format(CREDENTIALS_PUBLIC_SERVICE_URL))
self.assertContains(response, 'program_listing_url')
self.assertContains(response, self.data['title'])
self.assert_programs_tab_present(response)
@@ -230,7 +235,10 @@ class TestProgramDetails(ProgramsApiConfigMixin, CatalogIntegrationMixin, Shared
self.client.login(username=self.user.username, password=self.password)
response = self.client.get(self.url)
with mock.patch('lms.djangoapps.learner_dashboard.programs.get_certificates') as certs:
certs.return_value = [{'type': 'program', 'url': '/'}]
response = self.client.get(self.url)
self.assert_program_data_present(response)
def test_404_if_disabled(self, _mock_get_programs):

View File

@@ -52,6 +52,7 @@ describe('Program Progress View', () => {
model: programModel,
courseModel: courseData,
certificateCollection,
programRecordUrl: '/foo/bar'
});
beforeEach(() => {
@@ -94,6 +95,13 @@ describe('Program Progress View', () => {
expect(view.$('.certificate-heading')).toHaveText('Your XSeries Certificate');
});
it('should render the program record link', () => {
view = initView();
expect(view.$('.program-record-link button')).toBeInDOM();
expect(view.$('.program-record-link').attr('href')).toEqual('/foo/bar');
});
it('should render the course certificate list', () => {
view = initView();
const $certificates = view.$('.certificate-list .certificate');

View File

@@ -468,6 +468,7 @@ describe('Program Details Header View', () => {
program_listing_url: '/dashboard/programs/',
commerce_api_url: '/api/commerce/v0/baskets/',
track_selection_url: '/course_modes/choose/',
program_record_url: 'http://credentials.example.com/records/programs/UUID',
},
userPreferences: {
'pref-lang': 'en',

View File

@@ -16,6 +16,7 @@ class ProgramDetailsSidebarView extends Backbone.View {
this.courseModel = options.courseModel || {};
this.certificateCollection = options.certificateCollection || [];
this.programCertificate = this.getProgramCertificate();
this.programRecordUrl = options.programRecordUrl;
this.render();
}
@@ -23,6 +24,7 @@ class ProgramDetailsSidebarView extends Backbone.View {
const data = $.extend({}, this.model.toJSON(), {
programCertificate: this.programCertificate ?
this.programCertificate.toJSON() : {},
programRecordUrl: this.programRecordUrl,
});
HtmlUtils.setHtml(this.$el, this.tpl(data));

View File

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

View File

@@ -630,6 +630,10 @@
.program-sidebar {
padding: 30px 10px;
.program-record {
text-align: center;
}
@media (min-width: $bp-screen-md) {
@include float(right);

View File

@@ -7,4 +7,11 @@
</a>
<% } %>
</aside>
<% if (programRecordUrl) { %>
<aside class="aside js-program-record program-record">
<a href="<%- programRecordUrl %>" class="program-record-link">
<button class="program-record-button"><%- gettext('View Program Record') %></button>
</a>
</aside>
<% } %>
<aside class="aside js-course-certificates"></aside>

View File

@@ -2,6 +2,7 @@
Models for credentials support for the LMS and Studio.
"""
import waffle
from urlparse import urljoin
from config_models.models import ConfigurationModel
@@ -77,6 +78,17 @@ class CredentialsApiConfig(ConfigurationModel):
root = helpers.get_value('CREDENTIALS_PUBLIC_SERVICE_URL', settings.CREDENTIALS_PUBLIC_SERVICE_URL)
return urljoin(root, '/api/{}/'.format(API_VERSION))
@property
def public_records_url(self):
"""
Publicly-accessible Records URL root.
"""
# Temporarily disable this feature while we work on it
if not waffle.switch_is_active('student_records'):
return None
root = helpers.get_value('CREDENTIALS_PUBLIC_SERVICE_URL', settings.CREDENTIALS_PUBLIC_SERVICE_URL)
return urljoin(root, '/records/')
@property
def is_learner_issuance_enabled(self):
"""

View File

@@ -9,6 +9,19 @@ from openedx.core.lib.edx_api_utils import get_edx_api_data
from openedx.core.lib.token_utils import JwtBuilder
def get_credentials_records_url(program_uuid=None):
"""
Returns a URL for a given records page (or general records list if given no UUID).
May return None if this feature is disabled.
"""
base_url = CredentialsApiConfig.current().public_records_url
if base_url is None:
return None
if program_uuid:
return base_url + 'programs/{}/'.format(program_uuid)
return base_url
def get_credentials_api_client(user):
""" Returns an authenticated Credentials API client. """

View File

@@ -33,6 +33,10 @@ from openedx.core.djangolib.markup import HTML
${_('Build out your profile to personalize your identity on {platform_name}.').format(
platform_name=platform_name,
)}
% if records_url:
## We don't translate this yet because we know it's not the final string
<p>To view and share your program records, go to <a href="${records_url}">My Records</a>.</p>
% endif
</div>
</div>
% endif

View File

@@ -4,8 +4,10 @@
import datetime
import ddt
import mock
from waffle.testutils import override_switch
from lms.djangoapps.certificates.tests.factories import GeneratedCertificateFactory # pylint: disable=import-error
from lms.envs.test import CREDENTIALS_PUBLIC_SERVICE_URL
from course_modes.models import CourseMode
from django.conf import settings
from django.core.urlresolvers import reverse
@@ -20,6 +22,7 @@ from xmodule.modulestore.tests.factories import CourseFactory
@ddt.ddt
@override_switch('student_records', True)
class LearnerProfileViewTest(UrlResetMixin, ModuleStoreTestCase):
""" Tests for the student profile view. """
@@ -110,6 +113,11 @@ class LearnerProfileViewTest(UrlResetMixin, ModuleStoreTestCase):
for attribute in self.CONTEXT_DATA:
self.assertIn(attribute, response.content)
def test_records_link(self):
profile_path = reverse('learner_profile', kwargs={'username': self.USERNAME})
response = self.client.get(path=profile_path)
self.assertContains(response, '<a href="{}/records/">'.format(CREDENTIALS_PUBLIC_SERVICE_URL))
def test_undefined_profile_page(self):
"""
Verify that a 404 is returned for a non-existent profile page.

View File

@@ -12,6 +12,7 @@ from django.utils.translation import ugettext as _
from django.views.decorators.http import require_http_methods
from django_countries import countries
from edxmako.shortcuts import marketing_link
from openedx.core.djangoapps.credentials.utils import get_credentials_records_url
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.user_api.accounts.api import get_account_settings
@@ -139,6 +140,7 @@ def learner_profile_context(request, profile_username, user_is_staff):
'show_dashboard_tabs': True,
'disable_courseware_js': True,
'nav_hidden': True,
'records_url': get_credentials_records_url(),
}
if badges_enabled():