Add support for page banner status messages
LEARNER-1890
This commit is contained in:
@@ -388,7 +388,7 @@ class SelfPacedCourseInfoTestCase(LoginEnrollmentTestCase, SharedModuleStoreTest
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
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):
|
||||
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 @@
|
||||
@import 'edx-pattern-library-shims/buttons';
|
||||
|
||||
// base - elements
|
||||
@import 'elements/banners';
|
||||
@import 'elements/controls';
|
||||
@import 'elements/creative-commons';
|
||||
@import 'elements/icons';
|
||||
|
||||
19
lms/static/sass/bootstrap/_components.scss
Normal file
19
lms/static/sass/bootstrap/_components.scss
Normal file
@@ -0,0 +1,19 @@
|
||||
// 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 @@
|
||||
@import 'footer';
|
||||
@import 'navigation';
|
||||
@import 'layouts';
|
||||
@import 'components';
|
||||
|
||||
@@ -44,3 +44,50 @@ $full-width-banner-margin: 20px;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,6 +220,26 @@ $alert-color: rgb(212, 64, 64) !default;
|
||||
$success-color: rgb(0, 155, 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
|
||||
// ----------------------------
|
||||
|
||||
@@ -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 {
|
||||
@include clearfix();
|
||||
@include box-sizing(border-box);
|
||||
|
||||
@@ -62,3 +62,23 @@ $lms-dark-icon-background-color: palette(grayscale, black) !default;
|
||||
$site-status-color: rgb(182,37,103) !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
|
||||
<%include file="/preview_menu.html" />
|
||||
% endif
|
||||
|
||||
<%include file="/page_banner.html" />
|
||||
|
||||
<div class="content-wrapper ${"container-fluid" if uses_bootstrap else "" } main-container" id="content">
|
||||
${self.body()}
|
||||
<%block name="bodyextra"/>
|
||||
|
||||
24
lms/templates/page_banner.html
Normal file
24
lms/templates/page_banner.html
Normal file
@@ -0,0 +1,24 @@
|
||||
## 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
|
||||
@@ -1,3 +1,5 @@
|
||||
## mako
|
||||
|
||||
## Override the default styles_version to use Bootstrap
|
||||
<%! main_css = "css/bootstrap/lms-main.css" %>
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
## Override the default styles_version to the Pattern Library version (version 2)
|
||||
<%! main_css = "style-main-v2" %>
|
||||
## mako
|
||||
|
||||
<%page expression_filter="h"/>
|
||||
<%inherit file="/main.html" />
|
||||
@@ -18,10 +17,14 @@
|
||||
<h2>UX Style Reference</h2>
|
||||
|
||||
<section class="xblock xblock-student_view xmodule_display xmodule_HtmlModule">
|
||||
<h3>Page Types</h3>
|
||||
<h3>v1-style LMS Pages</h3>
|
||||
<ul>
|
||||
<li><a href="pattern-library-test.html">Pattern Library test page</a></li>
|
||||
<li><a href="course-skeleton.html">Course skeleton page</a></li>
|
||||
<li><a href="v1/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>
|
||||
|
||||
<h3>Bootstrap</h3>
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
## 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>
|
||||
@@ -1,3 +1,5 @@
|
||||
## mako
|
||||
|
||||
## Override the default styles_version to the Pattern Library version (version 2)
|
||||
<%! main_css = "style-main-v2" %>
|
||||
|
||||
945
lms/templates/ux/reference/v1/course-skeleton.html
Normal file
945
lms/templates/ux/reference/v1/course-skeleton.html
Normal file
File diff suppressed because one or more lines are too long
@@ -998,7 +998,10 @@ if settings.DEBUG:
|
||||
settings.PROFILE_IMAGE_BACKEND['options']['base_url'],
|
||||
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'),
|
||||
|
||||
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
|
||||
in a 404 error.
|
||||
"""
|
||||
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 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):
|
||||
@@ -23,13 +29,22 @@ def show_reference_template(request, template):
|
||||
"""
|
||||
try:
|
||||
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 = {
|
||||
"disable_courseware_js": True,
|
||||
"disable_courseware_js": not is_v1,
|
||||
"uses_pattern_library": uses_pattern_library,
|
||||
"uses_bootstrap": uses_bootstrap,
|
||||
}
|
||||
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)
|
||||
except TopLevelLookupException:
|
||||
return HttpResponseNotFound("Couldn't find template {template}".format(template=template))
|
||||
|
||||
85
openedx/core/djangoapps/util/tests/test_user_messages.py
Normal file
85
openedx/core/djangoapps/util/tests/test_user_messages.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""
|
||||
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 & Roll'),
|
||||
(Text('Rock & Roll'), 'Rock & 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)
|
||||
137
openedx/core/djangoapps/util/user_messages.py
Normal file
137
openedx/core/djangoapps/util/user_messages.py
Normal file
@@ -0,0 +1,137 @@
|
||||
"""
|
||||
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)
|
||||
Reference in New Issue
Block a user