Commit 95b16dba by Mushtaq Ali Committed by GitHub

Merge pull request #13183 from edx/mushtaq/maintenance

Maintenance app RequireJS
parents 7a071c1c fb2f9948
...@@ -26,7 +26,7 @@ from course_modes.tests.factories import CourseModeFactory ...@@ -26,7 +26,7 @@ from course_modes.tests.factories import CourseModeFactory
from contentstore.views.certificates import CertificateManager from contentstore.views.certificates import CertificateManager
from django.test.utils import override_settings from django.test.utils import override_settings
from contentstore.utils import get_lms_link_for_certificate_web_view from contentstore.utils import get_lms_link_for_certificate_web_view
from util.testing import EventTestMixin from util.testing import EventTestMixin, UrlResetMixin
FEATURES_WITH_CERTS_ENABLED = settings.FEATURES.copy() FEATURES_WITH_CERTS_ENABLED = settings.FEATURES.copy()
FEATURES_WITH_CERTS_ENABLED['CERTIFICATES_HTML_VIEW'] = True FEATURES_WITH_CERTS_ENABLED['CERTIFICATES_HTML_VIEW'] = True
...@@ -197,7 +197,9 @@ class CertificatesBaseTestCase(object): ...@@ -197,7 +197,9 @@ class CertificatesBaseTestCase(object):
@ddt.ddt @ddt.ddt
@override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED)
class CertificatesListHandlerTestCase(EventTestMixin, CourseTestCase, CertificatesBaseTestCase, HelperMethods): class CertificatesListHandlerTestCase(
EventTestMixin, CourseTestCase, CertificatesBaseTestCase, HelperMethods, UrlResetMixin
):
""" """
Test cases for certificates_list_handler. Test cases for certificates_list_handler.
""" """
...@@ -206,6 +208,7 @@ class CertificatesListHandlerTestCase(EventTestMixin, CourseTestCase, Certificat ...@@ -206,6 +208,7 @@ class CertificatesListHandlerTestCase(EventTestMixin, CourseTestCase, Certificat
Set up CertificatesListHandlerTestCase. Set up CertificatesListHandlerTestCase.
""" """
super(CertificatesListHandlerTestCase, self).setUp('contentstore.views.certificates.tracker') super(CertificatesListHandlerTestCase, self).setUp('contentstore.views.certificates.tracker')
self.reset_urls()
def _url(self): def _url(self):
""" """
...@@ -420,7 +423,9 @@ class CertificatesListHandlerTestCase(EventTestMixin, CourseTestCase, Certificat ...@@ -420,7 +423,9 @@ class CertificatesListHandlerTestCase(EventTestMixin, CourseTestCase, Certificat
@ddt.ddt @ddt.ddt
@override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED)
class CertificatesDetailHandlerTestCase(EventTestMixin, CourseTestCase, CertificatesBaseTestCase, HelperMethods): class CertificatesDetailHandlerTestCase(
EventTestMixin, CourseTestCase, CertificatesBaseTestCase, HelperMethods, UrlResetMixin
):
""" """
Test cases for CertificatesDetailHandlerTestCase. Test cases for CertificatesDetailHandlerTestCase.
""" """
...@@ -432,6 +437,7 @@ class CertificatesDetailHandlerTestCase(EventTestMixin, CourseTestCase, Certific ...@@ -432,6 +437,7 @@ class CertificatesDetailHandlerTestCase(EventTestMixin, CourseTestCase, Certific
Set up CertificatesDetailHandlerTestCase. Set up CertificatesDetailHandlerTestCase.
""" """
super(CertificatesDetailHandlerTestCase, self).setUp('contentstore.views.certificates.tracker') super(CertificatesDetailHandlerTestCase, self).setUp('contentstore.views.certificates.tracker')
self.reset_urls()
def _url(self, cid=-1): def _url(self, cid=-1):
""" """
......
...@@ -8,13 +8,14 @@ from django.test.utils import override_settings ...@@ -8,13 +8,14 @@ from django.test.utils import override_settings
from contentstore.tests.utils import CourseTestCase from contentstore.tests.utils import CourseTestCase
from contentstore.utils import reverse_course_url from contentstore.utils import reverse_course_url
from util.testing import UrlResetMixin
FEATURES_WITH_CERTS_ENABLED = settings.FEATURES.copy() FEATURES_WITH_CERTS_ENABLED = settings.FEATURES.copy()
FEATURES_WITH_CERTS_ENABLED['CERTIFICATES_HTML_VIEW'] = True FEATURES_WITH_CERTS_ENABLED['CERTIFICATES_HTML_VIEW'] = True
@override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED)
class TestHeaderMenu(CourseTestCase): class TestHeaderMenu(CourseTestCase, UrlResetMixin):
""" """
Unit tests for the course header menu. Unit tests for the course header menu.
""" """
...@@ -23,6 +24,7 @@ class TestHeaderMenu(CourseTestCase): ...@@ -23,6 +24,7 @@ class TestHeaderMenu(CourseTestCase):
Set up the for the course header menu tests. Set up the for the course header menu tests.
""" """
super(TestHeaderMenu, self).setUp() super(TestHeaderMenu, self).setUp()
self.reset_urls()
def test_header_menu_without_web_certs_enabled(self): def test_header_menu_without_web_certs_enabled(self):
""" """
......
"""
Tests for the maintenance app views.
"""
import ddt
import json
from django.conf import settings
from django.core.urlresolvers import reverse
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from contentstore.management.commands.utils import get_course_versions
from student.tests.factories import AdminFactory, UserFactory
from .views import COURSE_KEY_ERROR_MESSAGES, MAINTENANCE_VIEWS
# This list contains URLs of all maintenance app views.
MAINTENANCE_URLS = [reverse(view['url']) for view in MAINTENANCE_VIEWS.values()]
class TestMaintenanceIndex(ModuleStoreTestCase):
"""
Tests for maintenance index view.
"""
def setUp(self):
super(TestMaintenanceIndex, self).setUp()
self.user = AdminFactory()
login_success = self.client.login(username=self.user.username, password='test')
self.assertTrue(login_success)
self.view_url = reverse('maintenance:maintenance_index')
def test_maintenance_index(self):
"""
Test that maintenance index view lists all the maintenance app views.
"""
response = self.client.get(self.view_url)
self.assertContains(response, 'Maintenance', status_code=200)
# Check that all the expected links appear on the index page.
for url in MAINTENANCE_URLS:
self.assertContains(response, url, status_code=200)
@ddt.ddt
class MaintenanceViewTestCase(ModuleStoreTestCase):
"""
Base class for maintenance view tests.
"""
view_url = ''
def setUp(self):
super(MaintenanceViewTestCase, self).setUp()
self.user = AdminFactory()
login_success = self.client.login(username=self.user.username, password='test')
self.assertTrue(login_success)
def verify_error_message(self, data, error_message):
"""
Verify the response contains error message.
"""
response = self.client.post(self.view_url, data=data, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertContains(response, error_message, status_code=200)
def tearDown(self):
"""
Reverse the setup.
"""
self.client.logout()
super(MaintenanceViewTestCase, self).tearDown()
@ddt.ddt
class MaintenanceViewAccessTests(MaintenanceViewTestCase):
"""
Tests for access control of maintenance views.
"""
@ddt.data(MAINTENANCE_URLS)
@ddt.unpack
def test_require_login(self, url):
"""
Test that maintenance app requires user login.
"""
# Log out then try to retrieve the page
self.client.logout()
response = self.client.get(url)
# Expect a redirect to the login page
redirect_url = '{login_url}?next={original_url}'.format(
login_url=reverse('login'),
original_url=url,
)
self.assertRedirects(response, redirect_url)
@ddt.data(MAINTENANCE_URLS)
@ddt.unpack
def test_global_staff_access(self, url):
"""
Test that all maintenance app views are accessible to global staff user.
"""
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
@ddt.data(MAINTENANCE_URLS)
@ddt.unpack
def test_non_global_staff_access(self, url):
"""
Test that all maintenance app views are not accessible to non-global-staff user.
"""
user = UserFactory(username='test', email='test@example.com', password='test')
login_success = self.client.login(username=user.username, password='test')
self.assertTrue(login_success)
response = self.client.get(url)
self.assertContains(
response,
'Must be {platform_name} staff to perform this action.'.format(platform_name=settings.PLATFORM_NAME),
status_code=403
)
@ddt.ddt
class TestForcePublish(MaintenanceViewTestCase):
"""
Tests for the force publish view.
"""
def setUp(self):
super(TestForcePublish, self).setUp()
self.view_url = reverse('maintenance:force_publish_course')
def setup_test_course(self):
"""
Creates the course and add some changes to it.
Returns:
course: a course object
"""
course = CourseFactory.create(default_store=ModuleStoreEnum.Type.split)
# Add some changes to course
chapter = ItemFactory.create(category='chapter', parent_location=course.location)
self.store.create_child(
self.user.id, # pylint: disable=no-member
chapter.location,
'html',
block_id='html_component'
)
# verify that course has changes.
self.assertTrue(self.store.has_changes(self.store.get_item(course.location)))
return course
@ddt.data(
('', COURSE_KEY_ERROR_MESSAGES['empty_course_key']),
('edx', COURSE_KEY_ERROR_MESSAGES['invalid_course_key']),
('course-v1:e+d+X', COURSE_KEY_ERROR_MESSAGES['course_key_not_found']),
)
@ddt.unpack
def test_invalid_course_key_messages(self, course_key, error_message):
"""
Test all error messages for invalid course keys.
"""
# validate that course key contains error message
self.verify_error_message(
data={'course-id': course_key},
error_message=error_message
)
def test_mongo_course(self):
"""
Test that we get a error message on old mongo courses.
"""
# validate non split error message
course = CourseFactory.create(default_store=ModuleStoreEnum.Type.mongo)
self.verify_error_message(
data={'course-id': unicode(course.id)},
error_message='Force publishing course is not supported with old mongo courses.'
)
def test_already_published(self):
"""
Test that when a course is forcefully publish, we get a 'course is already published' message.
"""
course = self.setup_test_course()
# publish the course
source_store = modulestore()._get_modulestore_for_courselike(course.id) # pylint: disable=protected-access
source_store.force_publish_course(course.id, self.user.id, commit=True) # pylint: disable=no-member
# now course is published, we should get `already published course` error.
self.verify_error_message(
data={'course-id': unicode(course.id)},
error_message='Course is already in published state.'
)
def verify_versions_are_different(self, course):
"""
Verify draft and published versions point to different locations.
Arguments:
course (object): a course object.
"""
# get draft and publish branch versions
versions = get_course_versions(unicode(course.id))
# verify that draft and publish point to different versions
self.assertNotEqual(versions['draft-branch'], versions['published-branch'])
def get_force_publish_course_response(self, course):
"""
Get force publish the course response.
Arguments:
course (object): a course object.
Returns:
response : response from force publish post view.
"""
# Verify versions point to different locations initially
self.verify_versions_are_different(course)
# force publish course view
data = {
'course-id': unicode(course.id)
}
response = self.client.post(self.view_url, data=data, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
response_data = json.loads(response.content)
return response_data
def test_force_publish_dry_run(self):
"""
Test that dry run does not publishes the course but shows possible outcome if force published is executed.
"""
course = self.setup_test_course()
response = self.get_force_publish_course_response(course)
self.assertIn('current_versions', response)
# verify that course still has changes as we just dry ran force publish course.
self.assertTrue(self.store.has_changes(self.store.get_item(course.location)))
# verify that both branch versions are still different
self.verify_versions_are_different(course)
"""
URLs for the maintenance app.
"""
from django.conf.urls import patterns, url
from .views import MaintenanceIndexView, ForcePublishCourseView
urlpatterns = patterns(
'',
url(r'^$', MaintenanceIndexView.as_view(), name='maintenance_index'),
url(r'^force_publish_course/?$', ForcePublishCourseView.as_view(), name='force_publish_course'),
)
"""
Views for the maintenance app.
"""
import logging
from django.db import transaction
from django.core.validators import ValidationError
from django.utils.decorators import method_decorator
from django.utils.translation import ugettext as _
from django.views.generic import View
from edxmako.shortcuts import render_to_response
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError
from contentstore.management.commands.utils import get_course_versions
from util.json_request import JsonResponse
from util.views import require_global_staff
log = logging.getLogger(__name__)
# This dict maintains all the views that will be used Maintenance app.
MAINTENANCE_VIEWS = {
'force_publish_course': {
'url': 'maintenance:force_publish_course',
'name': _('Force Publish Course'),
'slug': 'force_publish_course',
'description': _(
'Sometimes the draft and published branches of a course can get out of sync. Force publish course command '
'resets the published branch of a course to point to the draft branch, effectively force publishing the '
'course. This view dry runs the force publish command'
),
},
}
COURSE_KEY_ERROR_MESSAGES = {
'empty_course_key': _('Please provide course id.'),
'invalid_course_key': _('Invalid course key.'),
'course_key_not_found': _('No matching course found.')
}
class MaintenanceIndexView(View):
"""
Index view for maintenance dashboard, used by global staff.
This view lists some commands/tasks that can be used to dry run or execute directly.
"""
@method_decorator(require_global_staff)
def get(self, request):
"""Render the maintenance index view. """
return render_to_response('maintenance/index.html', {
'views': MAINTENANCE_VIEWS,
})
class MaintenanceBaseView(View):
"""
Base class for Maintenance views.
"""
template = 'maintenance/container.html'
def __init__(self, view=None):
self.context = {
'view': view if view else '',
'form_data': {},
'error': False,
'msg': ''
}
def render_response(self):
"""
A short method to render_to_response that renders response.
"""
if self.request.is_ajax():
return JsonResponse(self.context)
return render_to_response(self.template, self.context)
@method_decorator(require_global_staff)
def get(self, request):
"""
Render get view.
"""
return self.render_response()
def validate_course_key(self, course_key, branch=ModuleStoreEnum.BranchName.draft):
"""
Validates the course_key that would be used by maintenance app views.
Arguments:
course_key (string): a course key
branch: a course locator branch, default value is ModuleStoreEnum.BranchName.draft .
values can be either ModuleStoreEnum.BranchName.draft or ModuleStoreEnum.BranchName.published.
Returns:
course_usage_key (CourseLocator): course usage locator
"""
if not course_key:
raise ValidationError(COURSE_KEY_ERROR_MESSAGES['empty_course_key'])
course_usage_key = CourseKey.from_string(course_key)
if not modulestore().has_course(course_usage_key):
raise ItemNotFoundError(COURSE_KEY_ERROR_MESSAGES['course_key_not_found'])
# get branch specific locator
course_usage_key = course_usage_key.for_branch(branch)
return course_usage_key
class ForcePublishCourseView(MaintenanceBaseView):
"""
View for force publishing state of the course, used by the global staff.
This view uses `force_publish_course` method of modulestore which publishes the draft state of the course. After
the course has been forced published, both draft and publish draft point to same location.
"""
def __init__(self):
super(ForcePublishCourseView, self).__init__(MAINTENANCE_VIEWS['force_publish_course'])
self.context.update({
'current_versions': [],
'updated_versions': [],
'form_data': {
'course_id': '',
'is_dry_run': True
}
})
def get_course_branch_versions(self, versions):
"""
Returns a dict containing unicoded values of draft and published draft versions.
"""
return {
'draft-branch': unicode(versions['draft-branch']),
'published-branch': unicode(versions['published-branch'])
}
@transaction.atomic
@method_decorator(require_global_staff)
def post(self, request):
"""
This method force publishes a course if dry-run argument is not selected. If dry-run is selected, this view
shows possible outcome if the `force_publish_course` modulestore method is executed.
Arguments:
course_id (string): a request parameter containing course id
is_dry_run (string): a request parameter containing dry run value.
It is obtained from checkbox so it has either values 'on' or ''.
"""
course_id = request.POST.get('course-id')
self.context.update({
'form_data': {
'course_id': course_id
}
})
try:
course_usage_key = self.validate_course_key(course_id)
except InvalidKeyError:
self.context['error'] = True
self.context['msg'] = COURSE_KEY_ERROR_MESSAGES['invalid_course_key']
except ItemNotFoundError as exc:
self.context['error'] = True
self.context['msg'] = exc.message
except ValidationError as exc:
self.context['error'] = True
self.context['msg'] = exc.message
if self.context['error']:
return self.render_response()
source_store = modulestore()._get_modulestore_for_courselike(course_usage_key) # pylint: disable=protected-access
if not hasattr(source_store, 'force_publish_course'):
self.context['msg'] = _('Force publishing course is not supported with old mongo courses.')
log.warning(
'Force publishing course is not supported with old mongo courses. \
%s attempted to force publish the course %s.',
request.user,
course_id,
exc_info=True
)
return self.render_response()
current_versions = self.get_course_branch_versions(get_course_versions(course_id))
# if publish and draft are NOT different
if current_versions['published-branch'] == current_versions['draft-branch']:
self.context['msg'] = _('Course is already in published state.')
log.warning(
'Course is already in published state. %s attempted to force publish the course %s.',
request.user,
course_id,
exc_info=True
)
return self.render_response()
self.context['current_versions'] = current_versions
log.info(
'%s dry ran force publish the course %s.',
request.user,
course_id,
exc_info=True
)
return self.render_response()
...@@ -834,6 +834,9 @@ INSTALLED_APPS = ( ...@@ -834,6 +834,9 @@ INSTALLED_APPS = (
'openedx.core.djangoapps.coursetalk', # not used in cms (yet), but tests run 'openedx.core.djangoapps.coursetalk', # not used in cms (yet), but tests run
'xblock_config', 'xblock_config',
# Maintenance tools
'maintenance',
# Tracking # Tracking
'track', 'track',
'eventtracking.django.apps.EventTrackingConfig', 'eventtracking.django.apps.EventTrackingConfig',
......
define([ // jshint ignore:line
'jquery',
'underscore',
'gettext',
'common/js/components/utils/view_utils',
'edx-ui-toolkit/js/utils/string-utils',
'edx-ui-toolkit/js/utils/html-utils'
],
function($, _, gettext, ViewUtils, StringUtils, HtmlUtils) {
'use strict';
return function(maintenanceViewURL) {
var showError;
// Reset values
$('#reset-button').click(function(e) {
e.preventDefault();
$('#course-id').val('');
$('#dry-run').prop('checked', true);
// clear out result container
$('#result-container').html('');
});
showError = function(containerElSelector, error) {
var errorWrapperElSelector, errorHtml;
errorWrapperElSelector = containerElSelector + ' .wrapper-error';
errorHtml = '<div class="error" aria-live="polite" id="course-id-error">' + error + '</div>';
HtmlUtils.setHtml($(errorWrapperElSelector), HtmlUtils.HTML(errorHtml));
$(errorWrapperElSelector).css('display', 'inline-block');
$(errorWrapperElSelector).fadeOut(5000);
};
$('form#force_publish').submit(function(event) {
var attrs, forcePublishedTemplate, $submitButton, deferred, promise, data;
event.preventDefault();
// clear out result container
$('#result-container').html('');
$submitButton = $('#submit_force_publish');
deferred = new $.Deferred();
promise = deferred.promise();
data = $('#force_publish').serialize();
// disable submit button while executing.
ViewUtils.disableElementWhileRunning($submitButton, function() { return promise; });
$.ajax({
type: 'POST',
url: maintenanceViewURL,
dataType: 'json',
data: data
})
.done(function(response) {
if (response.error) {
showError('#course-id-container', response.msg);
} else {
if (response.msg) {
showError('#result-error', response.msg);
} else {
attrs = $.extend({}, response, {StringUtils: StringUtils});
forcePublishedTemplate = HtmlUtils.template(
$('#force-published-course-response-tpl').text()
);
HtmlUtils.setHtml($('#result-container'), forcePublishedTemplate(attrs));
}
}
})
.fail(function() {
// response.responseText here because it would show some strange output, it may output Traceback
// sometimes if unexpected issue arises. Better to show just internal error when getting 500 error.
showError('#result-error', gettext('Internal Server Error.'));
})
.always(function() {
deferred.resolve();
});
});
};
});
...@@ -68,6 +68,7 @@ ...@@ -68,6 +68,7 @@
@import 'views/group-configuration'; @import 'views/group-configuration';
@import 'views/video-upload'; @import 'views/video-upload';
@import 'views/certificates'; @import 'views/certificates';
@import 'views/maintenance';
// +Base - Contexts // +Base - Contexts
// ==================== // ====================
......
.maintenance-header {
text-align: center;
margin-top: 50px;
h2 {
margin-bottom: 10px;
}
}
.maintenance-content {
padding: 3rem 0;
.maintenance-list {
max-width: 1280px;
margin: 0 auto;
.view-list-container {
padding: 10px 15px;
background-color: #fff;
border-bottom: 1px solid #ddd;
&:hover {
background-color: #fafafa;
}
.view-name {
display: inline-block;
width: 20%;
float: left;
}
.view-desc {
display: inline-block;
width: 80%;
font-size: 15px;
}
}
}
.maintenance-form {
width: 60%;
margin: auto;
.result-list {
height: calc(100vh - 200px);
overflow: auto;
}
.result{
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.2);
margin-top: 15px;
padding: 15px 30px;
background: #f9f9f9;
}
li {
font-size: 13px;
line-height: 9px;
}
.actions {
text-align: right;
}
.field-radio div {
display: inline-block;
margin-right: 10px;
}
div.error {
color: #F00;
margin-top: 10px;
font-size: 13px;
}
div.head-output {
font-size: 13px;
margin-bottom: 10px;
}
div.main-output {
color: #0A0;
font-size: 15px;
}
}
}
<div class="result">
<div class="head-output">
<%- gettext('You have done a dry run of force publishing the course. Nothing has changed. Had you run it, the following course versions would have been change.') %>
</div>
<div class="main-output">
<%= StringUtils.interpolate(
gettext('The published branch version, {published}, was reset to the draft branch version, {draft}.'),
{
published: current_versions['published-branch'],
draft: current_versions['draft-branch']
})
%>
</div>
</div>
<%page expression_filter="h"/>
<%namespace name='static' file='../static_content.html'/>
<%!
from django.utils.translation import ugettext as _
from openedx.core.djangolib.markup import HTML, Text
%>
<div id="force-published-form" class="wrap-instructor-info studio-view maintenance-form">
<form id="force_publish" class="form-create" method="post">
<input type="hidden" name="csrfmiddlewaretoken" value="${csrf_token}"/>
<div class="wrapper-form">
<fieldset>
<legend class="sr">${_("Required data to force publish course.")}</legend>
<div class="list-input">
<div id="course-id-container" class="field text required">
<label for="course-id">${_('Course ID')}</label>
<input id="course-id" type="text" name="course-id" aria-describedby="course-id-desc" required />
<div id="course-id-desc" class="tip tip-stacked">${_('course-v1:edX+DemoX+Demo_Course')}</div>
<div class="wrapper-error"></div>
</div>
</div>
</fieldset>
</div>
<div class="actions">
<button type="submit" id="submit_force_publish" class="action action-primary">${_('Force Publish Course')}
</button>
<button id="reset-button" class="action action-secondary action-cancel"
aria-describedby="reset-values-desc">${_('Reset')}</button>
<span id="reset-values-desc" class="is-hidden">${_('Reset values')}</span>
</div>
</form>
<div id="result-error"><div class="wrapper-error"></div></div>
<div id="result-container" class="result-container"></div>
</div>
<%page expression_filter="h"/>
<%inherit file="../base.html" />
<%def name='online_help_token()'><% return 'maintenance' %></%def>
<%namespace name='static' file='../static_content.html'/>
<%!
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
%>
<%block name="content">
<div class="wrapper-content wrapper">
<div class="maintenance-header">
<h2>
<a href="${reverse('maintenance:maintenance_index')}">
<span>${_('Maintenance Dashboard')}</span>
</a>
</h2>
<%block name="viewtitle">
</%block>
</div>
<%block name="viewcontent"></%block>
</%block>
<%page expression_filter="h"/>
<%inherit file="base.html" />
<%namespace name='static' file='../static_content.html'/>
<%!
from django.core.urlresolvers import reverse
from openedx.core.djangolib.js_utils import js_escaped_string
%>
<%block name="title">${view['name']}</%block>
<%block name="viewtitle">
<h3 class="info-course">
<span>${view['name']}</span>
</h3>
</%block>
<%block name="viewcontent">
<section class="container maintenance-content">
<%include file="_${view['slug']}.html"/>
</section>
</%block>
<%block name="header_extras">
% for template_name in ["force-published-course-response"]:
<script type="text/template" id="${template_name}-tpl">
<%static:include path="js/maintenance/${template_name}.underscore" />
</script>
% endfor
</%block>
<%block name="requirejs">
require(["js/maintenance/${view['slug'] | n, js_escaped_string}"], function(MaintenanceFactory) {
MaintenanceFactory("${reverse(view['url']) | n, js_escaped_string}");
});
</%block>
<%page expression_filter="h"/>
<%inherit file="base.html" />
<%namespace name='static' file='../static_content.html'/>
<%!
from django.utils.translation import ugettext as _
from django.core.urlresolvers import reverse
%>
<%block name="title">${_('Maintenance Dashboard')}</%block>
<%block name="viewcontent">
<div class="container maintenance-content">
<ul class="maintenance-list">
% for view in views.values():
<li class="view-list-container">
<a class="view-name" href='${reverse(view["url"])}'>${view['name']}</a>
<span class="view-desc">${view['description']}</span>
</li>
% endfor
</ul>
</div>
</%block>
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from student.roles import GlobalStaff
%> %>
% if uses_pattern_library: % if uses_pattern_library:
...@@ -45,6 +46,11 @@ ...@@ -45,6 +46,11 @@
<li class="nav-item nav-account-dashboard"> <li class="nav-item nav-account-dashboard">
<a href="/">${_("{studio_name} Home").format(studio_name=settings.STUDIO_SHORT_NAME)}</a> <a href="/">${_("{studio_name} Home").format(studio_name=settings.STUDIO_SHORT_NAME)}</a>
</li> </li>
% if GlobalStaff().has_user(user):
<li class="nav-item">
<a href="${reverse('maintenance:maintenance_index')}">${_("Maintenance")}</a>
</li>
% endif
<li class="nav-item nav-account-signout"> <li class="nav-item nav-account-signout">
<a class="action action-signout" href="${reverse('logout')}">${_("Sign Out")}</a> <a class="action action-signout" href="${reverse('logout')}">${_("Sign Out")}</a>
</li> </li>
......
...@@ -184,6 +184,12 @@ if settings.FEATURES.get('CERTIFICATES_HTML_VIEW'): ...@@ -184,6 +184,12 @@ if settings.FEATURES.get('CERTIFICATES_HTML_VIEW'):
'contentstore.views.certificates.certificates_list_handler') 'contentstore.views.certificates.certificates_list_handler')
) )
# Maintenance Dashboard
urlpatterns += patterns(
'',
url(r'^maintenance/', include('maintenance.urls', namespace='maintenance')),
)
urlpatterns += ( urlpatterns += (
# These views use a configuration model to determine whether or not to # These views use a configuration model to determine whether or not to
# display the Programs authoring app. If disabled, a 404 is returned. # display the Programs authoring app. If disabled, a 404 is returned.
......
...@@ -4,12 +4,13 @@ import sys ...@@ -4,12 +4,13 @@ import sys
from functools import wraps from functools import wraps
from django.conf import settings from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.core.cache import caches from django.core.cache import caches
from django.core.validators import ValidationError, validate_email from django.core.validators import ValidationError, validate_email
from django.views.decorators.csrf import requires_csrf_token from django.views.decorators.csrf import requires_csrf_token
from django.views.defaults import server_error from django.views.defaults import server_error
from django.http import (Http404, HttpResponse, HttpResponseNotAllowed, from django.http import (Http404, HttpResponse, HttpResponseNotAllowed,
HttpResponseServerError) HttpResponseServerError, HttpResponseForbidden)
import dogstats_wrapper as dog_stats_api import dogstats_wrapper as dog_stats_api
from edxmako.shortcuts import render_to_response from edxmako.shortcuts import render_to_response
import zendesk import zendesk
...@@ -21,6 +22,8 @@ import track.views ...@@ -21,6 +22,8 @@ import track.views
from opaque_keys import InvalidKeyError from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from student.roles import GlobalStaff
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -44,6 +47,21 @@ def ensure_valid_course_key(view_func): ...@@ -44,6 +47,21 @@ def ensure_valid_course_key(view_func):
return inner return inner
def require_global_staff(func):
"""View decorator that requires that the user have global staff permissions. """
@wraps(func)
def wrapped(request, *args, **kwargs): # pylint: disable=missing-docstring
if GlobalStaff().has_user(request.user):
return func(request, *args, **kwargs)
else:
return HttpResponseForbidden(
u"Must be {platform_name} staff to perform this action.".format(
platform_name=settings.PLATFORM_NAME
)
)
return login_required(wrapped)
@requires_csrf_token @requires_csrf_token
def jsonable_server_error(request, template_name='500.html'): def jsonable_server_error(request, template_name='500.html'):
""" """
......
...@@ -10,12 +10,10 @@ import json ...@@ -10,12 +10,10 @@ import json
import logging import logging
import re import re
import time import time
from functools import wraps
from django.conf import settings from django.conf import settings
from django.views.decorators.csrf import ensure_csrf_cookie from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.decorators.http import require_POST, require_http_methods from django.views.decorators.http import require_POST, require_http_methods
from django.views.decorators.cache import cache_control from django.views.decorators.cache import cache_control
from django.contrib.auth.decorators import login_required
from django.core.exceptions import ValidationError, PermissionDenied from django.core.exceptions import ValidationError, PermissionDenied
from django.core.mail.message import EmailMessage from django.core.mail.message import EmailMessage
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
...@@ -31,12 +29,13 @@ import random ...@@ -31,12 +29,13 @@ import random
import unicodecsv import unicodecsv
import decimal import decimal
from student import auth from student import auth
from student.roles import GlobalStaff, CourseSalesAdminRole, CourseFinanceAdminRole from student.roles import CourseSalesAdminRole, CourseFinanceAdminRole
from util.file import ( from util.file import (
store_uploaded_file, course_and_time_based_filename_generator, store_uploaded_file, course_and_time_based_filename_generator,
FileValidationException, UniversalNewlineIterator FileValidationException, UniversalNewlineIterator
) )
from util.json_request import JsonResponse, JsonResponseBadRequest from util.json_request import JsonResponse, JsonResponseBadRequest
from util.views import require_global_staff
from instructor.views.instructor_task_helpers import extract_email_features, extract_task_features from instructor.views.instructor_task_helpers import extract_email_features, extract_task_features
from courseware.access import has_access from courseware.access import has_access
...@@ -207,21 +206,6 @@ def require_level(level): ...@@ -207,21 +206,6 @@ def require_level(level):
return decorator return decorator
def require_global_staff(func):
"""View decorator that requires that the user have global staff permissions. """
@wraps(func)
def wrapped(request, *args, **kwargs): # pylint: disable=missing-docstring
if GlobalStaff().has_user(request.user):
return func(request, *args, **kwargs)
else:
return HttpResponseForbidden(
u"Must be {platform_name} staff to perform this action.".format(
platform_name=settings.PLATFORM_NAME
)
)
return login_required(wrapped)
def require_sales_admin(func): def require_sales_admin(func):
""" """
Decorator for checking sales administrator access before executing an HTTP endpoint. This decorator Decorator for checking sales administrator access before executing an HTTP endpoint. This decorator
......
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