diff --git a/common/djangoapps/status/models.py b/common/djangoapps/status/models.py index 894891f1b1..a4eb3bf161 100644 --- a/common/djangoapps/status/models.py +++ b/common/djangoapps/status/models.py @@ -26,10 +26,10 @@ class GlobalStatusMessage(ConfigurationModel): msg = self.message if course_key: try: - course_message = self.coursemessage_set.get(course_key=course_key) - # Don't add the message if course_message is blank. - if course_message: - msg = u"{}
{}".format(msg, course_message.message) + course_home_message = self.coursemessage_set.get(course_key=course_key) + # Don't add the message if course_home_message is blank. + if course_home_message: + msg = u"{}
{}".format(msg, course_home_message.message) except CourseMessage.DoesNotExist: # We don't have a course-specific message, so pass. pass diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index 426beaa225..7ce53d650b 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -82,7 +82,7 @@ from openedx.core.djangoapps.plugin_api.views import EdxFragmentView from openedx.core.djangoapps.programs.utils import ProgramMarketingDataExtender from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers -from openedx.core.djangoapps.util.user_messages import register_warning_message +from openedx.core.djangoapps.util.user_messages import PageLevelMessages from openedx.core.djangolib.markup import HTML, Text from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG, course_home_url_name from openedx.features.course_experience.course_tools import CourseToolsPluginManager @@ -456,7 +456,7 @@ class CourseTabView(EdxFragmentView): is_enrolled = CourseEnrollment.is_enrolled(request.user, course_key) is_staff = has_access(request.user, 'staff', course_key) if request.user.is_anonymous(): - register_warning_message( + PageLevelMessages.register_warning_message( request, Text(_("To see course content, {sign_in_link} or {register_link}.")).format( sign_in_link=HTML('{sign_in_label}').format( @@ -470,7 +470,7 @@ class CourseTabView(EdxFragmentView): ) ) elif not is_enrolled and not is_staff: - register_warning_message( + PageLevelMessages.register_warning_message( request, Text(_('You must be enrolled in the course to see course content. {enroll_link}.')).format( enroll_link=HTML('{enroll_link_label}').format( diff --git a/lms/static/sass/features/_course-experience.scss b/lms/static/sass/features/_course-experience.scss index 84464da698..e552f6a685 100644 --- a/lms/static/sass/features/_course-experience.scss +++ b/lms/static/sass/features/_course-experience.scss @@ -1,3 +1,69 @@ +// ------------------------------ +// Styling for files located in the openedx/features repository. + +// Course call to action message +.course-message { + .message-author { + display: inline-block; + width: 70px; + border-radius: $baseline*7/4; + border: 1px solid $lms-border-color; + + @media (max-width: $grid-breakpoints-md) { + display: none; + } + } + + .message-content { + position: relative; + border: 1px solid $lms-border-color; + margin: 0 $baseline $baseline/2; + padding: $baseline/2 $baseline; + border-radius: $baseline/4; + + @media (max-width: $grid-breakpoints-md) { + width: 100%; + margin: $baseline 0; + } + + &:after, &:before { + @include left(0); + bottom: 35%; + border: solid transparent; + height: 0; + width: 0; + content: " "; + position: absolute; + + @media (max-width: $grid-breakpoints-md) { + display: none; + } + } + + &:after { + @include border-right-color($white); + @include margin-left($baseline*-1+1); + border-width: $baseline/2; + } + + &:before { + @include margin-left($baseline*-1); + @include border-right-color($lms-border-color); + border-width: $baseline/2; + } + + .message-header { + font-weight: $font-semibold; + margin-bottom: $baseline/4; + } + + a { + font-weight: $font-semibold; + text-decoration: underline; + } + } +} + // Welcome message .welcome-message { border: solid 1px $lms-border-color; diff --git a/lms/static/sass/shared-v2/_variables.scss b/lms/static/sass/shared-v2/_variables.scss index 9603ac1136..e9e84343ff 100644 --- a/lms/static/sass/shared-v2/_variables.scss +++ b/lms/static/sass/shared-v2/_variables.scss @@ -11,6 +11,10 @@ // ---------------------------- $lms-max-width: 1180px !default; +$grid-breakpoints-sm: 576px !default; +$grid-breakpoints-md: 768px !default; +$grid-breakpoints-lg: 992px !default; + // ---------------------------- // #COLORS // ---------------------------- diff --git a/lms/templates/page_banner.html b/lms/templates/page_banner.html index bf1e906107..5e6dddd653 100644 --- a/lms/templates/page_banner.html +++ b/lms/templates/page_banner.html @@ -7,11 +7,11 @@ <%! from django.utils.translation import ugettext as _ from openedx.core.djangolib.markup import HTML -from openedx.core.djangoapps.util.user_messages import user_messages +from openedx.core.djangoapps.util.user_messages import PageLevelMessages %> <% -banner_messages = list(user_messages(request)) +banner_messages = list(PageLevelMessages.user_messages(request)) %> % if banner_messages: diff --git a/openedx/core/djangoapps/debug/views.py b/openedx/core/djangoapps/debug/views.py index 983ecda81a..674f7464d2 100644 --- a/openedx/core/djangoapps/debug/views.py +++ b/openedx/core/djangoapps/debug/views.py @@ -7,12 +7,7 @@ from django.http import HttpResponseNotFound from django.utils.translation import ugettext as _ from edxmako.shortcuts import render_to_response from mako.exceptions import TopLevelLookupException -from openedx.core.djangoapps.util.user_messages import ( - register_error_message, - register_info_message, - register_success_message, - register_warning_message, -) +from openedx.core.djangoapps.util.user_messages import PageLevelMessages def show_reference_template(request, template): @@ -40,10 +35,10 @@ def show_reference_template(request, template): # Add some messages to the course skeleton pages if u'course-skeleton.html' in request.path: - register_info_message(request, _('This is a test message')) - register_success_message(request, _('This is a success message')) - register_warning_message(request, _('This is a test warning')) - register_error_message(request, _('This is a test error')) + PageLevelMessages.register_info_message(request, _('This is a test message')) + PageLevelMessages.register_success_message(request, _('This is a success message')) + PageLevelMessages.register_warning_message(request, _('This is a test warning')) + PageLevelMessages.register_error_message(request, _('This is a test error')) return render_to_response(template, context) except TopLevelLookupException: diff --git a/openedx/core/djangoapps/util/tests/test_user_messages.py b/openedx/core/djangoapps/util/tests/test_user_messages.py index 6be513578c..f4376c7b80 100644 --- a/openedx/core/djangoapps/util/tests/test_user_messages.py +++ b/openedx/core/djangoapps/util/tests/test_user_messages.py @@ -10,15 +10,7 @@ from django.test import RequestFactory from openedx.core.djangolib.markup import HTML, Text from student.tests.factories import UserFactory -from ..user_messages import ( - register_error_message, - register_info_message, - register_success_message, - register_user_message, - register_warning_message, - user_messages, - UserMessageType, -) +from ..user_messages import PageLevelMessages, UserMessageType TEST_MESSAGE = 'Test message' @@ -26,7 +18,7 @@ TEST_MESSAGE = 'Test message' @ddt.ddt class UserMessagesTestCase(TestCase): """ - Unit tests for user messages. + Unit tests for page level user messages. """ def setUp(self): super(UserMessagesTestCase, self).setUp() @@ -46,8 +38,8 @@ class UserMessagesTestCase(TestCase): """ Verifies that a user message is escaped correctly. """ - register_user_message(self.request, UserMessageType.INFO, message) - messages = list(user_messages(self.request)) + PageLevelMessages.register_user_message(self.request, UserMessageType.INFO, message) + messages = list(PageLevelMessages.user_messages(self.request)) self.assertEqual(len(messages), 1) self.assertEquals(messages[0].message_html, expected_message_html) @@ -62,17 +54,17 @@ class UserMessagesTestCase(TestCase): """ Verifies that a user message returns the correct CSS and icon classes. """ - register_user_message(self.request, message_type, TEST_MESSAGE) - messages = list(user_messages(self.request)) + PageLevelMessages.register_user_message(self.request, message_type, TEST_MESSAGE) + messages = list(PageLevelMessages.user_messages(self.request)) self.assertEqual(len(messages), 1) self.assertEquals(messages[0].css_class, expected_css_class) self.assertEquals(messages[0].icon_class, expected_icon_class) @ddt.data( - (register_error_message, UserMessageType.ERROR), - (register_info_message, UserMessageType.INFO), - (register_success_message, UserMessageType.SUCCESS), - (register_warning_message, UserMessageType.WARNING), + (PageLevelMessages.register_error_message, UserMessageType.ERROR), + (PageLevelMessages.register_info_message, UserMessageType.INFO), + (PageLevelMessages.register_success_message, UserMessageType.SUCCESS), + (PageLevelMessages.register_warning_message, UserMessageType.WARNING), ) @ddt.unpack def test_message_type(self, register_message_function, expected_message_type): @@ -80,6 +72,6 @@ class UserMessagesTestCase(TestCase): Verifies that each user message function returns the correct type. """ register_message_function(self.request, TEST_MESSAGE) - messages = list(user_messages(self.request)) + messages = list(PageLevelMessages.user_messages(self.request)) self.assertEqual(len(messages), 1) self.assertEquals(messages[0].type, expected_message_type) diff --git a/openedx/core/djangoapps/util/user_messages.py b/openedx/core/djangoapps/util/user_messages.py index 251b4e9339..961d6288d4 100644 --- a/openedx/core/djangoapps/util/user_messages.py +++ b/openedx/core/djangoapps/util/user_messages.py @@ -14,12 +14,12 @@ There are two common use cases: used to show a success message to the use. """ +from abc import abstractmethod from enum import Enum from django.contrib import messages -from openedx.core.djangolib.markup import Text - -EDX_USER_MESSAGE_TAG = 'edx-user-message' +from django.utils.translation import ugettext as _ +from openedx.core.djangolib.markup import Text, HTML class UserMessageType(Enum): @@ -49,7 +49,7 @@ ICON_CLASSES = { class UserMessage(): """ - Representation of a message to be shown to a user + Representation of a message to be shown to a user. """ def __init__(self, type, message_html): assert isinstance(type, UserMessageType) @@ -67,71 +67,124 @@ class UserMessage(): def icon_class(self): """ Returns the CSS icon class representing the message type. - Returns: """ return ICON_CLASSES[self.type] -def register_user_message(request, message_type, message, title=None): +class UserMessageCollection(): """ - Register a message to be shown to the user in the next page. + A collection of messages to be shown to a user. """ - assert isinstance(message_type, UserMessageType) - messages.add_message(request, message_type.value, Text(message), extra_tags=EDX_USER_MESSAGE_TAG) - - -def register_info_message(request, message, **kwargs): - """ - Registers an information message to be shown to the user. - """ - register_user_message(request, UserMessageType.INFO, message, **kwargs) - - -def register_success_message(request, message, **kwargs): - """ - Registers a success message to be shown to the user. - """ - register_user_message(request, UserMessageType.SUCCESS, message, **kwargs) - - -def register_warning_message(request, message, **kwargs): - """ - Registers a warning message to be shown to the user. - """ - register_user_message(request, UserMessageType.WARNING, message, **kwargs) - - -def register_error_message(request, message, **kwargs): - """ - Registers an error message to be shown to the user. - """ - register_user_message(request, UserMessageType.ERROR, message, **kwargs) - - -def user_messages(request): - """ - Returns any outstanding user messages. - - Note: this function also marks these messages as being complete - so they won't be returned in the next request. - """ - def _get_message_type_for_level(level): + @classmethod + @abstractmethod + def get_namespace(self): """ - Returns the user message type associated with a level. - """ - for __, type in UserMessageType.__members__.items(): - if type.value is level: - return type - raise 'Unable to find UserMessageType for level {level}'.format(level=level) + Returns the namespace of the message collection. - def _create_user_message(message): + The name is used to namespace the subset of django messages. + For example, return 'course_home_messages'. """ - Creates a user message from a Django message. - """ - return UserMessage( - type=_get_message_type_for_level(message.level), - message_html=unicode(message.message), - ) + raise NotImplementedError('Subclasses must define a namespace for messages.') - django_messages = messages.get_messages(request) - return (_create_user_message(message) for message in django_messages if EDX_USER_MESSAGE_TAG in message.tags) + @classmethod + def get_message_html(self, body_html, title=None): + """ + Returns the entire HTML snippet for the message. + + Classes that extend this base class can override the message styling + by implementing their own version of this function. Messages that do + not use a title can just pass the body_html. + """ + if title: + return Text(_('{header_open}{title}{header_close}{body}')).format( + header_open=HTML('
'), + title=title, + body=body_html, + header_close=HTML('
') + ) + return body_html + + @classmethod + def register_user_message(self, request, message_type, body_html, title=None): + """ + Register a message to be shown to the user in the next page. + + Arguments: + message_type (UserMessageType): the user message type + body_html (str): body of the message in html format + title (str): optional title for the message as plain text + """ + assert isinstance(message_type, UserMessageType) + message = Text(self.get_message_html(body_html, title)) + messages.add_message(request, message_type.value, Text(message), extra_tags=self.get_namespace()) + + @classmethod + def register_info_message(self, request, message, **kwargs): + """ + Registers an information message to be shown to the user. + """ + self.register_user_message(request, UserMessageType.INFO, message, **kwargs) + + @classmethod + def register_success_message(self, request, message, **kwargs): + """ + Registers a success message to be shown to the user. + """ + self.register_user_message(request, UserMessageType.SUCCESS, message, **kwargs) + + @classmethod + def register_warning_message(self, request, message, **kwargs): + """ + Registers a warning message to be shown to the user. + """ + self.register_user_message(request, UserMessageType.WARNING, message, **kwargs) + + @classmethod + def register_error_message(self, request, message, **kwargs): + """ + Registers an error message to be shown to the user. + """ + self.register_user_message(request, UserMessageType.ERROR, message, **kwargs) + + @classmethod + def user_messages(self, request): + """ + Returns any outstanding user messages. + + Note: this function also marks these messages as being complete + so they won't be returned in the next request. + """ + def _get_message_type_for_level(level): + """ + Returns the user message type associated with a level. + """ + for __, type in UserMessageType.__members__.items(): + if type.value is level: + return type + raise 'Unable to find UserMessageType for level {level}'.format(level=level) + + def _create_user_message(message): + """ + Creates a user message from a Django message. + """ + return UserMessage( + type=_get_message_type_for_level(message.level), + message_html=unicode(message.message), + ) + + django_messages = messages.get_messages(request) + return (_create_user_message(message) for message in django_messages if self.get_namespace() in message.tags) + + +class PageLevelMessages(UserMessageCollection): + """ + This set of messages appears as top page level messages. + """ + NAMESPACE = 'page_level_messages' + + @classmethod + def get_namespace(self): + """ + Returns the namespace of the message collection. + """ + return self.NAMESPACE diff --git a/openedx/features/course_experience/__init__.py b/openedx/features/course_experience/__init__.py index 578c005e00..1946dc8c72 100644 --- a/openedx/features/course_experience/__init__.py +++ b/openedx/features/course_experience/__init__.py @@ -3,7 +3,8 @@ Unified course experience settings and helper methods. """ from django.utils.translation import ugettext as _ -from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag, WaffleFlag, WaffleFlagNamespace +from openedx.core.djangoapps.util.user_messages import UserMessageCollection +from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag, WaffleFlagNamespace # Namespace for course experience waffle flags. @@ -58,3 +59,17 @@ def course_home_url_name(course_key): return 'openedx.course_experience.course_home' else: return 'info' + + +class CourseHomeMessages(UserMessageCollection): + """ + This set of messages appear above the outline on the course home page. + """ + NAMESPACE = 'course_home_level_messages' + + @classmethod + def get_namespace(self): + """ + Returns the namespace of the message collection. + """ + return self.NAMESPACE diff --git a/openedx/features/course_experience/static/course_experience/images/home_message_author.png b/openedx/features/course_experience/static/course_experience/images/home_message_author.png new file mode 100644 index 0000000000..1b6e095a47 Binary files /dev/null and b/openedx/features/course_experience/static/course_experience/images/home_message_author.png differ diff --git a/openedx/features/course_experience/templates/course_experience/course-home-fragment.html b/openedx/features/course_experience/templates/course_experience/course-home-fragment.html index d68617a2d0..a11fa28750 100644 --- a/openedx/features/course_experience/templates/course_experience/course-home-fragment.html +++ b/openedx/features/course_experience/templates/course_experience/course-home-fragment.html @@ -57,6 +57,10 @@ from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG, SHOW_REV
+ % if course_home_message_fragment: + ${HTML(course_home_message_fragment.body_html())} + % endif + % if welcome_message_fragment and UNIFIED_COURSE_TAB_FLAG.is_enabled(course.id):