Add a Studio view and template to host 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). ECOM-2598.
This commit is contained in:
@@ -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'),
|
||||
})
|
||||
|
||||
|
||||
|
||||
40
cms/djangoapps/contentstore/views/program.py
Normal file
40
cms/djangoapps/contentstore/views/program.py
Normal file
@@ -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
|
||||
@@ -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)
|
||||
@@ -35,9 +35,8 @@
|
||||
% endif
|
||||
|
||||
% if is_programs_enabled:
|
||||
<!-- TODO: Link to the program creation view in the authoring app. -->
|
||||
<button class="button new-button new-program-button"><i class="icon fa fa-plus icon-inline"></i>
|
||||
${_("New Program")}</button>
|
||||
<a href=${program_authoring_url + 'new'} class="button new-button new-program-button"><i class="icon fa fa-plus icon-inline"></i>
|
||||
${_("New Program")}</a>
|
||||
% endif
|
||||
</li>
|
||||
</ul>
|
||||
@@ -508,8 +507,7 @@
|
||||
% for program in programs:
|
||||
<li class="course-item">
|
||||
|
||||
<!-- TODO: Use the program ID contained in the dict to link to the appropriate view in the authoring app. -->
|
||||
<a class="program-link" href="#">
|
||||
<a class="program-link" href=${program_authoring_url + str(program['id'])}>
|
||||
<h3 class="course-title">${program['name'] | h}</h3>
|
||||
|
||||
<div class="course-metadata">
|
||||
@@ -538,8 +536,7 @@
|
||||
|
||||
<ul class="list-actions">
|
||||
<li class="action-item">
|
||||
<!-- TODO: Link to the program creation view in the authoring app. -->
|
||||
<button class="action-primary action-create new-button action-create-program new-program-button"><i class="icon fa fa-plus icon-inline"></i> ${_('Create Your First Program')}</button>
|
||||
<a href=${program_authoring_url + 'new'} class="action-primary action-create new-button action-create-program new-program-button"><i class="icon fa fa-plus icon-inline"></i> ${_('Create Your First Program')}</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
16
cms/templates/program_authoring.html
Normal file
16
cms/templates/program_authoring.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
|
||||
<%inherit file="base.html" />
|
||||
<%block name="title">${_("Program Administration")}</%block>
|
||||
|
||||
<%block name="header_extras">
|
||||
<link rel="stylesheet" href=${authoring_app_config.css_url}>
|
||||
</%block>
|
||||
|
||||
<%block name="requirejs">
|
||||
require(['${authoring_app_config.js_url}'], function () {});
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
<div class="js-program-admin program-app layout-1q3q layout-reversed" data-api-url=${programs_api_url} data-home-url=${studio_home_url}></div>
|
||||
</%block>
|
||||
@@ -187,6 +187,11 @@
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
% elif show_programs_header:
|
||||
<h2 class="info-course">
|
||||
<span class="course-org">${settings.PLATFORM_NAME}</span><span class="course-number">${_("Programs")}</span>
|
||||
<span class="course-title">${_("Program Administration")}</span>
|
||||
</h2>
|
||||
% endif
|
||||
</div>
|
||||
|
||||
|
||||
16
cms/urls.py
16
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),
|
||||
)
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user