Commit a077a5e3 by Andy Armstrong

Add support for page banner status messages

LEARNER-1890
parent febf2a07
...@@ -388,7 +388,7 @@ class SelfPacedCourseInfoTestCase(LoginEnrollmentTestCase, SharedModuleStoreTest ...@@ -388,7 +388,7 @@ class SelfPacedCourseInfoTestCase(LoginEnrollmentTestCase, SharedModuleStoreTest
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
def test_num_queries_instructor_paced(self): def test_num_queries_instructor_paced(self):
self.fetch_course_info_with_queries(self.instructor_paced_course, 22, 3) self.fetch_course_info_with_queries(self.instructor_paced_course, 25, 3)
def test_num_queries_self_paced(self): def test_num_queries_self_paced(self):
self.fetch_course_info_with_queries(self.self_paced_course, 22, 3) self.fetch_course_info_with_queries(self.self_paced_course, 25, 3)
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
@import 'edx-pattern-library-shims/buttons'; @import 'edx-pattern-library-shims/buttons';
// base - elements // base - elements
@import 'elements/banners';
@import 'elements/controls'; @import 'elements/controls';
@import 'elements/creative-commons'; @import 'elements/creative-commons';
@import 'elements/icons'; @import 'elements/icons';
......
// Open edX: components
// ====================
// Page banner
.page-banner {
max-width: $lms-max-width;
margin: 0 auto;
.user-messages {
margin-top: $baseline;
}
}
// Alerts
.alert {
.icon-alert {
margin-right: $baseline / 4;
}
}
...@@ -13,3 +13,4 @@ ...@@ -13,3 +13,4 @@
@import 'footer'; @import 'footer';
@import 'navigation'; @import 'navigation';
@import 'layouts'; @import 'layouts';
@import 'components';
...@@ -44,3 +44,50 @@ $full-width-banner-margin: 20px; ...@@ -44,3 +44,50 @@ $full-width-banner-margin: 20px;
right: $full-width-banner-margin; right: $full-width-banner-margin;
} }
} }
.page-banner {
max-width: $lms-max-width;
margin: 0 auto;
.user-messages {
padding-top: $baseline;
}
.alert {
margin-bottom: $baseline !important;
padding: $baseline;
border: 1px solid;
.icon-alert {
margin-right: $baseline / 4;
}
&.alert-info {
color: $state-info-text;
background-color: $state-info-bg;
border-color: $state-info-border;
box-shadow: none;
}
&.alert-success {
color: $state-success-text;
background-color: $state-success-bg;
border-color: $state-success-border;
box-shadow: none;
}
&.alert-warning {
color: $state-warning-text;
background-color: $state-warning-bg;
border-color: $state-warning-border;
box-shadow: none;
}
&.alert-danger {
color: $state-danger-text;
background-color: $state-danger-bg;
border-color: $state-danger-border;
box-shadow: none;
}
}
}
...@@ -221,6 +221,26 @@ $success-color: rgb(0, 155, 0) !default; ...@@ -221,6 +221,26 @@ $success-color: rgb(0, 155, 0) !default;
$success-color-hover: rgb(0, 129, 0) !default; $success-color-hover: rgb(0, 129, 0) !default;
// ---------------------------- // ----------------------------
// #COLORS- Bootstrap-style
// ----------------------------
$state-success-text: $black !default;
$state-success-bg: #dff0d8 !default;
$state-success-border: darken($state-success-bg, 5%) !default;
$state-info-text: $black !default;
$state-info-bg: #d9edf7 !default;
$state-info-border: darken($state-info-bg, 7%) !default;
$state-warning-text: $black !default;
$state-warning-bg: #fcf8e3 !default;
$state-warning-border: darken($state-warning-bg, 5%) !default;
$state-danger-text: $black !default;
$state-danger-bg: #f2dede !default;
$state-danger-border: darken($state-danger-bg, 5%) !default;
// ----------------------------
// #COLORS- EDX-SPECIFIC // #COLORS- EDX-SPECIFIC
// ---------------------------- // ----------------------------
......
...@@ -89,6 +89,48 @@ ...@@ -89,6 +89,48 @@
} }
} }
.page-banner {
max-width: $lms-max-width;
margin: 0 auto;
.alert {
margin-top: $baseline;
border: 1px solid;
.icon-alert {
margin-right: $baseline / 4;
}
&.alert-info {
color: $state-info-text;
background-color: $state-info-bg;
border-color: $state-info-border;
box-shadow: none;
}
&.alert-success {
color: $state-success-text;
background-color: $state-success-bg;
border-color: $state-success-border;
box-shadow: none;
}
&.alert-warning {
color: $state-warning-text;
background-color: $state-warning-bg;
border-color: $state-warning-border;
box-shadow: none;
}
&.alert-danger {
color: $state-danger-text;
background-color: $state-danger-bg;
border-color: $state-danger-border;
box-shadow: none;
}
}
}
.wrapper-preview-menu { .wrapper-preview-menu {
@include clearfix(); @include clearfix();
@include box-sizing(border-box); @include box-sizing(border-box);
......
...@@ -62,3 +62,23 @@ $lms-dark-icon-background-color: palette(grayscale, black) !default; ...@@ -62,3 +62,23 @@ $lms-dark-icon-background-color: palette(grayscale, black) !default;
$site-status-color: rgb(182,37,103) !default; $site-status-color: rgb(182,37,103) !default;
$shadow-l1: rgba(0,0,0,0.1) !default; $shadow-l1: rgba(0,0,0,0.1) !default;
// ----------------------------
// #ALERTS
// ----------------------------
$state-success-text: $black !default;
$state-success-bg: #dff0d8 !default;
$state-success-border: darken($state-success-bg, 5%) !default;
$state-info-text: $black !default;
$state-info-bg: #d9edf7 !default;
$state-info-border: darken($state-info-bg, 7%) !default;
$state-warning-text: $black !default;
$state-warning-bg: #fcf8e3 !default;
$state-warning-border: darken($state-warning-bg, 5%) !default;
$state-danger-text: $black !default;
$state-danger-bg: #f2dede !default;
$state-danger-border: darken($state-danger-bg, 5%) !default;
...@@ -141,6 +141,8 @@ from pipeline_mako import render_require_js_path_overrides ...@@ -141,6 +141,8 @@ from pipeline_mako import render_require_js_path_overrides
<%include file="/preview_menu.html" /> <%include file="/preview_menu.html" />
% endif % endif
<%include file="/page_banner.html" />
<div class="content-wrapper ${"container-fluid" if uses_bootstrap else "" } main-container" id="content"> <div class="content-wrapper ${"container-fluid" if uses_bootstrap else "" } main-container" id="content">
${self.body()} ${self.body()}
<%block name="bodyextra"/> <%block name="bodyextra"/>
......
## mako
<%page expression_filter="h"/>
<%namespace name='static' file='static_content.html'/>
<% online_help_token = self.online_help_token() if hasattr(self, 'online_help_token') else None %>
<%!
from django.utils.translation import ugettext as _
from openedx.core.djangolib.markup import HTML
from openedx.core.djangoapps.util.user_messages import user_messages
%>
% if user_messages:
<div class="page-banner">
<div class="user-messages">
% for message in user_messages(request):
<div class="alert ${message.css_class}" role="alert">
<span class="icon icon-alert fa ${message.icon_class}" aria-hidden="true"></span>
${HTML(message.message_html)}
</div>
% endfor
</div>
</div>
% endif
## mako
## Override the default styles_version to use Bootstrap ## Override the default styles_version to use Bootstrap
<%! main_css = "css/bootstrap/lms-main.css" %> <%! main_css = "css/bootstrap/lms-main.css" %>
......
## Override the default styles_version to the Pattern Library version (version 2) ## mako
<%! main_css = "style-main-v2" %>
<%page expression_filter="h"/> <%page expression_filter="h"/>
<%inherit file="/main.html" /> <%inherit file="/main.html" />
...@@ -18,10 +17,14 @@ ...@@ -18,10 +17,14 @@
<h2>UX Style Reference</h2> <h2>UX Style Reference</h2>
<section class="xblock xblock-student_view xmodule_display xmodule_HtmlModule"> <section class="xblock xblock-student_view xmodule_display xmodule_HtmlModule">
<h3>Page Types</h3> <h3>v1-style LMS Pages</h3>
<ul> <ul>
<li><a href="pattern-library-test.html">Pattern Library test page</a></li> <li><a href="v1/course-skeleton.html">Course skeleton page</a></li>
<li><a href="course-skeleton.html">Course skeleton page</a></li> </ul>
<h3>Pattern Library</h3>
<ul>
<li><a href="pattern-library/course-skeleton.html">Course skeleton page</a></li>
</ul> </ul>
<h3>Bootstrap</h3> <h3>Bootstrap</h3>
......
## Override the default styles_version to the Pattern Library version (version 2)
<%! main_css = "style-main-v2" %>
<%page expression_filter="h"/>
<%inherit file="/main.html" />
<%block name="pagetitle">Pattern Library Test</%block>
<%block name="nav_skip">#content</%block>
<%block name="bodyclass">pattern-library</%block>
<%block name="content">
<h1>Pattern Library test page</h1>
<div class="alert alert-warning" role="alert" tabindex="-1">
<span class="icon alert-icon fa fa-exclamation-triangle" aria-hidden="true"></span>
<div class="alert-message">
<p class="alert-copy">
Interesting pattern library content to come...
</p>
</div>
</div>
</%block>
## mako
## Override the default styles_version to the Pattern Library version (version 2) ## Override the default styles_version to the Pattern Library version (version 2)
<%! main_css = "style-main-v2" %> <%! main_css = "style-main-v2" %>
......
...@@ -998,7 +998,10 @@ if settings.DEBUG: ...@@ -998,7 +998,10 @@ if settings.DEBUG:
settings.PROFILE_IMAGE_BACKEND['options']['base_url'], settings.PROFILE_IMAGE_BACKEND['options']['base_url'],
document_root=settings.PROFILE_IMAGE_BACKEND['options']['location'] document_root=settings.PROFILE_IMAGE_BACKEND['options']['location']
) )
# TODO: re-enable this after removing the URL below
# urlpatterns += url(r'^template/(?P<template>.+)$', 'openedx.core.djangoapps.debug.views.show_reference_template')
# TODO: DO NOT MERGE
urlpatterns += url(r'^template/(?P<template>.+)$', 'openedx.core.djangoapps.debug.views.show_reference_template'), urlpatterns += url(r'^template/(?P<template>.+)$', 'openedx.core.djangoapps.debug.views.show_reference_template'),
if 'debug_toolbar' in settings.INSTALLED_APPS: if 'debug_toolbar' in settings.INSTALLED_APPS:
......
...@@ -4,9 +4,15 @@ These views will NOT be shown on production: trying to access them will result ...@@ -4,9 +4,15 @@ These views will NOT be shown on production: trying to access them will result
in a 404 error. in a 404 error.
""" """
from django.http import HttpResponseNotFound from django.http import HttpResponseNotFound
from mako.exceptions import TopLevelLookupException from django.utils.translation import ugettext as _
from edxmako.shortcuts import render_to_response 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,
)
def show_reference_template(request, template): def show_reference_template(request, template):
...@@ -23,13 +29,22 @@ def show_reference_template(request, template): ...@@ -23,13 +29,22 @@ def show_reference_template(request, template):
""" """
try: try:
uses_bootstrap = u'/bootstrap/' in request.path uses_bootstrap = u'/bootstrap/' in request.path
uses_pattern_library = not uses_bootstrap uses_pattern_library = u'/pattern-library/' in request.path
is_v1 = u'/v1/' in request.path
context = { context = {
"disable_courseware_js": True, "disable_courseware_js": not is_v1,
"uses_pattern_library": uses_pattern_library, "uses_pattern_library": uses_pattern_library,
"uses_bootstrap": uses_bootstrap, "uses_bootstrap": uses_bootstrap,
} }
context.update(request.GET.dict()) context.update(request.GET.dict())
# 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'))
return render_to_response(template, context) return render_to_response(template, context)
except TopLevelLookupException: except TopLevelLookupException:
return HttpResponseNotFound("Couldn't find template {template}".format(template=template)) return HttpResponseNotFound("Couldn't find template {template}".format(template=template))
"""
Unit tests for user messages.
"""
import ddt
from unittest import TestCase
from django.contrib.messages.middleware import MessageMiddleware
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,
)
TEST_MESSAGE = 'Test message'
@ddt.ddt
class UserMessagesTestCase(TestCase):
"""
Unit tests for user messages.
"""
def setUp(self):
super(UserMessagesTestCase, self).setUp()
self.student = UserFactory.create()
self.request = RequestFactory().request()
self.request.session = {}
self.request.user = self.student
MessageMiddleware().process_request(self.request)
@ddt.data(
('Rock & Roll', 'Rock &amp; Roll'),
(Text('Rock & Roll'), 'Rock &amp; Roll'),
(HTML('<p>Hello, world!</p>'), '<p>Hello, world!</p>')
)
@ddt.unpack
def test_message_escaping(self, message, expected_message_html):
"""
Verifies that a user message is escaped correctly.
"""
register_user_message(self.request, UserMessageType.INFO, message)
messages = list(user_messages(self.request))
self.assertEqual(len(messages), 1)
self.assertEquals(messages[0].message_html, expected_message_html)
@ddt.data(
(UserMessageType.ERROR, 'alert-danger', 'fa fa-warning'),
(UserMessageType.INFO, 'alert-info', 'fa fa-bullhorn'),
(UserMessageType.SUCCESS, 'alert-success', 'fa fa-check-circle'),
(UserMessageType.WARNING, 'alert-warning', 'fa fa-warning'),
)
@ddt.unpack
def test_message_icon(self, message_type, expected_css_class, expected_icon_class):
"""
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))
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),
)
@ddt.unpack
def test_message_type(self, register_message_function, expected_message_type):
"""
Verifies that each user message function returns the correct type.
"""
register_message_function(self.request, TEST_MESSAGE)
messages = list(user_messages(self.request))
self.assertEqual(len(messages), 1)
self.assertEquals(messages[0].type, expected_message_type)
"""
Support for per-request messages to be shown to the user.
These utilities are based upon the Django message framework, and allow
code to register messages to be shown to the user on their next page
view. These messages are shown in a page banner which is supported on
all pages that utilize the main.html template.
There are two common use cases:
- register a message before rendering a view, in which case the message
will be shown on the resulting page
- register a message before posting or redirecting. In these situations
the message will be shown on the subsequent page. This is typically
used to show a success message to the use.
"""
from enum import Enum
from django.contrib import messages
from openedx.core.djangolib.markup import Text
EDX_USER_MESSAGE_TAG = 'edx-user-message'
class UserMessageType(Enum):
"""
An enumeration of the types of user messages.
"""
INFO = messages.constants.INFO
SUCCESS = messages.constants.SUCCESS
WARNING = messages.constants.WARNING
ERROR = messages.constants.ERROR
CSS_CLASSES = {
UserMessageType.INFO: 'alert-info',
UserMessageType.SUCCESS: 'alert-success',
UserMessageType.WARNING: 'alert-warning',
UserMessageType.ERROR: 'alert-danger',
}
ICON_CLASSES = {
UserMessageType.INFO: 'fa fa-bullhorn',
UserMessageType.SUCCESS: 'fa fa-check-circle',
UserMessageType.WARNING: 'fa fa-warning',
UserMessageType.ERROR: 'fa fa-warning',
}
class UserMessage():
"""
Representation of a message to be shown to a user
"""
def __init__(self, type, message_html):
assert isinstance(type, UserMessageType)
self.type = type
self.message_html = message_html
@property
def css_class(self):
"""
Returns the CSS class to be used on the message element.
"""
return CSS_CLASSES[self.type]
@property
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):
"""
Register a message to be shown to the user in the next page.
"""
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):
"""
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 EDX_USER_MESSAGE_TAG in message.tags)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment