Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cms/djangoapps/contentstore/views/course.py
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,7 @@ def format_library_for_view(library):
'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'),
})


Expand Down
40 changes: 40 additions & 0 deletions cms/djangoapps/contentstore/views/program.py
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -17,8 +18,8 @@ def setUp(self):

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')

Expand All @@ -37,9 +38,12 @@ def test_programs_config_disabled(self):

@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()
Expand All @@ -64,3 +68,53 @@ def test_programs_displayed(self):
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)
11 changes: 4 additions & 7 deletions cms/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,8 @@ <h3 class="sr">${_("Page Actions")}</h3>
% 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>
Expand Down Expand Up @@ -508,8 +507,7 @@ <h3 class="title">${_('Create Your First Library')}</h3>
% 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">
Expand Down Expand Up @@ -538,8 +536,7 @@ <h3 class="title">${_("You haven't created any programs yet.")}</h3>

<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>

Expand Down
16 changes: 16 additions & 0 deletions cms/templates/program_authoring.html
Original file line number Diff line number Diff line change
@@ -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>
5 changes: 5 additions & 0 deletions cms/templates/widgets/header.html
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,11 @@ <h3 class="title"><span class="label">${_("Tools")}</span> <i class="icon fa fa-
</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>

Expand Down
16 changes: 13 additions & 3 deletions cms/urls.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -181,6 +184,13 @@
'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
Expand All @@ -201,6 +211,6 @@

# display error page templates, for testing purposes
urlpatterns += (
url(r'404', handler404),
url(r'500', handler500),
url(r'^404$', handler404),
url(r'^500$', handler500),
)
4 changes: 2 additions & 2 deletions common/test/acceptance/pages/studio/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,15 +154,15 @@ def is_new_program_button_present(self):
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):
"""
Determine if the "create your first program" button is visible under
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):
"""
Expand Down