From 0af641acee6ea8a3d43939be32e47e3c9308ba27 Mon Sep 17 00:00:00 2001 From: Kyle McCormick Date: Tue, 26 Nov 2024 10:15:27 -0500 Subject: [PATCH] feat!: Remove Studio Maintenance & Announcements The announcements editor was never ported to frontend-app-authoring, and the announcements display was never ported to frontend-app-learner-dashboard. This announcements feature is rarely used, undocumented, non-a11y-friendly, and there were no volunteers to port it to the new frontends. It is the last remaining part of the legacy Studio "Maintenance" dashboard. So, we are removing it. Note that this commit does not include a migration for the announcements Django app. So, announcements_announcement table will not be deleted. Given the small expected size of any past-authored announcements, we are not worried about leaving them in the database perpetually. Part of: https://github.com/openedx/edx-platform/issues/36263 --- .github/workflows/unit-test-shards.json | 1 - cms/djangoapps/maintenance/__init__.py | 0 cms/djangoapps/maintenance/tests.py | 194 ------------------ cms/djangoapps/maintenance/urls.py | 23 --- cms/djangoapps/maintenance/views.py | 190 ----------------- cms/envs/common.py | 2 - cms/static/sass/_build-v1.scss | 1 - cms/static/sass/views/_maintenance.scss | 104 ---------- .../maintenance/_announcement_delete.html | 40 ---- .../maintenance/_announcement_edit.html | 50 ----- .../maintenance/_announcement_index.html | 59 ------ cms/templates/maintenance/base.html | 21 -- cms/templates/maintenance/container.html | 25 --- cms/templates/maintenance/index.html | 20 -- cms/templates/widgets/user_dropdown.html | 5 - cms/urls.py | 2 - lms/static/sass/_build-lms-v1.scss | 1 - lms/static/sass/features/_announcements.scss | 28 --- lms/templates/dashboard.html | 10 - openedx/features/announcements/__init__.py | 0 openedx/features/announcements/apps.py | 32 --- openedx/features/announcements/forms.py | 20 -- .../announcements/migrations/0001_initial.py | 18 -- .../announcements/migrations/__init__.py | 0 openedx/features/announcements/models.py | 22 -- .../announcements/settings/__init__.py | 0 .../features/announcements/settings/common.py | 21 -- .../features/announcements/settings/test.py | 8 - .../announcements/jsx/Announcements.jsx | 141 ------------- .../announcements/jsx/Announcements.test.jsx | 25 --- .../__snapshots__/Announcements.test.jsx.snap | 78 ------- .../announcements/jsx/test-announcements.json | 17 -- .../features/announcements/tests/__init__.py | 0 .../announcements/tests/test_announcements.py | 95 --------- openedx/features/announcements/urls.py | 13 -- openedx/features/announcements/views.py | 37 ---- setup.py | 2 - webpack.common.config.js | 1 - 38 files changed, 1306 deletions(-) delete mode 100644 cms/djangoapps/maintenance/__init__.py delete mode 100644 cms/djangoapps/maintenance/tests.py delete mode 100644 cms/djangoapps/maintenance/urls.py delete mode 100644 cms/djangoapps/maintenance/views.py delete mode 100644 cms/static/sass/views/_maintenance.scss delete mode 100644 cms/templates/maintenance/_announcement_delete.html delete mode 100644 cms/templates/maintenance/_announcement_edit.html delete mode 100644 cms/templates/maintenance/_announcement_index.html delete mode 100644 cms/templates/maintenance/base.html delete mode 100644 cms/templates/maintenance/container.html delete mode 100644 cms/templates/maintenance/index.html delete mode 100644 lms/static/sass/features/_announcements.scss delete mode 100644 openedx/features/announcements/__init__.py delete mode 100644 openedx/features/announcements/apps.py delete mode 100644 openedx/features/announcements/forms.py delete mode 100644 openedx/features/announcements/migrations/0001_initial.py delete mode 100644 openedx/features/announcements/migrations/__init__.py delete mode 100644 openedx/features/announcements/models.py delete mode 100644 openedx/features/announcements/settings/__init__.py delete mode 100644 openedx/features/announcements/settings/common.py delete mode 100644 openedx/features/announcements/settings/test.py delete mode 100644 openedx/features/announcements/static/announcements/jsx/Announcements.jsx delete mode 100644 openedx/features/announcements/static/announcements/jsx/Announcements.test.jsx delete mode 100644 openedx/features/announcements/static/announcements/jsx/__snapshots__/Announcements.test.jsx.snap delete mode 100644 openedx/features/announcements/static/announcements/jsx/test-announcements.json delete mode 100644 openedx/features/announcements/tests/__init__.py delete mode 100644 openedx/features/announcements/tests/test_announcements.py delete mode 100644 openedx/features/announcements/urls.py delete mode 100644 openedx/features/announcements/views.py diff --git a/.github/workflows/unit-test-shards.json b/.github/workflows/unit-test-shards.json index 827366365fa8..cb9beeb3c6bf 100644 --- a/.github/workflows/unit-test-shards.json +++ b/.github/workflows/unit-test-shards.json @@ -239,7 +239,6 @@ "cms/djangoapps/course_creators/", "cms/djangoapps/export_course_metadata/", "cms/djangoapps/modulestore_migrator/", - "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 51f776b00afa..000000000000 --- a/cms/djangoapps/maintenance/tests.py +++ /dev/null @@ -1,194 +0,0 @@ -""" -Tests for the maintenance app views. -""" - - -import ddt -from django.conf import settings -from django.urls import reverse - -from common.djangoapps.student.tests.factories import AdminFactory, UserFactory -from openedx.features.announcements.models import Announcement -from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order - -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()] - - -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 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 2937727bfa39..000000000000 --- a/cms/djangoapps/maintenance/urls.py +++ /dev/null @@ -1,23 +0,0 @@ -""" -URLs for the maintenance app. -""" - -from django.urls import path, re_path - -from .views import ( - AnnouncementCreateView, - AnnouncementDeleteView, - AnnouncementEditView, - AnnouncementIndexView, - MaintenanceIndexView -) - -app_name = 'cms.djangoapps.maintenance' - -urlpatterns = [ - path('', MaintenanceIndexView.as_view(), name='maintenance_index'), - 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 1ec0f9c5cff2..000000000000 --- a/cms/djangoapps/maintenance/views.py +++ /dev/null @@ -1,190 +0,0 @@ -""" -Views for the maintenance app. -""" - - -import logging - -from django.core.validators import ValidationError -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.edx.keys import CourseKey - -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 = { - '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 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 99db8900d3c4..e219e3c48daf 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -1010,8 +1010,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/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/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 319a57bfe995..000000000000 --- a/cms/templates/maintenance/container.html +++ /dev/null @@ -1,25 +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="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 c60b56c3bd49..048339bc9fe9 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -284,8 +284,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 4d64e6768515..90c5077c1f38 100644 --- a/lms/static/sass/_build-lms-v1.scss +++ b/lms/static/sass/_build-lms-v1.scss @@ -67,7 +67,6 @@ // features @import 'features/bookmarks-v1'; -@import "features/announcements"; @import 'features/_unsupported-browser-alert'; @import 'features/content-type-gating'; @import 'features/course-duration-limits'; 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/lms/templates/dashboard.html b/lms/templates/dashboard.html index d44cdefc6389..6149418035ca 100644 --- a/lms/templates/dashboard.html +++ b/lms/templates/dashboard.html @@ -294,16 +294,6 @@

${course_dir}

% endif - - <%block name="skip_links"> - % if settings.FEATURES.get('ENABLE_ANNOUNCEMENTS'): - ${_("Skip to list of announcements")} - % endif - - % if settings.FEATURES.get('ENABLE_ANNOUNCEMENTS'): - <%include file='dashboard/_dashboard_announcements.html' /> - % endif -
diff --git a/openedx/features/announcements/__init__.py b/openedx/features/announcements/__init__.py deleted file mode 100644 index e69de29bb2d1..000000000000 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/migrations/0001_initial.py b/openedx/features/announcements/migrations/0001_initial.py deleted file mode 100644 index c959b634905e..000000000000 --- a/openedx/features/announcements/migrations/0001_initial.py +++ /dev/null @@ -1,18 +0,0 @@ -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='Announcement', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('content', models.CharField(default='lorem ipsum', max_length=1000)), - ('active', models.BooleanField(default=True)), - ], - ), - ] diff --git a/openedx/features/announcements/migrations/__init__.py b/openedx/features/announcements/migrations/__init__.py deleted file mode 100644 index e69de29bb2d1..000000000000 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 4de3740d2d27..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.ENABLE_ANNOUNCEMENTS = False - # Configure number of announcements to show per page - settings.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 8c8406d23f4b..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.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 5b9f020ac3c5..eeb7b79f534d 100644 --- a/setup.py +++ b/setup.py @@ -139,7 +139,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", @@ -158,7 +157,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 36ac75708c23..4c8e201051d4 100644 --- a/webpack.common.config.js +++ b/webpack.common.config.js @@ -134,7 +134,6 @@ module.exports = Merge.merge({ // Features 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