diff --git a/common/test/acceptance/fixtures/programs.py b/common/test/acceptance/fixtures/programs.py index 6a26745bd5..242b0a7714 100644 --- a/common/test/acceptance/fixtures/programs.py +++ b/common/test/acceptance/fixtures/programs.py @@ -65,4 +65,5 @@ class ProgramsConfigMixin(object): 'enable_certification': is_enabled, 'xseries_ad_enabled': is_enabled, 'program_listing_enabled': is_enabled, + 'program_details_enabled': is_enabled, }).install() diff --git a/common/test/acceptance/pages/lms/programs.py b/common/test/acceptance/pages/lms/programs.py index 08efd8558a..884063d500 100644 --- a/common/test/acceptance/pages/lms/programs.py +++ b/common/test/acceptance/pages/lms/programs.py @@ -20,3 +20,11 @@ class ProgramListingPage(PageObject): def is_sidebar_present(self): """Check whether sidebar is present.""" return self.q(css='.sidebar').present + + +class ProgramDetailsPage(PageObject): + """Program details page.""" + url = BASE_URL + '/dashboard/programs/123' + + def is_browser_on_page(self): + return self.q(css='.js-program-details-wrapper').present diff --git a/common/test/acceptance/tests/lms/test_programs.py b/common/test/acceptance/tests/lms/test_programs.py index 2d69fbcc14..6a12e60054 100644 --- a/common/test/acceptance/tests/lms/test_programs.py +++ b/common/test/acceptance/tests/lms/test_programs.py @@ -5,16 +5,15 @@ from ...fixtures.programs import FakeProgram, ProgramsFixture, ProgramsConfigMix from ...fixtures.course import CourseFixture from ..helpers import UniqueCourseTest from ...pages.lms.auto_auth import AutoAuthPage -from ...pages.lms.programs import ProgramListingPage +from ...pages.lms.programs import ProgramListingPage, ProgramDetailsPage -class ProgramListingPageBase(ProgramsConfigMixin, UniqueCourseTest): +class ProgramPageBase(ProgramsConfigMixin, UniqueCourseTest): """Base class used for program listing page tests.""" def setUp(self): - super(ProgramListingPageBase, self).setUp() + super(ProgramPageBase, self).setUp() self.set_programs_api_configuration(is_enabled=True) - self.listing_page = ProgramListingPage(self.browser) def stub_api(self, course_id=None): """Stub out the programs API with fake data.""" @@ -35,8 +34,13 @@ class ProgramListingPageBase(ProgramsConfigMixin, UniqueCourseTest): AutoAuthPage(self.browser, course_id=course_id).visit() -class ProgramListingPageTest(ProgramListingPageBase): +class ProgramListingPageTest(ProgramPageBase): """Verify user-facing behavior of the program listing page.""" + def setUp(self): + super(ProgramListingPageTest, self).setUp() + + self.listing_page = ProgramListingPage(self.browser) + def test_no_enrollments(self): """Verify that no cards appear when the user has no enrollments.""" self.stub_api() @@ -76,8 +80,12 @@ class ProgramListingPageTest(ProgramListingPageBase): @attr('a11y') -class ProgramListingPageA11yTest(ProgramListingPageBase): +class ProgramListingPageA11yTest(ProgramPageBase): """Test program listing page accessibility.""" + def setUp(self): + super(ProgramListingPageA11yTest, self).setUp() + + self.listing_page = ProgramListingPage(self.browser) def test_empty_a11y(self): """Test a11y of the page's empty state.""" @@ -100,3 +108,19 @@ class ProgramListingPageA11yTest(ProgramListingPageBase): self.assertTrue(self.listing_page.are_cards_present) self.listing_page.a11y_audit.check_for_accessibility_errors() + + +@attr('a11y') +class ProgramDetailsPageA11yTest(ProgramPageBase): + """Test program details page accessibility.""" + def setUp(self): + super(ProgramDetailsPageA11yTest, self).setUp() + + self.details_page = ProgramDetailsPage(self.browser) + + def test_a11y(self): + """Test a11y of the page's state.""" + self.auth(enroll=False) + self.details_page.visit() + + self.details_page.a11y_audit.check_for_accessibility_errors() diff --git a/lms/djangoapps/learner_dashboard/tests/test_programs.py b/lms/djangoapps/learner_dashboard/tests/test_programs.py index a447007382..11cfb12edb 100644 --- a/lms/djangoapps/learner_dashboard/tests/test_programs.py +++ b/lms/djangoapps/learner_dashboard/tests/test_programs.py @@ -8,7 +8,7 @@ from urlparse import urljoin from django.conf import settings from django.core.urlresolvers import reverse -from django.test import override_settings +from django.test import override_settings, TestCase from edx_oauth2_provider.tests.factories import ClientFactory from opaque_keys.edx import locator from provider.constants import CONFIDENTIAL @@ -205,3 +205,39 @@ class TestProgramListing( for certificate in self._expected_credentials_data(): self.assertNotContains(response, certificate['display_name']) self.assertNotContains(response, certificate['credential_url']) + + +@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') +@override_settings(MKTG_URLS={'ROOT': 'http://edx.org'}) +class TestProgramDetails(ProgramsApiConfigMixin, TestCase): + """ + Unit tests for the program details page + """ + def setUp(self): + super(TestProgramDetails, self).setUp() + self.user = UserFactory() + self.details_page = reverse('program_details_view', args=['123']) + + def test_login_required(self): + """ + Verify that login is required to access the page. + """ + self.create_programs_config() + response = self.client.get(self.details_page) + self.assertRedirects( + response, + '{}?next={}'.format(reverse('signin_user'), self.details_page) + ) + + self.client.login(username=self.user.username, password='test') + response = self.client.get(self.details_page) + self.assertEquals(response.status_code, 200) + + def test_404_if_disabled(self): + """ + Verify that the page 404s if disabled. + """ + self.create_programs_config(program_details_enabled=False) + self.client.login(username=self.user.username, password='test') + response = self.client.get(self.details_page) + self.assertEquals(response.status_code, 404) diff --git a/lms/djangoapps/learner_dashboard/urls.py b/lms/djangoapps/learner_dashboard/urls.py index 548724c14a..3a004b0251 100644 --- a/lms/djangoapps/learner_dashboard/urls.py +++ b/lms/djangoapps/learner_dashboard/urls.py @@ -6,5 +6,6 @@ from django.conf.urls import url from . import views urlpatterns = [ + url(r'^programs/(?P[0-9a-f-]+)/$', views.program_details, name='program_details_view'), url(r'^programs/$', views.view_programs, name='program_listing_view'), ] diff --git a/lms/djangoapps/learner_dashboard/views.py b/lms/djangoapps/learner_dashboard/views.py index c719583c0c..ac75b6715a 100644 --- a/lms/djangoapps/learner_dashboard/views.py +++ b/lms/djangoapps/learner_dashboard/views.py @@ -45,3 +45,20 @@ def view_programs(request): } return render_to_response('learner_dashboard/programs.html', context) + + +@login_required +@require_GET +def program_details(request, program_uuid): # pylint: disable=unused-argument + """View programs in which the user is engaged.""" + show_program_details = ProgramsApiConfig.current().show_program_details + if not show_program_details: + raise Http404 + + context = { + 'nav_hidden': True, + 'disable_courseware_js': True, + 'uses_pattern_library': True + } + + return render_to_response('learner_dashboard/program_details.html', context) diff --git a/lms/static/js/learner_dashboard/program_details_factory.js b/lms/static/js/learner_dashboard/program_details_factory.js new file mode 100644 index 0000000000..af90d4ce6a --- /dev/null +++ b/lms/static/js/learner_dashboard/program_details_factory.js @@ -0,0 +1,13 @@ +;(function (define) { + 'use strict'; + + define([ + 'js/learner_dashboard/views/program_details_view' + ], + function(ProgramDetailsView) { + return function (options) { + var ProgramDetails = new ProgramDetailsView(options); + return ProgramDetails; + }; + }); +}).call(this, define || RequireJS.define); diff --git a/lms/static/js/learner_dashboard/views/program_details_view.js b/lms/static/js/learner_dashboard/views/program_details_view.js new file mode 100644 index 0000000000..fd83ff42df --- /dev/null +++ b/lms/static/js/learner_dashboard/views/program_details_view.js @@ -0,0 +1,38 @@ +;(function (define) { + 'use strict'; + + define(['backbone', + 'jquery', + 'underscore', + 'gettext', + 'text!../../../templates/learner_dashboard/program_details_view.underscore' + ], + function( + Backbone, + $, + _, + gettext, + pageTpl + ) { + return Backbone.View.extend({ + el: '.js-program-details-wrapper', + + tpl: _.template(pageTpl), + + initialize: function(data) { + this.context = data.context; + this.render(); + }, + + render: function() { + this.$el.html(this.tpl(this.context)); + this.postRender(); + }, + + postRender: function() { + // Add subviews + } + }); + } + ); +}).call(this, define || RequireJS.define); diff --git a/lms/static/lms/js/build.js b/lms/static/lms/js/build.js index 3e19d1e1eb..1135da85e1 100644 --- a/lms/static/lms/js/build.js +++ b/lms/static/lms/js/build.js @@ -35,6 +35,7 @@ 'support/js/certificates_factory', 'support/js/enrollment_factory', 'js/bookmarks/bookmarks_factory', + 'js/learner_dashboard/program_details_factory', 'js/learner_dashboard/program_list_factory', 'js/api_admin/catalog_preview_factory' ]), diff --git a/lms/templates/learner_dashboard/program_details.html b/lms/templates/learner_dashboard/program_details.html new file mode 100644 index 0000000000..7999fb2a81 --- /dev/null +++ b/lms/templates/learner_dashboard/program_details.html @@ -0,0 +1,24 @@ +## Override the default styles_version to the Pattern Library version (version 2) +<%! main_css = "style-learner-dashboard" %> + +<%page expression_filter="h"/> +<%inherit file="../main.html" /> +<%namespace name='static' file='../static_content.html'/> +<%! +from django.utils.translation import ugettext as _ +from openedx.core.djangolib.js_utils import ( + dump_js_escaped_json, js_escaped_string +) +%> + +<%block name="js_extra"> +<%static:require_module module_name="js/learner_dashboard/program_details_factory" class_name="ProgramDetailsFactory"> +ProgramDetailsFactory({}); + + + +<%block name="pagetitle">${_("Program Details")} + +
+
+
diff --git a/lms/templates/learner_dashboard/program_details_view.underscore b/lms/templates/learner_dashboard/program_details_view.underscore new file mode 100644 index 0000000000..660a5ffb20 --- /dev/null +++ b/lms/templates/learner_dashboard/program_details_view.underscore @@ -0,0 +1,9 @@ +
+
+

Program Title

+

Program Subtitle

+
+
+
+
+ diff --git a/lms/templates/navigation.html b/lms/templates/navigation.html index 6cbafd75e3..7c24659139 100644 --- a/lms/templates/navigation.html +++ b/lms/templates/navigation.html @@ -85,7 +85,7 @@ site_status_msg = get_site_status_msg(course_id)
  • - + ${_("Programs")}
  • diff --git a/openedx/core/djangoapps/programs/migrations/0008_programsapiconfig_program_details_enabled.py b/openedx/core/djangoapps/programs/migrations/0008_programsapiconfig_program_details_enabled.py new file mode 100644 index 0000000000..c43818f879 --- /dev/null +++ b/openedx/core/djangoapps/programs/migrations/0008_programsapiconfig_program_details_enabled.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('programs', '0007_programsapiconfig_program_listing_enabled'), + ] + + operations = [ + migrations.AddField( + model_name='programsapiconfig', + name='program_details_enabled', + field=models.BooleanField(default=False, verbose_name='Do we want to show program details pages'), + ), + ] diff --git a/openedx/core/djangoapps/programs/models.py b/openedx/core/djangoapps/programs/models.py index c1482c8fe6..affc4cdeee 100644 --- a/openedx/core/djangoapps/programs/models.py +++ b/openedx/core/djangoapps/programs/models.py @@ -84,6 +84,11 @@ class ProgramsApiConfig(ConfigurationModel): default=False ) + program_details_enabled = models.BooleanField( + verbose_name=_("Do we want to show program details pages"), + default=False + ) + @property def internal_api_url(self): """ @@ -156,3 +161,10 @@ class ProgramsApiConfig(ConfigurationModel): Indicates whether we want to show program listing page """ return self.enabled and self.program_listing_enabled + + @property + def show_program_details(self): + """ + Indicates whether we want to show program details pages + """ + return self.enabled and self.program_details_enabled diff --git a/openedx/core/djangoapps/programs/tests/mixins.py b/openedx/core/djangoapps/programs/tests/mixins.py index 616fbe0621..4377198153 100644 --- a/openedx/core/djangoapps/programs/tests/mixins.py +++ b/openedx/core/djangoapps/programs/tests/mixins.py @@ -22,6 +22,7 @@ class ProgramsApiConfigMixin(object): 'enable_studio_tab': True, 'enable_certification': True, 'xseries_ad_enabled': True, + 'program_details_enabled': True, } def create_programs_config(self, **kwargs): diff --git a/themes/edx.org/lms/templates/header.html b/themes/edx.org/lms/templates/header.html index f1fde78a41..0f8b50a6ad 100644 --- a/themes/edx.org/lms/templates/header.html +++ b/themes/edx.org/lms/templates/header.html @@ -92,7 +92,7 @@ site_status_msg = get_site_status_msg(course_id)
  • - + ${_("Programs")}
  • diff --git a/themes/red-theme/lms/templates/header.html b/themes/red-theme/lms/templates/header.html index cb8fa2b61e..381e14a527 100755 --- a/themes/red-theme/lms/templates/header.html +++ b/themes/red-theme/lms/templates/header.html @@ -81,7 +81,7 @@ site_status_msg = get_site_status_msg(course_id)
  • - + ${_("Programs")}