diff --git a/.github/workflows/unit-test-shards.json b/.github/workflows/unit-test-shards.json index 4ab126cb4715..1af8af814254 100644 --- a/.github/workflows/unit-test-shards.json +++ b/.github/workflows/unit-test-shards.json @@ -239,7 +239,6 @@ "cms/djangoapps/cms_user_tasks/", "cms/djangoapps/course_creators/", "cms/djangoapps/export_course_metadata/", - "cms/djangoapps/maintenance/", "cms/djangoapps/models/", "cms/djangoapps/pipeline_js/", "cms/djangoapps/xblock_config/", diff --git a/cms/djangoapps/maintenance/__init__.py b/cms/djangoapps/maintenance/__init__.py deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/cms/djangoapps/maintenance/tests.py b/cms/djangoapps/maintenance/tests.py deleted file mode 100644 index a487f8e37faa..000000000000 --- a/cms/djangoapps/maintenance/tests.py +++ /dev/null @@ -1,311 +0,0 @@ -""" -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 - -# 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().setUp() - self.user = AdminFactory() - login_success = self.client.login(username=self.user.username, password=self.TEST_PASSWORD) - 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().setUp() - self.user = AdminFactory() - login_success = self.client.login(username=self.user.username, password=self.TEST_PASSWORD) - 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().tearDown() - - -@ddt.ddt -class MaintenanceViewAccessTests(MaintenanceViewTestCase): - """ - Tests for access control of maintenance views. - """ - @ddt.data(*MAINTENANCE_URLS) - 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=settings.LOGIN_URL, - original_url=url, - ) - - # Studio login redirects to LMS login - self.assertRedirects(response, redirect_url, target_status_code=302) - - @ddt.data(*MAINTENANCE_URLS) - 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) - 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=self.TEST_PASSWORD) - login_success = self.client.login(username=user.username, password=self.TEST_PASSWORD) - self.assertTrue(login_success) - - response = self.client.get(url) - self.assertContains( - response, - f'Must be {settings.PLATFORM_NAME} staff to perform this action.', - status_code=403 - ) - - -@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): - """ - Tests for the announcements edit view. - """ - - def setUp(self): - super().setUp() - self.admin = AdminFactory.create( - email='staff@edx.org', - username='admin', - password=self.TEST_PASSWORD - ) - self.client.login(username=self.admin.username, password=self.TEST_PASSWORD) - self.non_staff_user = UserFactory.create( - email='test@edx.org', - username='test', - password=self.TEST_PASSWORD - ) - - def test_index(self): - """ - Test create announcement view - """ - url = reverse("maintenance:announcement_index") - response = self.client.get(url) - self.assertContains(response, '
') - - def test_create(self): - """ - Test create announcement view - """ - url = reverse("maintenance:announcement_create") - self.client.post(url, {"content": "Test Create Announcement", "active": True}) - result = Announcement.objects.filter(content="Test Create Announcement").exists() - self.assertTrue(result) - - def test_edit(self): - """ - Test edit announcement view - """ - announcement = Announcement.objects.create(content="test") - announcement.save() - url = reverse("maintenance:announcement_edit", kwargs={"pk": announcement.pk}) - response = self.client.get(url) - self.assertContains(response, '
') - self.client.post(url, {"content": "Test Edit Announcement", "active": True}) - announcement = Announcement.objects.get(pk=announcement.pk) - self.assertEqual(announcement.content, "Test Edit Announcement") - - def test_delete(self): - """ - Test delete announcement view - """ - announcement = Announcement.objects.create(content="Test Delete") - announcement.save() - url = reverse("maintenance:announcement_delete", kwargs={"pk": announcement.pk}) - self.client.post(url) - result = Announcement.objects.filter(content="Test Edit Announcement").exists() - self.assertFalse(result) - - def _test_403(self, viewname, kwargs=None): - url = reverse("maintenance:%s" % viewname, kwargs=kwargs) - response = self.client.get(url) - self.assertEqual(response.status_code, 403) - - def test_authorization(self): - self.client.login(username=self.non_staff_user, password=self.TEST_PASSWORD) - announcement = Announcement.objects.create(content="Test Delete") - announcement.save() - - self._test_403("announcement_index") - self._test_403("announcement_create") - self._test_403("announcement_edit", {"pk": announcement.pk}) - self._test_403("announcement_delete", {"pk": announcement.pk}) diff --git a/cms/djangoapps/maintenance/urls.py b/cms/djangoapps/maintenance/urls.py deleted file mode 100644 index 42febd139512..000000000000 --- a/cms/djangoapps/maintenance/urls.py +++ /dev/null @@ -1,25 +0,0 @@ -""" -URLs for the maintenance app. -""" - -from django.urls import path, re_path - -from .views import ( - AnnouncementCreateView, - AnnouncementDeleteView, - AnnouncementEditView, - AnnouncementIndexView, - ForcePublishCourseView, - MaintenanceIndexView -) - -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\d+)?$', AnnouncementIndexView.as_view(), name='announcement_index'), - path('announcements/create', AnnouncementCreateView.as_view(), name='announcement_create'), - re_path(r'^announcements/edit/(?P\d+)?$', AnnouncementEditView.as_view(), name='announcement_edit'), - path('announcements/delete/', AnnouncementDeleteView.as_view(), name='announcement_delete'), -] diff --git a/cms/djangoapps/maintenance/views.py b/cms/djangoapps/maintenance/views.py deleted file mode 100644 index 357f64e90ebd..000000000000 --- a/cms/djangoapps/maintenance/views.py +++ /dev/null @@ -1,301 +0,0 @@ -""" -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 -from openedx.features.announcements.forms import AnnouncementForm -from openedx.features.announcements.models import Announcement -from xmodule.modulestore import ModuleStoreEnum # lint-amnesty, pylint: disable=wrong-import-order -from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order -from xmodule.modulestore.exceptions import ItemNotFoundError # lint-amnesty, pylint: disable=wrong-import-order - -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'), - 'slug': 'announcement_index', - 'description': _( - 'This view shows the announcement editor to create or alter announcements that are shown on the right' - 'side of the dashboard.' - ), - }, -} - - -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): - super().__init__() - 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.headers.get('x-requested-with') == 'XMLHttpRequest': - 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().__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 - """ - - @method_decorator(require_global_staff) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) - - -class AnnouncementIndexView(ListView, MaintenanceBaseView): - """ - View for viewing the announcements shown on the dashboard, used by the global staff. - """ - model = Announcement - object_list = Announcement.objects.order_by('-active') - context_object_name = 'announcement_list' - paginate_by = 8 - - def __init__(self): - super().__init__(MAINTENANCE_VIEWS['announcement_index']) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['view'] = MAINTENANCE_VIEWS['announcement_index'] - return context - - @method_decorator(require_global_staff) - def get(self, request, *args, **kwargs): - context = self.get_context_data() - return render_to_response(self.template, context) - - -class AnnouncementEditView(UpdateView, AnnouncementBaseView): - """ - View for editing an announcement. - """ - model = Announcement - form_class = AnnouncementForm - success_url = reverse_lazy('maintenance:announcement_index') - template_name = '/maintenance/_announcement_edit.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['action_url'] = reverse('maintenance:announcement_edit', kwargs={'pk': context['announcement'].pk}) - return context - - -class AnnouncementCreateView(CreateView, AnnouncementBaseView): - """ - View for creating an announcement. - """ - model = Announcement - form_class = AnnouncementForm - success_url = reverse_lazy('maintenance:announcement_index') - template_name = '/maintenance/_announcement_edit.html' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['action_url'] = reverse('maintenance:announcement_create') - return context - - -class AnnouncementDeleteView(DeleteView, AnnouncementBaseView): - """ - View for deleting an announcement. - """ - model = Announcement - success_url = reverse_lazy('maintenance:announcement_index') - template_name = '/maintenance/_announcement_delete.html' diff --git a/cms/envs/common.py b/cms/envs/common.py index a72af4c9f37f..00a384a359c6 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -1695,8 +1695,6 @@ # New (Learning-Core-based) XBlock runtime 'openedx.core.djangoapps.xblock.apps.StudioXBlockAppConfig', - # Maintenance tools - 'cms.djangoapps.maintenance', 'openedx.core.djangoapps.util.apps.UtilConfig', # Tracking diff --git a/cms/static/js/maintenance/force_publish_course.js b/cms/static/js/maintenance/force_publish_course.js deleted file mode 100644 index 642b5bea4f2b..000000000000 --- a/cms/static/js/maintenance/force_publish_course.js +++ /dev/null @@ -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('
'), - error, - HtmlUtils.HTML('
') - ); - 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(); - }); - }); - }; -}); diff --git a/cms/static/sass/_build-v1.scss b/cms/static/sass/_build-v1.scss index 178f6b167473..7aedd0c6b8a3 100644 --- a/cms/static/sass/_build-v1.scss +++ b/cms/static/sass/_build-v1.scss @@ -77,7 +77,6 @@ @import 'views/group-configuration'; @import 'views/video-upload'; @import 'views/certificates'; -@import 'views/maintenance'; // +Base - Contexts // ==================== diff --git a/cms/static/sass/views/_maintenance.scss b/cms/static/sass/views/_maintenance.scss deleted file mode 100644 index 58d1b7494751..000000000000 --- a/cms/static/sass/views/_maintenance.scss +++ /dev/null @@ -1,104 +0,0 @@ -.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; - } - } - - .announcement-container { - width: 100%; - text-align: center; - - .announcement-item { - display: inline-block; - max-width: 300px; - min-width: 300px; - margin: 15px; - - .announcement-content { - background-color: $body-bg; - text-align: center; - padding: 22px 33px; - } - } - } -} diff --git a/cms/templates/maintenance/_announcement_delete.html b/cms/templates/maintenance/_announcement_delete.html deleted file mode 100644 index 0397ef5a0bf8..000000000000 --- a/cms/templates/maintenance/_announcement_delete.html +++ /dev/null @@ -1,40 +0,0 @@ -<%page expression_filter="h"/> -<%inherit file="base.html" /> -<%namespace name='static' file='../static_content.html'/> -<%! -from django.utils.translation import gettext as _ -from openedx.core.djangolib.markup import HTML, Text -%> -<%block name="title">${_('Delete Announcement')} -<%block name="viewtitle"> -

- ${_('Delete Announcement')} -

- - -<%block name="viewcontent"> -
-
-
- -
-
- -
-
- ## xss-lint: disable=mako-invalid-html-filter - ${object.content | n} -
-
-
- -
-
-
-
- diff --git a/cms/templates/maintenance/_announcement_edit.html b/cms/templates/maintenance/_announcement_edit.html deleted file mode 100644 index a9bee1c6fce2..000000000000 --- a/cms/templates/maintenance/_announcement_edit.html +++ /dev/null @@ -1,50 +0,0 @@ -<%page expression_filter="h"/> -<%inherit file="base.html" /> -<%namespace name='static' file='../static_content.html'/> -<%! -from django.utils.translation import gettext as _ -from openedx.core.djangolib.markup import HTML, Text -%> -<%block name="title">${_('Edit Announcement')} -<%block name="viewtitle"> -

- ${_('Edit Announcement')} -

- - -<%block name="viewcontent"> -
-
-
-
- -
- ## xss-lint: disable=mako-invalid-html-filter - ${form.as_p() | n} -
-
- -
-
-
-
-
- - -<%block name="header_extras"> - - - diff --git a/cms/templates/maintenance/_announcement_index.html b/cms/templates/maintenance/_announcement_index.html deleted file mode 100644 index 68713c9986cc..000000000000 --- a/cms/templates/maintenance/_announcement_index.html +++ /dev/null @@ -1,59 +0,0 @@ -<%page expression_filter="h"/> -<%namespace name='static' file='../static_content.html'/> -<%! -from django.urls import reverse -from django.utils.translation import gettext as _ - -from openedx.core.djangolib.markup import HTML, Text - -%> -
-
-
- % for announcement in announcement_list: -
-
- ## xss-lint: disable=mako-invalid-html-filter - ${announcement.content | n} -
- -
- - % if announcement.active: - Active
-
- % endfor -
-
- - - - % if is_paginated: - % if page_obj.has_previous(): - - - - % endif - - % if page_obj.has_next(): - - - - % endif - % endif -
-
-
diff --git a/cms/templates/maintenance/_force_publish_course.html b/cms/templates/maintenance/_force_publish_course.html deleted file mode 100644 index 31cc1e8887dc..000000000000 --- a/cms/templates/maintenance/_force_publish_course.html +++ /dev/null @@ -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 -%> -
-
- -
-
- ${_("Required data to force publish course.")} -
-
- - -
${_('course-v1:edX+DemoX+Demo_Course')}
-
-
-
-
-
-
- - - -
-
-
-
-
diff --git a/cms/templates/maintenance/base.html b/cms/templates/maintenance/base.html deleted file mode 100644 index 6979797a629c..000000000000 --- a/cms/templates/maintenance/base.html +++ /dev/null @@ -1,21 +0,0 @@ -<%page expression_filter="h"/> -<%inherit file="../base.html" /> -<%def name='online_help_token()'><% return 'maintenance' %> -<%namespace name='static' file='../static_content.html'/> -<%! -from django.urls import reverse -from django.utils.translation import gettext as _ -%> -<%block name="content"> -
-
-

- - ${_('Maintenance Dashboard')} - -

- <%block name="viewtitle"> - -
-<%block name="viewcontent"> - diff --git a/cms/templates/maintenance/container.html b/cms/templates/maintenance/container.html deleted file mode 100644 index 417471a1bd66..000000000000 --- a/cms/templates/maintenance/container.html +++ /dev/null @@ -1,33 +0,0 @@ -<%page expression_filter="h"/> -<%inherit file="base.html" /> -<%namespace name='static' file='../static_content.html'/> -<%! -from django.urls import reverse -from openedx.core.djangolib.js_utils import js_escaped_string -%> -<%block name="title">${view['name']} -<%block name="viewtitle"> -

- ${view['name']} -

- - -<%block name="viewcontent"> -
- <%include file="_${view['slug']}.html"/> -
- - -<%block name="header_extras"> -% for template_name in ["force-published-course-response"]: - -% endfor - - -<%block name="requirejs"> - require(["js/maintenance/${view['slug'] | n, js_escaped_string}"], function(MaintenanceFactory) { - MaintenanceFactory("${reverse(view['url']) | n, js_escaped_string}"); - }); - diff --git a/cms/templates/maintenance/index.html b/cms/templates/maintenance/index.html deleted file mode 100644 index 293cb90b4a9c..000000000000 --- a/cms/templates/maintenance/index.html +++ /dev/null @@ -1,20 +0,0 @@ -<%page expression_filter="h"/> -<%inherit file="base.html" /> -<%namespace name='static' file='../static_content.html'/> -<%! -from django.utils.translation import gettext as _ -from django.urls import reverse -%> -<%block name="title">${_('Maintenance Dashboard')} -<%block name="viewcontent"> -
-
    - % for view in views.values(): -
  • - ${view['name']} - ${view['description']} -
  • - % endfor -
-
- diff --git a/cms/templates/widgets/user_dropdown.html b/cms/templates/widgets/user_dropdown.html index 0ec00257ffe1..3fc0934b0db7 100644 --- a/cms/templates/widgets/user_dropdown.html +++ b/cms/templates/widgets/user_dropdown.html @@ -21,11 +21,6 @@

- % if GlobalStaff().has_user(user): - - % endif diff --git a/cms/urls.py b/cms/urls.py index d72189445883..2e64d4bbeb79 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -276,8 +276,6 @@ certificates_list_handler, name='certificates_list_handler') ] -# Maintenance Dashboard -urlpatterns.append(path('maintenance/', include('cms.djangoapps.maintenance.urls', namespace='maintenance'))) if settings.DEBUG: try: diff --git a/lms/static/sass/_build-lms-v1.scss b/lms/static/sass/_build-lms-v1.scss index 1171e3d14cdf..7a77eb34ca07 100644 --- a/lms/static/sass/_build-lms-v1.scss +++ b/lms/static/sass/_build-lms-v1.scss @@ -68,7 +68,6 @@ // features @import 'features/bookmarks-v1'; -@import "features/announcements"; @import 'features/learner-profile'; @import 'features/_unsupported-browser-alert'; @import 'features/content-type-gating'; diff --git a/lms/static/sass/features/_announcements.scss b/lms/static/sass/features/_announcements.scss deleted file mode 100644 index 0c3c01fe6077..000000000000 --- a/lms/static/sass/features/_announcements.scss +++ /dev/null @@ -1,28 +0,0 @@ -// lms - features - announcements -// ==================== -.announcements-list { - display: inline-block; - width: 100%; - - .announcement { - background-color: $course-profile-bg; - align-content: center; - text-align: center; - padding: 22px 33px; - margin-bottom: 15px; - } - - .announcement-button { - display: inline-block; - padding: 3px 10px; - font-size: 0.75rem; - } - - .prev { - float: left; - } - - .next { - float: right; - } -} diff --git a/openedx/features/announcements/apps.py b/openedx/features/announcements/apps.py deleted file mode 100644 index 4bf964cae51b..000000000000 --- a/openedx/features/announcements/apps.py +++ /dev/null @@ -1,32 +0,0 @@ -""" -Announcements Application Configuration -""" - - -from django.apps import AppConfig -from edx_django_utils.plugins import PluginURLs, PluginSettings - -from openedx.core.djangoapps.plugins.constants import ProjectType, SettingsType - - -class AnnouncementsConfig(AppConfig): - """ - Application Configuration for Announcements - """ - name = 'openedx.features.announcements' - - plugin_app = { - PluginURLs.CONFIG: { - ProjectType.LMS: { - PluginURLs.NAMESPACE: 'announcements', - PluginURLs.REGEX: '^announcements/', - PluginURLs.RELATIVE_PATH: 'urls', - } - }, - PluginSettings.CONFIG: { - ProjectType.LMS: { - SettingsType.COMMON: {PluginSettings.RELATIVE_PATH: 'settings.common'}, - SettingsType.TEST: {PluginSettings.RELATIVE_PATH: 'settings.test'}, - } - } - } diff --git a/openedx/features/announcements/forms.py b/openedx/features/announcements/forms.py deleted file mode 100644 index 879101ca37d0..000000000000 --- a/openedx/features/announcements/forms.py +++ /dev/null @@ -1,20 +0,0 @@ -""" -Forms for the Announcement Editor -""" - - -from django import forms - -from .models import Announcement - - -class AnnouncementForm(forms.ModelForm): - """ - Form for editing Announcements - """ - content = forms.CharField(widget=forms.Textarea, label='', required=False) - active = forms.BooleanField(initial=True, required=False) - - class Meta: - model = Announcement - fields = ['content', 'active'] diff --git a/openedx/features/announcements/models.py b/openedx/features/announcements/models.py deleted file mode 100644 index f58f61165db6..000000000000 --- a/openedx/features/announcements/models.py +++ /dev/null @@ -1,22 +0,0 @@ -""" -Models for Announcements -""" - - -from django.db import models - - -class Announcement(models.Model): - """ - Site-wide announcements to be displayed on the dashboard - - .. no_pii: - """ - class Meta: - app_label = 'announcements' - - content = models.CharField(max_length=1000, null=False, default="lorem ipsum") - active = models.BooleanField(default=True) - - def __str__(self): - return self.content diff --git a/openedx/features/announcements/settings/__init__.py b/openedx/features/announcements/settings/__init__.py deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/openedx/features/announcements/settings/common.py b/openedx/features/announcements/settings/common.py deleted file mode 100644 index 1a1a5ca497ab..000000000000 --- a/openedx/features/announcements/settings/common.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Common settings for Announcements""" - - -def plugin_settings(settings): - """ - Common settings for Announcements - .. toggle_name: FEATURES['ENABLE_ANNOUNCEMENTS'] - .. toggle_implementation: SettingDictToggle - .. toggle_default: False - .. toggle_description: This feature can be enabled to show system wide announcements - on the sidebar of the learner dashboard. Announcements can be created by Global Staff - users on maintenance dashboard of studio. Maintenance dashboard can accessed at - https://{studio.domain}/maintenance - .. toggle_warning: TinyMCE is needed to show an editor in the studio. - .. toggle_use_cases: open_edx - .. toggle_creation_date: 2017-11-08 - .. toggle_tickets: https://github.com/openedx/edx-platform/pull/16496 - """ - settings.FEATURES['ENABLE_ANNOUNCEMENTS'] = False - # Configure number of announcements to show per page - settings.FEATURES['ANNOUNCEMENTS_PER_PAGE'] = 5 diff --git a/openedx/features/announcements/settings/test.py b/openedx/features/announcements/settings/test.py deleted file mode 100644 index 47d57ca3dcbf..000000000000 --- a/openedx/features/announcements/settings/test.py +++ /dev/null @@ -1,8 +0,0 @@ -"""Test settings for Announcements""" - - -def plugin_settings(settings): - """ - Test settings for Announcements - """ - settings.FEATURES['ENABLE_ANNOUNCEMENTS'] = True diff --git a/openedx/features/announcements/static/announcements/jsx/Announcements.jsx b/openedx/features/announcements/static/announcements/jsx/Announcements.jsx deleted file mode 100644 index 9d370883352c..000000000000 --- a/openedx/features/announcements/static/announcements/jsx/Announcements.jsx +++ /dev/null @@ -1,141 +0,0 @@ -// eslint-disable-next-line max-classes-per-file -import React from 'react'; -import ReactDOM from 'react-dom'; -import PropTypes from 'prop-types'; -import {Button} from '@edx/paragon'; -import $ from 'jquery'; - -class AnnouncementSkipLink extends React.Component { - constructor(props) { - super(props); - this.state = { - count: 0 - }; - $.get('/announcements/page/1') - .then(data => { - this.setState({ - count: data.count - }); - }); - } - - render() { - return (
{'Skip to list of ' + this.state.count + ' announcements'}
); - } -} - -// eslint-disable-next-line react/prefer-stateless-function -class Announcement extends React.Component { - render() { - return ( -
- ); - } -} - -Announcement.propTypes = { - content: PropTypes.string.isRequired, -}; - -class AnnouncementList extends React.Component { - constructor(props) { - super(props); - this.state = { - page: 1, - announcements: [], - // eslint-disable-next-line react/no-unused-state - num_pages: 0, - has_prev: false, - has_next: false, - start_index: 0, - end_index: 0, - }; - } - - retrievePage(page) { - $.get('/announcements/page/' + page) - .then(data => { - this.setState({ - announcements: data.announcements, - has_next: data.next, - has_prev: data.prev, - // eslint-disable-next-line react/no-unused-state - num_pages: data.num_pages, - count: data.count, - start_index: data.start_index, - end_index: data.end_index, - page: page - }); - }); - } - - renderPrevPage() { - this.retrievePage(this.state.page - 1); - } - - renderNextPage() { - this.retrievePage(this.state.page + 1); - } - - // eslint-disable-next-line react/no-deprecated, react/sort-comp - componentWillMount() { - this.retrievePage(this.state.page); - } - - render() { - var children = this.state.announcements.map( - // eslint-disable-next-line react/no-array-index-key - (announcement, index) => - ); - if (this.state.has_prev) { - var prev_button = ( -
-
- ); - } - if (this.state.has_next) { - var next_button = ( -
-
- ); - } - return ( -
- {children} - {prev_button} - {next_button} -
- ); - } -} - -export default class AnnouncementsView { - constructor() { - ReactDOM.render( - , - document.getElementById('announcements'), - ); - ReactDOM.render( - , - document.getElementById('announcements-skip'), - ); - } -} - -export {AnnouncementsView, AnnouncementList, AnnouncementSkipLink}; diff --git a/openedx/features/announcements/static/announcements/jsx/Announcements.test.jsx b/openedx/features/announcements/static/announcements/jsx/Announcements.test.jsx deleted file mode 100644 index 3ec55f392889..000000000000 --- a/openedx/features/announcements/static/announcements/jsx/Announcements.test.jsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import renderer from 'react-test-renderer'; -import testAnnouncements from './test-announcements.json'; - -import {AnnouncementSkipLink, AnnouncementList} from './Announcements'; - -describe('Announcements component', () => { - test('render skip link', () => { - const component = renderer.create( - , - ); - component.root.instance.setState({count: 10}); - const tree = component.toJSON(); - expect(tree).toMatchSnapshot(); - }); - - test('render test announcements', () => { - const component = renderer.create( - , - ); - component.root.instance.setState(testAnnouncements); - const tree = component.toJSON(); - expect(tree).toMatchSnapshot(); - }); -}); diff --git a/openedx/features/announcements/static/announcements/jsx/__snapshots__/Announcements.test.jsx.snap b/openedx/features/announcements/static/announcements/jsx/__snapshots__/Announcements.test.jsx.snap deleted file mode 100644 index bbf9bfaaaa69..000000000000 --- a/openedx/features/announcements/static/announcements/jsx/__snapshots__/Announcements.test.jsx.snap +++ /dev/null @@ -1,78 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Announcements component render skip link 1`] = ` -
- Skip to list of 10 announcements -
-`; - -exports[`Announcements component render test announcements 1`] = ` -
-
-
Announcement 2", - } - } - /> -
-
-
-
-
- - - 1 - 5) of 6 - -
-
-`; diff --git a/openedx/features/announcements/static/announcements/jsx/test-announcements.json b/openedx/features/announcements/static/announcements/jsx/test-announcements.json deleted file mode 100644 index d23d39303020..000000000000 --- a/openedx/features/announcements/static/announcements/jsx/test-announcements.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "announcements": [ - {"content": "Test Announcement 1"}, - {"content": "Bold Announcement 2"}, - {"content": "Test Announcement 3"}, - {"content": "Test Announcement 4"}, - {"content": "Test Announcement 5"}, - {"content": "Test Announcement 6"} - ], - "has_next": true, - "has_prev": false, - "num_pages": 2, - "count": 6, - "start_index": 1, - "end_index": 5, - "page": 1 -} diff --git a/openedx/features/announcements/tests/__init__.py b/openedx/features/announcements/tests/__init__.py deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/openedx/features/announcements/tests/test_announcements.py b/openedx/features/announcements/tests/test_announcements.py deleted file mode 100644 index 10c608b4a6cd..000000000000 --- a/openedx/features/announcements/tests/test_announcements.py +++ /dev/null @@ -1,95 +0,0 @@ -""" -Unit tests for the announcements feature. -""" - -import json -from unittest.mock import patch - -from django.conf import settings -from django.test import TestCase -from django.test.client import Client -from django.urls import reverse - -from common.djangoapps.student.tests.factories import AdminFactory -from openedx.core.djangolib.testing.utils import skip_unless_lms -from openedx.features.announcements.models import Announcement - -TEST_ANNOUNCEMENTS = [ - ("Active Announcement", True), - ("Inactive Announcement", False), - ("Another Test Announcement", True), - ("Formatted Announcement", True), - ("Other Formatted Announcement", True), -] - - -@skip_unless_lms -class TestGlobalAnnouncements(TestCase): - """ - Test Announcements in LMS - """ - - @classmethod - def setUpTestData(cls): - super().setUpTestData() - Announcement.objects.bulk_create([ - Announcement(content=content, active=active) - for content, active in TEST_ANNOUNCEMENTS - ]) - - def setUp(self): - super().setUp() - self.client = Client() - self.admin = AdminFactory.create( - email='staff@edx.org', - username='admin', - password='pass' - ) - self.client.login(username=self.admin.username, password='pass') - - @patch.dict(settings.FEATURES, {'ENABLE_ANNOUNCEMENTS': False}) - def test_feature_flag_disabled(self): - """Ensures that the default settings effectively disables the feature""" - response = self.client.get('/dashboard') - self.assertNotContains(response, 'AnnouncementsView') - self.assertNotContains(response, '
Formatted Announcement") diff --git a/openedx/features/announcements/urls.py b/openedx/features/announcements/urls.py deleted file mode 100644 index 0f0ad3a33960..000000000000 --- a/openedx/features/announcements/urls.py +++ /dev/null @@ -1,13 +0,0 @@ -""" -Defines URLs for announcements in the LMS. -""" -from django.contrib.auth.decorators import login_required -from django.urls import path - -from .views import AnnouncementsJSONView - -urlpatterns = [ - path('page/', login_required(AnnouncementsJSONView.as_view()), - name='page', - ), -] diff --git a/openedx/features/announcements/views.py b/openedx/features/announcements/views.py deleted file mode 100644 index b6657c29cc12..000000000000 --- a/openedx/features/announcements/views.py +++ /dev/null @@ -1,37 +0,0 @@ -""" -Views to show announcements. -""" - - -from django.conf import settings -from django.http import JsonResponse -from django.views.generic.list import ListView - -from .models import Announcement - - -class AnnouncementsJSONView(ListView): - """ - View returning a page of announcements for the dashboard - """ - model = Announcement - object_list = Announcement.objects.filter(active=True) - paginate_by = settings.FEATURES.get('ANNOUNCEMENTS_PER_PAGE', 5) - - def get(self, request, *args, **kwargs): - """ - Return active announcements as json - """ - context = self.get_context_data() - - announcements = [{"content": announcement.content} for announcement in context['object_list']] - result = { - "announcements": announcements, - "next": context['page_obj'].has_next(), - "prev": context['page_obj'].has_previous(), - "start_index": context['page_obj'].start_index(), - "end_index": context['page_obj'].end_index(), - "count": context['paginator'].count, - "num_pages": context['paginator'].num_pages, - } - return JsonResponse(result) diff --git a/setup.py b/setup.py index 3b8f8c59498d..3ccfe7734e33 100644 --- a/setup.py +++ b/setup.py @@ -138,7 +138,6 @@ ], "lms.djangoapp": [ "ace_common = openedx.core.djangoapps.ace_common.apps:AceCommonConfig", - "announcements = openedx.features.announcements.apps:AnnouncementsConfig", "content_libraries = openedx.core.djangoapps.content_libraries.apps:ContentLibrariesConfig", "course_apps = openedx.core.djangoapps.course_apps.apps:CourseAppsConfig", "course_live = openedx.core.djangoapps.course_live.apps:CourseLiveConfig", @@ -157,7 +156,6 @@ "program_enrollments = lms.djangoapps.program_enrollments.apps:ProgramEnrollmentsConfig", ], "cms.djangoapp": [ - "announcements = openedx.features.announcements.apps:AnnouncementsConfig", "ace_common = openedx.core.djangoapps.ace_common.apps:AceCommonConfig", "bookmarks = openedx.core.djangoapps.bookmarks.apps:BookmarksConfig", "course_live = openedx.core.djangoapps.course_live.apps:CourseLiveConfig", diff --git a/webpack.common.config.js b/webpack.common.config.js index 322e252c6ae2..de8b545c978b 100644 --- a/webpack.common.config.js +++ b/webpack.common.config.js @@ -112,7 +112,6 @@ module.exports = Merge.smart({ CourseSock: './openedx/features/course_experience/static/course_experience/js/CourseSock.js', Currency: './openedx/features/course_experience/static/course_experience/js/currency.js', - AnnouncementsView: './openedx/features/announcements/static/announcements/jsx/Announcements.jsx', CookiePolicyBanner: './common/static/js/src/CookiePolicyBanner.jsx', // Common @@ -172,19 +171,19 @@ module.exports = Merge.smart({ multiple: [ { search: defineHeader, replace: '' }, { search: defineFooter, replace: '' }, - { + { search: /(\/\* RequireJS) \*\//g, replace(match, p1, offset, string) { return p1; } }, - { + { search: /\/\* Webpack/g, replace(match, p1, offset, string) { return match + ' */'; } }, - { + { search: /text!(.*?\.underscore)/g, replace(match, p1, offset, string) { return p1; @@ -635,13 +634,13 @@ module.exports = Merge.smart({ // We used to have node: { fs: 'empty' } in this file, // that is no longer supported. Adding this based on the recommendation in // https://stackoverflow.com/questions/64361940/webpack-error-configuration-node-has-an-unknown-property-fs - // + // // With this uncommented tests fail // Tests failed in the following suites: // * lms javascript // * xmodule-webpack javascript // Error: define cannot be used indirect - // + // // fallback: { // fs: false // }