Commit 9cd7c932 by Renzo Lucioni

Link program listing cards to detail pages

When program detail pages are enabled, cards on the listing page will link to their respective detail pages. Includes extensive cleanup of program listing tests. ECOM-4227.
parent 142eb42e
...@@ -891,7 +891,7 @@ class AnonymousLookupTable(ModuleStoreTestCase): ...@@ -891,7 +891,7 @@ class AnonymousLookupTable(ModuleStoreTestCase):
self.assertEqual(anonymous_id, anonymous_id_for_user(self.user, course2.id, save=False)) self.assertEqual(anonymous_id, anonymous_id_for_user(self.user, course2.id, save=False))
# TODO: Clean up these tests so that they use the ProgramsDataMixin. # TODO: Clean up these tests so that they use program factories.
@attr('shard_3') @attr('shard_3')
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
@ddt.ddt @ddt.ddt
......
# -*- coding: utf-8 -*-
""" """
Unit tests covering the program listing and detail pages. Unit tests covering the program listing and detail pages.
""" """
import datetime
import json import json
import re
import unittest import unittest
from urlparse import urljoin from urlparse import urljoin
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.test import override_settings, TestCase from django.test import override_settings
from django.utils.text import slugify
from edx_oauth2_provider.tests.factories import ClientFactory from edx_oauth2_provider.tests.factories import ClientFactory
import httpretty import httpretty
from opaque_keys.edx import locator
from provider.constants import CONFIDENTIAL from provider.constants import CONFIDENTIAL
from openedx.core.djangoapps.credentials.models import CredentialsApiConfig from openedx.core.djangoapps.credentials.models import CredentialsApiConfig
from openedx.core.djangoapps.credentials.tests import factories as credentials_factories from openedx.core.djangoapps.credentials.tests import factories as credentials_factories
from openedx.core.djangoapps.credentials.tests.mixins import CredentialsDataMixin, CredentialsApiConfigMixin from openedx.core.djangoapps.credentials.tests.mixins import CredentialsApiConfigMixin
from openedx.core.djangoapps.programs.models import ProgramsApiConfig from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.djangoapps.programs.tests import factories from openedx.core.djangoapps.programs.tests import factories as programs_factories
from openedx.core.djangoapps.programs.tests.mixins import ( from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin
ProgramsApiConfigMixin, from openedx.core.djangoapps.programs.utils import get_display_category
ProgramsDataMixin) from student.tests.factories import UserFactory, CourseEnrollmentFactory
from student.models import CourseEnrollment from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from student.tests.factories import UserFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
@httpretty.activate
@override_settings(MKTG_URLS={'ROOT': 'https://www.example.com'})
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
@override_settings(MKTG_URLS={'ROOT': 'http://edx.org'}) class TestProgramListing(ProgramsApiConfigMixin, CredentialsApiConfigMixin, SharedModuleStoreTestCase):
class TestProgramListing( """Unit tests for the program listing page."""
ModuleStoreTestCase, maxDiff = None
ProgramsApiConfigMixin, password = 'test'
ProgramsDataMixin,
CredentialsDataMixin,
CredentialsApiConfigMixin):
"""
Unit tests for getting the list of programs enrolled by a logged in user
"""
PASSWORD = 'test'
url = reverse('program_listing_view') url = reverse('program_listing_view')
@classmethod
def setUpClass(cls):
super(TestProgramListing, cls).setUpClass()
for name in [ProgramsApiConfig.OAUTH2_CLIENT_NAME, CredentialsApiConfig.OAUTH2_CLIENT_NAME]:
ClientFactory(name=name, client_type=CONFIDENTIAL)
cls.course = CourseFactory()
organization = programs_factories.Organization()
run_mode = programs_factories.RunMode(course_key=unicode(cls.course.id)) # pylint: disable=no-member
course_code = programs_factories.CourseCode(run_modes=[run_mode])
cls.first_program = programs_factories.Program(
organizations=[organization],
course_codes=[course_code]
)
cls.second_program = programs_factories.Program(
organizations=[organization],
course_codes=[course_code]
)
cls.data = sorted([cls.first_program, cls.second_program], key=cls.program_sort_key)
cls.marketing_root = urljoin(settings.MKTG_URLS.get('ROOT'), 'xseries').rstrip('/')
def setUp(self): def setUp(self):
super(TestProgramListing, self).setUp()
self.user = UserFactory()
self.client.login(username=self.user.username, password=self.password)
@classmethod
def program_sort_key(cls, program):
""" """
Add a student Helper function used to sort dictionaries representing programs.
""" """
super(TestProgramListing, self).setUp() return program['id']
ClientFactory(name=CredentialsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL)
ClientFactory(name=ProgramsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL)
self.student = UserFactory()
def _create_course_and_enroll(self, student, org, course, run): def credential_sort_key(self, credential):
""" """
Creates a course and associated enrollment. Helper function used to sort dictionaries representing credentials.
""" """
course_location = locator.CourseLocator(org, course, run) try:
course = CourseFactory.create( return credential['certificate_url']
org=course_location.org, except KeyError:
number=course_location.course, return credential['credential_url']
run=course_location.run
def mock_programs_api(self, data):
"""Helper for mocking out Programs API URLs."""
self.assertTrue(httpretty.is_enabled(), msg='httpretty must be enabled to mock Programs API calls.')
url = ProgramsApiConfig.current().internal_api_url.strip('/') + '/programs/'
body = json.dumps({'results': data})
httpretty.register_uri(httpretty.GET, url, body=body, content_type='application/json')
def mock_credentials_api(self, data):
"""Helper for mocking out Credentials API URLs."""
self.assertTrue(httpretty.is_enabled(), msg='httpretty must be enabled to mock Credentials API calls.')
url = '{base}/user_credentials/?username={username}'.format(
base=CredentialsApiConfig.current().internal_api_url.strip('/'),
username=self.user.username
) )
enrollment = CourseEnrollment.enroll(student, course.id) body = json.dumps({'results': data})
enrollment.created = datetime.datetime(2000, 12, 31, 0, 0, 0, 0)
enrollment.save()
def _get_program_url(self, marketing_slug): httpretty.register_uri(httpretty.GET, url, body=body, content_type='application/json')
def load_serialized_data(self, response, key):
""" """
Helper function to get the program card url Extract and deserialize serialized data from the response.
""" """
return urljoin( pattern = re.compile(r'{key}: (?P<data>\[.*\])'.format(key=key))
settings.MKTG_URLS.get('ROOT'), match = pattern.search(response.content)
'xseries' + '/{}' serialized = match.group('data')
).format(marketing_slug)
return json.loads(serialized)
def _setup_and_get_program(self): def assert_dict_contains_subset(self, superset, subset):
""" """
The core function to setup the mock program api, Verify that the dict superset contains the dict subset.
then call the django test client to get the actual program listing page
make sure the request suceeds and make sure x_series_url is on the page Works like assertDictContainsSubset, deprecated since Python 3.2.
See: https://docs.python.org/2.7/library/unittest.html#unittest.TestCase.assertDictContainsSubset.
""" """
self.mock_programs_api() superset_keys = set(superset.keys())
self.client.login(username=self.student.username, password=self.PASSWORD) subset_keys = set(subset.keys())
response = self.client.get(self.url) intersection = {key: superset[key] for key in superset_keys & subset_keys}
x_series_url = urljoin(settings.MKTG_URLS.get('ROOT'), 'xseries')
self.assertContains(response, x_series_url) self.assertEqual(subset, intersection)
return response
def _get_program_checklist(self, program_id): def test_login_required(self):
""" """
The convenience function to get all the program related page element we would like to check against Verify that login is required to access the page.
""" """
return [
self.PROGRAM_NAMES[program_id],
self._get_program_url(self.PROGRAMS_API_RESPONSE['results'][program_id]['marketing_slug']),
self.PROGRAMS_API_RESPONSE['results'][program_id]['organizations'][0]['display_name'],
]
def _assert_progress_data_present(self, response):
"""Verify that progress data is present."""
self.assertContains(response, 'userProgress')
@httpretty.activate
def test_get_program_with_no_enrollment(self):
self.create_programs_config() self.create_programs_config()
response = self._setup_and_get_program() self.mock_programs_api(self.data)
for program_element in self._get_program_checklist(0):
self.assertNotContains(response, program_element)
for program_element in self._get_program_checklist(1):
self.assertNotContains(response, program_element)
@httpretty.activate
def test_get_one_program(self):
self.create_programs_config()
self._create_course_and_enroll(self.student, *self.COURSE_KEYS[0].split('/'))
response = self._setup_and_get_program()
for program_element in self._get_program_checklist(0):
self.assertContains(response, program_element)
for program_element in self._get_program_checklist(1):
self.assertNotContains(response, program_element)
self._assert_progress_data_present(response) self.client.logout()
@httpretty.activate response = self.client.get(self.url)
def test_get_both_program(self): self.assertRedirects(
self.create_programs_config() response,
self._create_course_and_enroll(self.student, *self.COURSE_KEYS[0].split('/')) '{}?next={}'.format(reverse('signin_user'), self.url)
self._create_course_and_enroll(self.student, *self.COURSE_KEYS[5].split('/')) )
response = self._setup_and_get_program()
for program_element in self._get_program_checklist(0):
self.assertContains(response, program_element)
for program_element in self._get_program_checklist(1):
self.assertContains(response, program_element)
self._assert_progress_data_present(response) self.client.login(username=self.user.username, password=self.password)
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
def test_get_programs_dashboard_not_enabled(self): def test_404_if_disabled(self):
"""
Verify that the page 404s if disabled.
"""
self.create_programs_config(program_listing_enabled=False) self.create_programs_config(program_listing_enabled=False)
self.client.login(username=self.student.username, password=self.PASSWORD)
response = self.client.get(self.url) response = self.client.get(self.url)
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
def test_xseries_advertise_disabled(self): def test_empty_state(self):
self.create_programs_config(xseries_ad_enabled=False) """
self.client.login(username=self.student.username, password=self.PASSWORD) Verify that the response contains no programs data when no programs are engaged.
"""
self.create_programs_config()
self.mock_programs_api(self.data)
response = self.client.get(self.url) response = self.client.get(self.url)
x_series_url = urljoin(settings.MKTG_URLS.get('ROOT'), 'xseries') self.assertContains(response, 'programsData: []')
self.assertNotContains(response, x_series_url)
def test_get_programs_not_logged_in(self): def test_programs_listed(self):
"""
Verify that the response contains accurate programs data when programs are engaged.
"""
self.create_programs_config() self.create_programs_config()
self.mock_programs_api(self.data)
CourseEnrollmentFactory(user=self.user, course_id=self.course.id) # pylint: disable=no-member
response = self.client.get(self.url) response = self.client.get(self.url)
actual = self.load_serialized_data(response, 'programsData')
actual = sorted(actual, key=self.program_sort_key)
self.assertRedirects( for index, actual_program in enumerate(actual):
response, expected_program = self.data[index]
'{}?next={}'.format(reverse('signin_user'), self.url)
self.assert_dict_contains_subset(actual_program, expected_program)
self.assertEqual(
actual_program['display_category'],
get_display_category(expected_program)
) )
def _expected_progam_credentials_data(self): def test_toggle_xseries_advertising(self):
""" """
Dry method for getting expected program credentials response data. Verify that when XSeries advertising is disabled, no link to the marketing site
appears in the response (and vice versa).
""" """
return [ # Verify the URL is present when advertising is enabled.
credentials_factories.UserCredential( self.create_programs_config()
id=1, self.mock_programs_api(self.data)
username='test',
credential=credentials_factories.ProgramCredential() response = self.client.get(self.url)
), self.assertContains(response, self.marketing_root)
credentials_factories.UserCredential(
id=2, # Verify the URL is missing when advertising is disabled.
username='test', self.create_programs_config(xseries_ad_enabled=False)
credential=credentials_factories.ProgramCredential()
) response = self.client.get(self.url)
] self.assertNotContains(response, self.marketing_root)
def _expected_credentials_data(self):
""" Dry method for getting expected credentials."""
program_credentials_data = self._expected_progam_credentials_data()
return [
{
'display_name': self.PROGRAMS_API_RESPONSE['results'][0]['name'],
'subtitle': self.PROGRAMS_API_RESPONSE['results'][0]['subtitle'],
'credential_url':program_credentials_data[0]['certificate_url']
},
{
'display_name': self.PROGRAMS_API_RESPONSE['results'][1]['name'],
'subtitle':self.PROGRAMS_API_RESPONSE['results'][1]['subtitle'],
'credential_url':program_credentials_data[1]['certificate_url']
}
]
@httpretty.activate
def test_get_xseries_certificates_with_data(self):
def test_links_to_detail_pages(self):
"""
Verify that links to detail pages are present when enabled, instead of
links to the marketing site.
"""
self.create_programs_config() self.create_programs_config()
self.create_credentials_config(is_learner_issuance_enabled=True) self.mock_programs_api(self.data)
CourseEnrollmentFactory(user=self.user, course_id=self.course.id) # pylint: disable=no-member
self.client.login(username=self.student.username, password=self.PASSWORD) response = self.client.get(self.url)
actual = self.load_serialized_data(response, 'programsData')
actual = sorted(actual, key=self.program_sort_key)
# mock programs and credentials apis for index, actual_program in enumerate(actual):
self.mock_programs_api() expected_program = self.data[index]
self.mock_credentials_api(self.student, data=self.CREDENTIALS_API_RESPONSE, reset_url=False)
response = self.client.get(reverse("program_listing_view")) base = reverse('program_details_view', args=[expected_program['id']]).rstrip('/')
for certificate in self._expected_credentials_data(): slug = slugify(expected_program['name'])
self.assertContains(response, certificate['display_name']) self.assertEqual(
self.assertContains(response, certificate['credential_url']) actual_program['detail_url'],
'{}/{}'.format(base, slug)
)
self.assertContains(response, 'images/xseries-certificate-visual.png') # Verify that links to the marketing site are present when detail pages are disabled.
self.create_programs_config(program_details_enabled=False)
@httpretty.activate response = self.client.get(self.url)
def test_get_xseries_certificates_without_data(self): actual = self.load_serialized_data(response, 'programsData')
actual = sorted(actual, key=self.program_sort_key)
for index, actual_program in enumerate(actual):
expected_program = self.data[index]
self.assertEqual(
actual_program['detail_url'],
'{}/{}'.format(self.marketing_root, expected_program['marketing_slug'])
)
def test_certificates_listed(self):
"""
Verify that the response contains accurate certificate data when certificates are available.
"""
self.create_programs_config() self.create_programs_config()
self.create_credentials_config(is_learner_issuance_enabled=True) self.create_credentials_config(is_learner_issuance_enabled=True)
self.client.login(username=self.student.username, password=self.PASSWORD) self.mock_programs_api(self.data)
first_credential = credentials_factories.UserCredential(
username=self.user.username,
credential=credentials_factories.ProgramCredential(
program_id=self.first_program['id']
)
)
second_credential = credentials_factories.UserCredential(
username=self.user.username,
credential=credentials_factories.ProgramCredential(
program_id=self.second_program['id']
)
)
credentials_data = sorted([first_credential, second_credential], key=self.credential_sort_key)
self.mock_credentials_api(credentials_data)
# mock programs and credentials apis response = self.client.get(self.url)
self.mock_programs_api() actual = self.load_serialized_data(response, 'certificatesData')
self.mock_credentials_api(self.student, data={"results": []}, reset_url=False) actual = sorted(actual, key=self.credential_sort_key)
for index, actual_credential in enumerate(actual):
expected_credential = credentials_data[index]
response = self.client.get(reverse("program_listing_view")) self.assertEqual(
for certificate in self._expected_credentials_data(): # TODO: certificate_url is needlessly transformed to credential_url. (╯°□°)╯︵ ┻━┻
self.assertNotContains(response, certificate['display_name']) # Clean this up!
self.assertNotContains(response, certificate['credential_url']) actual_credential['credential_url'],
expected_credential['certificate_url']
)
@httpretty.activate @httpretty.activate
@override_settings(MKTG_URLS={'ROOT': 'http://edx.org'}) @override_settings(MKTG_URLS={'ROOT': 'https://www.example.com'})
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class TestProgramDetails(ProgramsApiConfigMixin, SharedModuleStoreTestCase): class TestProgramDetails(ProgramsApiConfigMixin, SharedModuleStoreTestCase):
""" """Unit tests for the program details page."""
Unit tests for the program details page
"""
program_id = 123 program_id = 123
password = 'test' password = 'test'
url = reverse('program_details_view', args=[program_id])
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
super(TestProgramDetails, cls).setUpClass() super(TestProgramDetails, cls).setUpClass()
cls.course = CourseFactory()
ClientFactory(name=ProgramsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL)
course = CourseFactory()
organization = programs_factories.Organization()
run_mode = programs_factories.RunMode(course_key=unicode(course.id)) # pylint: disable=no-member
course_code = programs_factories.CourseCode(run_modes=[run_mode])
cls.data = programs_factories.Program(
organizations=[organization],
course_codes=[course_code]
)
def setUp(self): def setUp(self):
super(TestProgramDetails, self).setUp() super(TestProgramDetails, self).setUp()
self.details_page = reverse('program_details_view', args=[self.program_id])
self.user = UserFactory() self.user = UserFactory()
self.client.login(username=self.user.username, password=self.password) self.client.login(username=self.user.username, password=self.password)
ClientFactory(name=ProgramsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL) def mock_programs_api(self, data, status=200):
self.organization = factories.Organization()
self.run_mode = factories.RunMode(course_key=unicode(self.course.id)) # pylint: disable=no-member
self.course_code = factories.CourseCode(run_modes=[self.run_mode])
self.data = factories.Program(
organizations=[self.organization],
course_codes=[self.course_code]
)
def _mock_programs_api(self, data, status=200):
"""Helper for mocking out Programs API URLs.""" """Helper for mocking out Programs API URLs."""
self.assertTrue(httpretty.is_enabled(), msg='httpretty must be enabled to mock Programs API calls.') self.assertTrue(httpretty.is_enabled(), msg='httpretty must be enabled to mock Programs API calls.')
...@@ -281,15 +337,15 @@ class TestProgramDetails(ProgramsApiConfigMixin, SharedModuleStoreTestCase): ...@@ -281,15 +337,15 @@ class TestProgramDetails(ProgramsApiConfigMixin, SharedModuleStoreTestCase):
content_type='application/json', content_type='application/json',
) )
def _assert_program_data_present(self, response): def assert_program_data_present(self, response):
"""Verify that program data is present.""" """Verify that program data is present."""
self.assertContains(response, 'programData') self.assertContains(response, 'programData')
self.assertContains(response, 'urls') self.assertContains(response, 'urls')
self.assertContains(response, 'program_listing_url') self.assertContains(response, 'program_listing_url')
self.assertContains(response, self.data['name']) self.assertContains(response, self.data['name'])
self._assert_programs_tab_present(response) self.assert_programs_tab_present(response)
def _assert_programs_tab_present(self, response): def assert_programs_tab_present(self, response):
"""Verify that the programs tab is present in the nav.""" """Verify that the programs tab is present in the nav."""
soup = BeautifulSoup(response.content, 'html.parser') soup = BeautifulSoup(response.content, 'html.parser')
self.assertTrue( self.assertTrue(
...@@ -301,20 +357,20 @@ class TestProgramDetails(ProgramsApiConfigMixin, SharedModuleStoreTestCase): ...@@ -301,20 +357,20 @@ class TestProgramDetails(ProgramsApiConfigMixin, SharedModuleStoreTestCase):
Verify that login is required to access the page. Verify that login is required to access the page.
""" """
self.create_programs_config() self.create_programs_config()
self._mock_programs_api(self.data) self.mock_programs_api(self.data)
self.client.logout() self.client.logout()
response = self.client.get(self.details_page) response = self.client.get(self.url)
self.assertRedirects( self.assertRedirects(
response, response,
'{}?next={}'.format(reverse('signin_user'), self.details_page) '{}?next={}'.format(reverse('signin_user'), self.url)
) )
self.client.login(username=self.user.username, password=self.password) self.client.login(username=self.user.username, password=self.password)
response = self.client.get(self.details_page) response = self.client.get(self.url)
self._assert_program_data_present(response) self.assert_program_data_present(response)
def test_404_if_disabled(self): def test_404_if_disabled(self):
""" """
...@@ -322,33 +378,33 @@ class TestProgramDetails(ProgramsApiConfigMixin, SharedModuleStoreTestCase): ...@@ -322,33 +378,33 @@ class TestProgramDetails(ProgramsApiConfigMixin, SharedModuleStoreTestCase):
""" """
self.create_programs_config(program_details_enabled=False) self.create_programs_config(program_details_enabled=False)
response = self.client.get(self.details_page) response = self.client.get(self.url)
self.assertEquals(response.status_code, 404) self.assertEqual(response.status_code, 404)
def test_page_routing(self): def test_404_if_no_data(self):
"""Verify that the page can be hit with or without a program name in the URL.""" """Verify that the page 404s if no program data is found."""
self.create_programs_config() self.create_programs_config()
self._mock_programs_api(self.data)
response = self.client.get(self.details_page) self.mock_programs_api(self.data, status=404)
self._assert_program_data_present(response) response = self.client.get(self.url)
self.assertEqual(response.status_code, 404)
response = self.client.get(self.details_page + 'program_name/') httpretty.reset()
self._assert_program_data_present(response)
response = self.client.get(self.details_page + 'program_name/invalid/') self.mock_programs_api({})
self.assertEquals(response.status_code, 404) response = self.client.get(self.url)
self.assertEqual(response.status_code, 404)
def test_404_if_no_data(self): def test_page_routing(self):
"""Verify that the page 404s if no program data is found.""" """Verify that the page can be hit with or without a program name in the URL."""
self.create_programs_config() self.create_programs_config()
self.mock_programs_api(self.data)
self._mock_programs_api(self.data, status=404) response = self.client.get(self.url)
response = self.client.get(self.details_page) self.assert_program_data_present(response)
self.assertEquals(response.status_code, 404)
httpretty.reset() response = self.client.get(self.url + 'program_name/')
self.assert_program_data_present(response)
self._mock_programs_api({}) response = self.client.get(self.url + 'program_name/invalid/')
response = self.client.get(self.details_page) self.assertEqual(response.status_code, 404)
self.assertEquals(response.status_code, 404)
...@@ -21,28 +21,26 @@ from lms.djangoapps.learner_dashboard.utils import ( ...@@ -21,28 +21,26 @@ from lms.djangoapps.learner_dashboard.utils import (
@require_GET @require_GET
def view_programs(request): def view_programs(request):
"""View programs in which the user is engaged.""" """View programs in which the user is engaged."""
show_program_listing = ProgramsApiConfig.current().show_program_listing programs_config = ProgramsApiConfig.current()
if not show_program_listing: if not programs_config.show_program_listing:
raise Http404 raise Http404
meter = utils.ProgramProgressMeter(request.user) meter = utils.ProgramProgressMeter(request.user)
programs = meter.engaged_programs programs = meter.engaged_programs
# TODO: Pull 'xseries' string from configuration model. # TODO: Pull 'xseries' string from configuration model.
marketing_root = urljoin(settings.MKTG_URLS.get('ROOT'), 'xseries').strip('/') marketing_root = urljoin(settings.MKTG_URLS.get('ROOT'), 'xseries').rstrip('/')
for program in programs: for program in programs:
program['detail_url'] = utils.get_program_detail_url(program, marketing_root)
program['display_category'] = utils.get_display_category(program) program['display_category'] = utils.get_display_category(program)
program['marketing_url'] = '{root}/{slug}'.format(
root=marketing_root,
slug=program['marketing_slug']
)
context = { context = {
'programs': programs, 'programs': programs,
'progress': meter.progress, 'progress': meter.progress,
'xseries_url': marketing_root if ProgramsApiConfig.current().show_xseries_ad else None, 'xseries_url': marketing_root if programs_config.show_xseries_ad else None,
'nav_hidden': True, 'nav_hidden': True,
'show_program_listing': show_program_listing, 'show_program_listing': programs_config.show_program_listing,
'credentials': get_programs_credentials(request.user, category='xseries'), 'credentials': get_programs_credentials(request.user, category='xseries'),
'disable_courseware_js': True, 'disable_courseware_js': True,
'uses_pattern_library': True 'uses_pattern_library': True
...@@ -55,8 +53,8 @@ def view_programs(request): ...@@ -55,8 +53,8 @@ def view_programs(request):
@require_GET @require_GET
def program_details(request, program_id): def program_details(request, program_id):
"""View details about a specific program.""" """View details about a specific program."""
show_program_details = ProgramsApiConfig.current().show_program_details programs_config = ProgramsApiConfig.current()
if not show_program_details: if not programs_config.show_program_details:
raise Http404 raise Http404
program_data = utils.get_programs(request.user, program_id=program_id) program_data = utils.get_programs(request.user, program_id=program_id)
...@@ -65,7 +63,6 @@ def program_details(request, program_id): ...@@ -65,7 +63,6 @@ def program_details(request, program_id):
raise Http404 raise Http404
program_data = utils.supplement_program_data(program_data, request.user) program_data = utils.supplement_program_data(program_data, request.user)
show_program_listing = ProgramsApiConfig.current().show_program_listing
urls = { urls = {
'program_listing_url': reverse('program_listing_view'), 'program_listing_url': reverse('program_listing_view'),
...@@ -77,7 +74,7 @@ def program_details(request, program_id): ...@@ -77,7 +74,7 @@ def program_details(request, program_id):
context = { context = {
'program_data': program_data, 'program_data': program_data,
'urls': urls, 'urls': urls,
'show_program_listing': show_program_listing, 'show_program_listing': programs_config.show_program_listing,
'nav_hidden': True, 'nav_hidden': True,
'disable_courseware_js': True, 'disable_courseware_js': True,
'uses_pattern_library': True 'uses_pattern_library': True
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
type: data.display_category + ' Program', type: data.display_category + ' Program',
subtitle: data.subtitle, subtitle: data.subtitle,
organizations: data.organizations, organizations: data.organizations,
marketingUrl: data.marketing_url, detailUrl: data.detail_url,
smallBannerUrl: data.banner_image_urls.w348h116, smallBannerUrl: data.banner_image_urls.w348h116,
mediumBannerUrl: data.banner_image_urls.w435h145, mediumBannerUrl: data.banner_image_urls.w435h145,
largeBannerUrl: data.banner_image_urls.w726h242, largeBannerUrl: data.banner_image_urls.w726h242,
......
...@@ -28,7 +28,7 @@ define([ ...@@ -28,7 +28,7 @@ define([
modified: '2016-03-25T13:45:21.220732Z', modified: '2016-03-25T13:45:21.220732Z',
marketing_slug: 'p_2?param=haha&test=b', marketing_slug: 'p_2?param=haha&test=b',
id: 146, id: 146,
marketing_url: 'http://www.edx.org/xseries/p_2?param=haha&test=b', detail_url: 'http://courses.edx.org/dashboard/programs/1/foo',
banner_image_urls: { banner_image_urls: {
w348h116: 'http://www.edx.org/images/test1', w348h116: 'http://www.edx.org/images/test1',
w435h145: 'http://www.edx.org/images/test2', w435h145: 'http://www.edx.org/images/test2',
...@@ -55,7 +55,7 @@ define([ ...@@ -55,7 +55,7 @@ define([
expect($card.find('.title').html().trim()).toEqual(program.name); expect($card.find('.title').html().trim()).toEqual(program.name);
expect($card.find('.category span').html().trim()).toEqual('XSeries Program'); expect($card.find('.category span').html().trim()).toEqual('XSeries Program');
expect($card.find('.organization').html().trim()).toEqual(program.organizations[0].key); expect($card.find('.organization').html().trim()).toEqual(program.organizations[0].key);
expect($card.find('.card-link').attr('href')).toEqual(program.marketing_url); expect($card.find('.card-link').attr('href')).toEqual(program.detail_url);
}; };
beforeEach(function() { beforeEach(function() {
......
...@@ -5,4 +5,3 @@ ...@@ -5,4 +5,3 @@
<span><%- gettext('Explore XSeries Programs') %></span> <span><%- gettext('Explore XSeries Programs') %></span>
</a> </a>
</section> </section>
<div class="text-section"> <div class="text-section">
<h3 id="program-<%- id %>" class="title hd-3"><%- gettext(name) %></h3> <h3 id="program-<%- id %>" class="title hd-3"><%- gettext(name) %></h3>
<div class="meta-info grid-container"> <div class="meta-info grid-container">
...@@ -10,7 +9,7 @@ ...@@ -10,7 +9,7 @@
</div> </div>
<% if (progress) { %> <% if (progress) { %>
<p class="certificate-status"> <p class="certificate-status">
<a href="<%- marketingUrl %>" class="status-text secondary" aria-describedby="program-<%- id %>"><%= interpolate( <a href="<%- detailUrl %>" class="status-text secondary" aria-describedby="program-<%- id %>"><%= interpolate(
ngettext( ngettext(
'%(count)s course is in progress.', '%(count)s course is in progress.',
'%(count)s courses are in progress.', '%(count)s courses are in progress.',
...@@ -19,7 +18,7 @@ ...@@ -19,7 +18,7 @@
{count: progress.total.in_progress}, true {count: progress.total.in_progress}, true
) %></a> ) %></a>
<a href="<%- marketingUrl %>" class="status-text secondary" aria-describedby="program-<%- id %>"><%= interpolate( <a href="<%- detailUrl %>" class="status-text secondary" aria-describedby="program-<%- id %>"><%= interpolate(
ngettext( ngettext(
'%(count)s course has not been started.', '%(count)s course has not been started.',
'%(count)s courses have not been started.', '%(count)s courses have not been started.',
...@@ -42,7 +41,7 @@ ...@@ -42,7 +41,7 @@
<div class="bar not-started"></div> <div class="bar not-started"></div>
</div> </div>
<% } %> <% } %>
<a href="<%- marketingUrl %>" class="card-link"> <a href="<%- detailUrl %>" class="card-link">
<div class="banner-image-container"> <div class="banner-image-container">
<picture> <picture>
<source srcset="<%- smallBannerUrl %>" media="(max-width: <%- breakpoints.max.tiny %>)"> <source srcset="<%- smallBannerUrl %>" media="(max-width: <%- breakpoints.max.tiny %>)">
......
...@@ -14,7 +14,7 @@ class UserCredential(factory.Factory): ...@@ -14,7 +14,7 @@ class UserCredential(factory.Factory):
username = FuzzyText(prefix='user_') username = FuzzyText(prefix='user_')
status = 'awarded' status = 'awarded'
uuid = FuzzyText(prefix='uuid_') uuid = FuzzyText(prefix='uuid_')
certificate_url = 'http=//credentials.edx.org/credentials/dummy-uuid' certificate_url = FuzzyText(prefix='https://www.example.com/credentials/')
credential = {} credential = {}
......
...@@ -49,12 +49,14 @@ class TestCredentialsRetrieval(ProgramsApiConfigMixin, CredentialsApiConfigMixin ...@@ -49,12 +49,14 @@ class TestCredentialsRetrieval(ProgramsApiConfigMixin, CredentialsApiConfigMixin
factories.UserCredential( factories.UserCredential(
id=1, id=1,
username='test', username='test',
credential=factories.ProgramCredential() credential=factories.ProgramCredential(),
certificate_url=self.CREDENTIALS_API_RESPONSE['results'][0]['certificate_url'],
), ),
factories.UserCredential( factories.UserCredential(
id=2, id=2,
username='test', username='test',
credential=factories.ProgramCredential() credential=factories.ProgramCredential(),
certificate_url=self.CREDENTIALS_API_RESPONSE['results'][1]['certificate_url'],
) )
] ]
......
...@@ -6,6 +6,7 @@ import logging ...@@ -6,6 +6,7 @@ import logging
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.text import slugify
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
import pytz import pytz
...@@ -128,6 +129,26 @@ def get_programs_for_credentials(user, programs_credentials): ...@@ -128,6 +129,26 @@ def get_programs_for_credentials(user, programs_credentials):
return certificate_programs return certificate_programs
def get_program_detail_url(program, marketing_root):
"""Construct the URL to be used when linking to program details.
Arguments:
program (dict): Representation of a program.
marketing_root (str): Root URL used to build links to XSeries marketing pages.
Returns:
str, a link to program details
"""
if ProgramsApiConfig.current().show_program_details:
base = reverse('program_details_view', kwargs={'program_id': program['id']}).rstrip('/')
slug = slugify(program['name'])
else:
base = marketing_root.rstrip('/')
slug = program['marketing_slug']
return '{base}/{slug}'.format(base=base, slug=slug)
def get_display_category(program): def get_display_category(program):
""" Given the program, return the category of the program for display """ Given the program, return the category of the program for display
Arguments: Arguments:
......
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