Commit 1cee9a0d by Renzo Lucioni

Merge pull request #10714 from edx/renzo/programs-authoring-page

Add a Studio view and template to host the Programs authoring app
parents 9e1c9ab0 e2833d8f
...@@ -480,6 +480,7 @@ def course_listing(request): ...@@ -480,6 +480,7 @@ def course_listing(request):
'allow_course_reruns': settings.FEATURES.get('ALLOW_COURSE_RERUNS', True), 'allow_course_reruns': settings.FEATURES.get('ALLOW_COURSE_RERUNS', True),
'is_programs_enabled': programs_config.is_studio_tab_enabled and request.user.is_staff, 'is_programs_enabled': programs_config.is_studio_tab_enabled and request.user.is_staff,
'programs': programs, 'programs': programs,
'program_authoring_url': reverse('programs'),
}) })
......
"""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
"""Tests covering the Programs listing on the Studio home.""" """Tests covering the Programs listing on the Studio home."""
from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
import httpretty import httpretty
from oauth2_provider.tests.factories import ClientFactory from oauth2_provider.tests.factories import ClientFactory
...@@ -17,8 +18,8 @@ class TestProgramListing(ProgramsApiConfigMixin, ProgramsDataMixin, ModuleStoreT ...@@ -17,8 +18,8 @@ class TestProgramListing(ProgramsApiConfigMixin, ProgramsDataMixin, ModuleStoreT
ClientFactory(name=ProgramsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL) ClientFactory(name=ProgramsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL)
self.user = UserFactory(is_staff=True) self.staff = UserFactory(is_staff=True)
self.client.login(username=self.user.username, password='test') self.client.login(username=self.staff.username, password='test')
self.studio_home = reverse('home') self.studio_home = reverse('home')
...@@ -37,9 +38,12 @@ class TestProgramListing(ProgramsApiConfigMixin, ProgramsDataMixin, ModuleStoreT ...@@ -37,9 +38,12 @@ class TestProgramListing(ProgramsApiConfigMixin, ProgramsDataMixin, ModuleStoreT
@httpretty.activate @httpretty.activate
def test_programs_requires_staff(self): 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) Verify that the programs tab and creation button aren't rendered unless the user has
self.client.login(username=self.user.username, password='test') global staff permissions.
"""
student = UserFactory(is_staff=False)
self.client.login(username=student.username, password='test')
self.create_config() self.create_config()
self.mock_programs_api() self.mock_programs_api()
...@@ -64,3 +68,53 @@ class TestProgramListing(ProgramsApiConfigMixin, ProgramsDataMixin, ModuleStoreT ...@@ -64,3 +68,53 @@ class TestProgramListing(ProgramsApiConfigMixin, ProgramsDataMixin, ModuleStoreT
response = self.client.get(self.studio_home) response = self.client.get(self.studio_home)
for program_name in self.PROGRAM_NAMES: for program_name in self.PROGRAM_NAMES:
self.assertIn(program_name, response.content) 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 @@ ...@@ -35,9 +35,8 @@
% endif % endif
% if is_programs_enabled: % if is_programs_enabled:
<!-- TODO: Link to the program creation view in the authoring app. --> <a href=${program_authoring_url + 'new'} class="button new-button new-program-button"><i class="icon fa fa-plus icon-inline"></i>
<button class="button new-button new-program-button"><i class="icon fa fa-plus icon-inline"></i> ${_("New Program")}</a>
${_("New Program")}</button>
% endif % endif
</li> </li>
</ul> </ul>
...@@ -508,8 +507,7 @@ ...@@ -508,8 +507,7 @@
% for program in programs: % for program in programs:
<li class="course-item"> <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=${program_authoring_url + str(program['id'])}>
<a class="program-link" href="#">
<h3 class="course-title">${program['name'] | h}</h3> <h3 class="course-title">${program['name'] | h}</h3>
<div class="course-metadata"> <div class="course-metadata">
...@@ -538,8 +536,7 @@ ...@@ -538,8 +536,7 @@
<ul class="list-actions"> <ul class="list-actions">
<li class="action-item"> <li class="action-item">
<!-- TODO: Link to the program creation view in the authoring app. --> <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>
<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>
</li> </li>
</ul> </ul>
......
<%! 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 @@ ...@@ -187,6 +187,11 @@
</li> </li>
</ol> </ol>
</nav> </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 % endif
</div> </div>
......
from django.conf import settings from django.conf import settings
from django.conf.urls import patterns, include, url from django.conf.urls import patterns, include, url
# There is a course creators admin table. # There is a course creators admin table.
from ratelimitbackend import admin from ratelimitbackend import admin
from cms.djangoapps.contentstore.views.program import ProgramAuthoringView
admin.autodiscover() admin.autodiscover()
# Pattern to match a course key or a library key # Pattern to match a course key or a library key
...@@ -181,6 +184,13 @@ if settings.FEATURES.get('CERTIFICATES_HTML_VIEW'): ...@@ -181,6 +184,13 @@ if settings.FEATURES.get('CERTIFICATES_HTML_VIEW'):
'contentstore.views.certificates.certificates_list_handler') '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: if settings.DEBUG:
try: try:
from .urls_dev import urlpatterns as dev_urlpatterns from .urls_dev import urlpatterns as dev_urlpatterns
...@@ -201,6 +211,6 @@ handler500 = 'contentstore.views.render_500' ...@@ -201,6 +211,6 @@ handler500 = 'contentstore.views.render_500'
# display error page templates, for testing purposes # display error page templates, for testing purposes
urlpatterns += ( urlpatterns += (
url(r'404', handler404), url(r'^404$', handler404),
url(r'500', handler500), url(r'^500$', handler500),
) )
...@@ -154,7 +154,7 @@ class DashboardPageWithPrograms(DashboardPage): ...@@ -154,7 +154,7 @@ class DashboardPageWithPrograms(DashboardPage):
Determine if the "new program" button is visible in the top "nav Determine if the "new program" button is visible in the top "nav
actions" section of the page. 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): def is_empty_list_create_button_present(self):
""" """
...@@ -162,7 +162,7 @@ class DashboardPageWithPrograms(DashboardPage): ...@@ -162,7 +162,7 @@ class DashboardPageWithPrograms(DashboardPage):
the programs tab (when the program list result is empty). the programs tab (when the program list result is empty).
""" """
self._click_programs_tab() 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): def get_program_list(self):
""" """
......
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