From 08df53e11086936ec4264dad7de4d56c7d209838 Mon Sep 17 00:00:00 2001 From: Harry Rein Date: Mon, 17 Jul 2017 13:25:34 -0400 Subject: [PATCH] Adding in course messaging to the home page. LEARNER-1894 This commit adds in course messaging for three use cases. First, when the user is not signed in, the user is shown a message that provides a link to sign in or register. If the user is signed in but not enrolled, they are given a link to do so. If the user is enrolled but the course has not yet started, they are shown a message explaining when the course starts and shown a link (not yet enabled) to add a reminder to their calendar. The implementation defines a base message class and extends it for the course home messages as well as the previously implemented page level messages. --- common/djangoapps/status/models.py | 8 +- lms/djangoapps/courseware/views/views.py | 6 +- .../sass/features/_course-experience.scss | 66 +++++++ lms/static/sass/shared-v2/_variables.scss | 4 + lms/templates/page_banner.html | 4 +- openedx/core/djangoapps/debug/views.py | 15 +- .../util/tests/test_user_messages.py | 30 ++- openedx/core/djangoapps/util/user_messages.py | 177 ++++++++++++------ .../features/course_experience/__init__.py | 17 +- .../images/home_message_author.png | Bin 0 -> 5608 bytes .../course-home-fragment.html | 4 + .../course-messages-fragment.html | 30 +++ .../tests/views/test_course_home.py | 86 +++++++-- .../course_experience/views/course_home.py | 16 +- .../views/course_home_messages.py | 126 +++++++++++++ .../images/home_message_author.png | Bin 0 -> 1020 bytes 16 files changed, 471 insertions(+), 118 deletions(-) create mode 100644 openedx/features/course_experience/static/course_experience/images/home_message_author.png create mode 100644 openedx/features/course_experience/templates/course_experience/course-messages-fragment.html create mode 100644 openedx/features/course_experience/views/course_home_messages.py create mode 100644 themes/edx.org/openedx/features/course_experience/static/course_experience/images/home_message_author.png 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 0000000000000000000000000000000000000000..1b6e095a47c5d4522ffee6858ebb849035c31898 GIT binary patch literal 5608 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
+ % 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):