refactor: Remove broken Force-Publish UI from Studio
Co-Authored-By: Feanil Patel <feanil@axim.org>
This commit is contained in:
committed by
Kyle McCormick
parent
569c2d9ad2
commit
6124695fdf
@@ -3,20 +3,15 @@ Tests for the maintenance app views.
|
||||
"""
|
||||
|
||||
|
||||
import json
|
||||
|
||||
import ddt
|
||||
from django.conf import settings
|
||||
from django.urls import reverse
|
||||
|
||||
from cms.djangoapps.contentstore.management.commands.utils import get_course_versions
|
||||
from common.djangoapps.student.tests.factories import AdminFactory, UserFactory
|
||||
from openedx.features.announcements.models import Announcement
|
||||
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, BlockFactory # lint-amnesty, pylint: disable=wrong-import-order
|
||||
|
||||
from .views import COURSE_KEY_ERROR_MESSAGES, MAINTENANCE_VIEWS
|
||||
from .views import MAINTENANCE_VIEWS
|
||||
|
||||
# This list contains URLs of all maintenance app views.
|
||||
MAINTENANCE_URLS = [reverse(view['url']) for view in MAINTENANCE_VIEWS.values()]
|
||||
@@ -122,118 +117,6 @@ class MaintenanceViewAccessTests(MaintenanceViewTestCase):
|
||||
)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestForcePublish(MaintenanceViewTestCase):
|
||||
"""
|
||||
Tests for the force publish view.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super().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()
|
||||
# Add some changes to course
|
||||
chapter = BlockFactory.create(category='chapter', parent_location=course.location)
|
||||
self.store.create_child(
|
||||
self.user.id,
|
||||
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_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)
|
||||
|
||||
# now course is published, we should get `already published course` error.
|
||||
self.verify_error_message(
|
||||
data={'course-id': str(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(str(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': str(course.id)
|
||||
}
|
||||
response = self.client.post(self.view_url, data=data, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||
response_data = json.loads(response.content.decode('utf-8'))
|
||||
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)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestAnnouncementsViews(MaintenanceViewTestCase):
|
||||
"""
|
||||
|
||||
@@ -9,7 +9,6 @@ from .views import (
|
||||
AnnouncementDeleteView,
|
||||
AnnouncementEditView,
|
||||
AnnouncementIndexView,
|
||||
ForcePublishCourseView,
|
||||
MaintenanceIndexView
|
||||
)
|
||||
|
||||
@@ -17,7 +16,6 @@ app_name = 'cms.djangoapps.maintenance'
|
||||
|
||||
urlpatterns = [
|
||||
path('', MaintenanceIndexView.as_view(), name='maintenance_index'),
|
||||
re_path(r'^force_publish_course/?$', ForcePublishCourseView.as_view(), name='force_publish_course'),
|
||||
re_path(r'^announcements/(?P<page>\d+)?$', AnnouncementIndexView.as_view(), name='announcement_index'),
|
||||
path('announcements/create', AnnouncementCreateView.as_view(), name='announcement_create'),
|
||||
re_path(r'^announcements/edit/(?P<pk>\d+)?$', AnnouncementEditView.as_view(), name='announcement_edit'),
|
||||
|
||||
@@ -6,17 +6,14 @@ Views for the maintenance app.
|
||||
import logging
|
||||
|
||||
from django.core.validators import ValidationError
|
||||
from django.db import transaction
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic import View
|
||||
from django.views.generic.edit import CreateView, DeleteView, UpdateView
|
||||
from django.views.generic.list import ListView
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
from cms.djangoapps.contentstore.management.commands.utils import get_course_versions
|
||||
from common.djangoapps.edxmako.shortcuts import render_to_response
|
||||
from common.djangoapps.util.json_request import JsonResponse
|
||||
from common.djangoapps.util.views import require_global_staff
|
||||
@@ -30,16 +27,6 @@ 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'
|
||||
),
|
||||
},
|
||||
'announcement_index': {
|
||||
'url': 'maintenance:announcement_index',
|
||||
'name': _('Edit Announcements'),
|
||||
@@ -131,104 +118,6 @@ class MaintenanceBaseView(View):
|
||||
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().__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': str(versions['draft-branch']),
|
||||
'published-branch': str(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'] = str(exc)
|
||||
except ValidationError as exc:
|
||||
self.context['error'] = True
|
||||
self.context['msg'] = str(exc)
|
||||
|
||||
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()
|
||||
|
||||
|
||||
class AnnouncementBaseView(View):
|
||||
"""
|
||||
Base view for Announcements pages
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
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 = HtmlUtils.joinHtml(
|
||||
HtmlUtils.HTML('<div class="error" aria-live="polite" id="course-id-error">'),
|
||||
error,
|
||||
HtmlUtils.HTML('</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();
|
||||
});
|
||||
});
|
||||
};
|
||||
});
|
||||
@@ -1,33 +0,0 @@
|
||||
<%page expression_filter="h"/>
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
<%!
|
||||
from django.utils.translation import gettext 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>
|
||||
@@ -18,14 +18,6 @@ from openedx.core.djangolib.js_utils import js_escaped_string
|
||||
</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}");
|
||||
|
||||
Reference in New Issue
Block a user