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:
committed by
Michael Terry
parent
4297c999c4
commit
502287b07e
@@ -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 = {
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -630,6 +630,10 @@
|
||||
.program-sidebar {
|
||||
padding: 30px 10px;
|
||||
|
||||
.program-record {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (min-width: $bp-screen-md) {
|
||||
@include float(right);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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. """
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user