diff --git a/openedx/core/djangoapps/util/tests/test_user_messages.py b/openedx/core/djangoapps/util/tests/test_user_messages.py index 1cf74b305e..80758ee43d 100644 --- a/openedx/core/djangoapps/util/tests/test_user_messages.py +++ b/openedx/core/djangoapps/util/tests/test_user_messages.py @@ -3,6 +3,8 @@ Unit tests for user messages. """ +import warnings + import ddt from django.contrib.messages.middleware import MessageMiddleware from django.test import RequestFactory, TestCase @@ -76,3 +78,59 @@ class UserMessagesTestCase(TestCase): messages = list(PageLevelMessages.user_messages(self.request)) assert len(messages) == 1 assert messages[0].type == expected_message_type + + def global_message_count(self): + """ + Count the number of times the global message appears in the user messages. + """ + expected_html = """
I <3 HTML-escaping
""" + messages = list(PageLevelMessages.user_messages(self.request)) + return len(list(msg for msg in messages if expected_html in msg.message_html)) + + def test_global_message_off_by_default(self): + """Verifies feature toggle.""" + with self.settings( + GLOBAL_NOTICE_ENABLED=False, + GLOBAL_NOTICE_MESSAGE="I <3 HTML-escaping", + GLOBAL_NOTICE_TYPE='WARNING' + ): + # Missing when feature disabled + assert self.global_message_count() == 0 + + def test_global_message_persistent(self): + """Verifies global message is always included, when enabled.""" + with self.settings( + GLOBAL_NOTICE_ENABLED=True, + GLOBAL_NOTICE_MESSAGE="I <3 HTML-escaping", + GLOBAL_NOTICE_TYPE='WARNING' + ): + # Present with no other setup + assert self.global_message_count() == 1 + + # Present when other messages are present + PageLevelMessages.register_user_message(self.request, UserMessageType.INFO, "something else") + assert self.global_message_count() == 1 + + def test_global_message_error_isolation(self): + """Verifies that any setting errors don't break the page, or other messages.""" + with self.settings( + GLOBAL_NOTICE_ENABLED=True, + GLOBAL_NOTICE_MESSAGE=ThrowingMarkup(), # force an error + GLOBAL_NOTICE_TYPE='invalid' + ): + PageLevelMessages.register_user_message(self.request, UserMessageType.WARNING, "something else") + # Doesn't throw, or even interfere with other messages, + # when given invalid settings + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always') + messages = list(PageLevelMessages.user_messages(self.request)) + assert len(w) == 1 + assert str(w[0].message) == "Could not register global notice: Exception('Some random error')" + assert len(messages) == 1 + assert "something else" in messages[0].message_html + + +class ThrowingMarkup: + """Class that raises an exception if markupsafe tries to get HTML from it.""" + def __html__(self): + raise Exception("Some random error") diff --git a/openedx/core/djangoapps/util/user_messages.py b/openedx/core/djangoapps/util/user_messages.py index 1a5c6ed6cb..e6438c58a3 100644 --- a/openedx/core/djangoapps/util/user_messages.py +++ b/openedx/core/djangoapps/util/user_messages.py @@ -15,11 +15,14 @@ There are two common use cases: """ +import warnings from abc import abstractmethod from enum import Enum +from django.conf import settings from django.contrib import messages from django.utils.translation import ugettext as _ +from edx_toggles.toggles import SettingToggle from openedx.core.djangolib.markup import HTML, Text @@ -181,12 +184,54 @@ class UserMessageCollection(): return (_create_user_message(message) for message in django_messages if self.get_namespace() in message.tags) +# .. toggle_name: GLOBAL_NOTICE_ENABLED +# .. toggle_implementation: SettingToggle +# .. toggle_default: False +# .. toggle_description: When enabled, show the contents of GLOBAL_NOTICE_MESSAGE +# as a message on every page. This is intended to be used as a way of +# communicating an upcoming or currently active maintenance window or to +# warn of known site issues. HTML is not supported for the message content, +# only plaintext. Message styling can be controlled with GLOBAL_NOTICE_TYPE, +# set to one of INFO, SUCCESS, WARNING, or ERROR (defaulting to INFO). Also +# see openedx.core.djangoapps.util.maintenance_banner.add_maintenance_banner +# for a variation that only shows a message on specific views. +# .. toggle_use_cases: open_edx +# .. toggle_creation_date: 2021-09-08 +GLOBAL_NOTICE_ENABLED = SettingToggle('GLOBAL_NOTICE_ENABLED', default=False) + + class PageLevelMessages(UserMessageCollection): """ This set of messages appears as top page level messages. """ NAMESPACE = 'page_level_messages' + @classmethod + def user_messages(cls, request): + """ + Returns outstanding user messages, along with any persistent site-wide messages. + """ + msgs = list(super().user_messages(request)) + + # Add a global notice message to the returned list, if enabled. + try: + if GLOBAL_NOTICE_ENABLED.is_enabled(): + if notice_message := getattr(settings, 'GLOBAL_NOTICE_MESSAGE', None): + notice_type_str = getattr(settings, 'GLOBAL_NOTICE_TYPE', None) + # If an invalid type is given, better to show a + # message with the default type than to fail to + # show it at all. + notice_type = getattr(UserMessageType, notice_type_str, UserMessageType.INFO) + + msgs.append(UserMessage( + type=notice_type, + message_html=str(cls.get_message_html(Text(notice_message))), + )) + except BaseException as e: + warnings.warn(f"Could not register global notice: {e!r}", UserWarning) + + return msgs + @classmethod def get_message_html(cls, body_html, title=None, dismissable=False, **kwargs): """