diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index 680e4a5945..41a9bccf08 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -480,6 +480,7 @@ def course_listing(request): 'allow_course_reruns': settings.FEATURES.get('ALLOW_COURSE_RERUNS', True), 'is_programs_enabled': programs_config.is_studio_tab_enabled and request.user.is_staff, 'programs': programs, + 'program_authoring_url': reverse('programs'), }) diff --git a/cms/djangoapps/contentstore/views/program.py b/cms/djangoapps/contentstore/views/program.py new file mode 100644 index 0000000000..8983798612 --- /dev/null +++ b/cms/djangoapps/contentstore/views/program.py @@ -0,0 +1,40 @@ +"""Programs views for use with Studio.""" +from django.contrib.auth.decorators import login_required +from django.core.urlresolvers import reverse +from django.http import Http404 +from django.utils.decorators import method_decorator +from django.views.generic import View + +from edxmako.shortcuts import render_to_response +from openedx.core.djangoapps.programs.models import ProgramsApiConfig + + +class ProgramAuthoringView(View): + """View rendering a template which hosts the Programs authoring app. + + The Programs authoring app is a Backbone SPA maintained in a separate repository. + The app handles its own routing and provides a UI which can be used to create and + publish new Programs (e.g, XSeries). + """ + + @method_decorator(login_required) + def dispatch(self, *args, **kwargs): + """Relays requests to matching methods. + + Decorated to require login before accessing the authoring app. + """ + return super(ProgramAuthoringView, self).dispatch(*args, **kwargs) + + def get(self, request, *args, **kwargs): + """Populate the template context with values required for the authoring app to run.""" + programs_config = ProgramsApiConfig.current() + + if programs_config.is_studio_tab_enabled and request.user.is_staff: + return render_to_response('program_authoring.html', { + 'show_programs_header': programs_config.is_studio_tab_enabled, + 'authoring_app_config': programs_config.authoring_app_config, + 'programs_api_url': programs_config.public_api_url, + 'studio_home_url': reverse('home'), + }) + else: + raise Http404 diff --git a/cms/djangoapps/contentstore/tests/test_programs.py b/cms/djangoapps/contentstore/views/tests/test_programs.py similarity index 50% rename from cms/djangoapps/contentstore/tests/test_programs.py rename to cms/djangoapps/contentstore/views/tests/test_programs.py index e2af560a3f..e148109c54 100644 --- a/cms/djangoapps/contentstore/tests/test_programs.py +++ b/cms/djangoapps/contentstore/views/tests/test_programs.py @@ -1,4 +1,5 @@ """Tests covering the Programs listing on the Studio home.""" +from django.conf import settings from django.core.urlresolvers import reverse import httpretty from oauth2_provider.tests.factories import ClientFactory @@ -17,8 +18,8 @@ class TestProgramListing(ProgramsApiConfigMixin, ProgramsDataMixin, ModuleStoreT ClientFactory(name=ProgramsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL) - self.user = UserFactory(is_staff=True) - self.client.login(username=self.user.username, password='test') + self.staff = UserFactory(is_staff=True) + self.client.login(username=self.staff.username, password='test') self.studio_home = reverse('home') @@ -37,9 +38,12 @@ class TestProgramListing(ProgramsApiConfigMixin, ProgramsDataMixin, ModuleStoreT @httpretty.activate def test_programs_requires_staff(self): - """Verify that the programs tab and creation button aren't rendered unless the user has global staff.""" - self.user = UserFactory(is_staff=False) - self.client.login(username=self.user.username, password='test') + """ + Verify that the programs tab and creation button aren't rendered unless the user has + global staff permissions. + """ + student = UserFactory(is_staff=False) + self.client.login(username=student.username, password='test') self.create_config() self.mock_programs_api() @@ -64,3 +68,53 @@ class TestProgramListing(ProgramsApiConfigMixin, ProgramsDataMixin, ModuleStoreT response = self.client.get(self.studio_home) for program_name in self.PROGRAM_NAMES: self.assertIn(program_name, response.content) + + +class TestProgramAuthoringView(ProgramsApiConfigMixin, ModuleStoreTestCase): + """Verify the behavior of the program authoring app's host view.""" + def setUp(self): + super(TestProgramAuthoringView, self).setUp() + + self.staff = UserFactory(is_staff=True) + self.programs_path = reverse('programs') + + def _assert_status(self, status_code): + """Verify the status code returned by the Program authoring view.""" + response = self.client.get(self.programs_path) + self.assertEquals(response.status_code, status_code) + + return response + + def test_authoring_login_required(self): + """Verify that accessing the view requires the user to be authenticated.""" + response = self.client.get(self.programs_path) + self.assertRedirects( + response, + '{login_url}?next={programs}'.format( + login_url=settings.LOGIN_URL, + programs=self.programs_path + ) + ) + + def test_authoring_header(self): + """Verify that the header contains the expected text.""" + self.client.login(username=self.staff.username, password='test') + self.create_config() + + response = self._assert_status(200) + self.assertIn("Program Administration", response.content) + + def test_authoring_access(self): + """ + Verify that a 404 is returned if Programs authoring is disabled, or the user does not have + global staff permissions. + """ + self.client.login(username=self.staff.username, password='test') + self._assert_status(404) + + # Enable Programs authoring interface + self.create_config() + + student = UserFactory(is_staff=False) + self.client.login(username=student.username, password='test') + self._assert_status(404) diff --git a/cms/templates/index.html b/cms/templates/index.html index 7f9dc051f5..5193b015fa 100644 --- a/cms/templates/index.html +++ b/cms/templates/index.html @@ -35,9 +35,8 @@ % endif % if is_programs_enabled: - - + + ${_("New Program")} % endif @@ -508,8 +507,7 @@ % for program in programs:
  • - - +

    ${program['name'] | h}

    @@ -538,8 +536,7 @@ diff --git a/cms/templates/program_authoring.html b/cms/templates/program_authoring.html new file mode 100644 index 0000000000..36f9e0e5c8 --- /dev/null +++ b/cms/templates/program_authoring.html @@ -0,0 +1,16 @@ +<%! from django.utils.translation import ugettext as _ %> + +<%inherit file="base.html" /> +<%block name="title">${_("Program Administration")} + +<%block name="header_extras"> + + + +<%block name="requirejs"> + require(['${authoring_app_config.js_url}'], function () {}); + + +<%block name="content"> +
    + diff --git a/cms/templates/widgets/header.html b/cms/templates/widgets/header.html index 9257ac5a38..4014139a70 100644 --- a/cms/templates/widgets/header.html +++ b/cms/templates/widgets/header.html @@ -187,6 +187,11 @@
  • + % elif show_programs_header: +

    + ${settings.PLATFORM_NAME}${_("Programs")} + ${_("Program Administration")} +

    % endif diff --git a/cms/urls.py b/cms/urls.py index f282dad6b2..2f955e8414 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -1,8 +1,11 @@ from django.conf import settings from django.conf.urls import patterns, include, url - # There is a course creators admin table. from ratelimitbackend import admin + +from cms.djangoapps.contentstore.views.program import ProgramAuthoringView + + admin.autodiscover() # Pattern to match a course key or a library key @@ -181,6 +184,13 @@ if settings.FEATURES.get('CERTIFICATES_HTML_VIEW'): 'contentstore.views.certificates.certificates_list_handler') ) +urlpatterns += ( + # Drops into the Programs authoring app, which handles its own routing. + # The view uses a configuration model to determine whether or not to + # display the authoring app. If disabled, a 404 is returned. + url(r'^program/', ProgramAuthoringView.as_view(), name='programs'), +) + if settings.DEBUG: try: from .urls_dev import urlpatterns as dev_urlpatterns @@ -201,6 +211,6 @@ handler500 = 'contentstore.views.render_500' # display error page templates, for testing purposes urlpatterns += ( - url(r'404', handler404), - url(r'500', handler500), + url(r'^404$', handler404), + url(r'^500$', handler500), ) diff --git a/common/test/acceptance/pages/studio/index.py b/common/test/acceptance/pages/studio/index.py index ecef5fb1d6..3ced5bf2f9 100644 --- a/common/test/acceptance/pages/studio/index.py +++ b/common/test/acceptance/pages/studio/index.py @@ -154,7 +154,7 @@ class DashboardPageWithPrograms(DashboardPage): Determine if the "new program" button is visible in the top "nav actions" section of the page. """ - return self.q(css='.nav-actions button.new-program-button').present + return self.q(css='.nav-actions a.new-program-button').present def is_empty_list_create_button_present(self): """ @@ -162,7 +162,7 @@ class DashboardPageWithPrograms(DashboardPage): the programs tab (when the program list result is empty). """ self._click_programs_tab() - return self.q(css='div.programs-tab.active button.new-program-button').present + return self.q(css='div.programs-tab.active a.new-program-button').present def get_program_list(self): """