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.
This commit is contained in:
@@ -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"{} <br /> {}".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"{} <br /> {}".format(msg, course_home_message.message)
|
||||
except CourseMessage.DoesNotExist:
|
||||
# We don't have a course-specific message, so pass.
|
||||
pass
|
||||
|
||||
@@ -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('<a href="/login?next={current_url}">{sign_in_label}</a>').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('<a href="{url_to_enroll}">{enroll_link_label}</a>').format(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
// ----------------------------
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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('<div class="message-header">'),
|
||||
title=title,
|
||||
body=body_html,
|
||||
header_close=HTML('</div>')
|
||||
)
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
@@ -57,6 +57,10 @@ from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG, SHOW_REV
|
||||
<div class="page-content">
|
||||
<div class="layout layout-1t2t">
|
||||
<main class="layout-col layout-col-b">
|
||||
% 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):
|
||||
<div class="section section-dates">
|
||||
${HTML(welcome_message_fragment.body_html())}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
## mako
|
||||
|
||||
<%page expression_filter="h"/>
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
|
||||
<%!
|
||||
from django.utils.translation import get_language_bidi
|
||||
from openedx.core.djangolib.markup import HTML
|
||||
from openedx.features.course_experience import CourseHomeMessages
|
||||
%>
|
||||
|
||||
<%
|
||||
is_rtl = get_language_bidi()
|
||||
%>
|
||||
|
||||
% if course_home_messages:
|
||||
% for message in course_home_messages:
|
||||
<div class="course-message grid-manual">
|
||||
% if not is_rtl:
|
||||
<img class="message-author col col-2" src="${static.url(image_src)}"/>
|
||||
% endif
|
||||
<div class="message-content col col-9">
|
||||
${HTML(message.message_html)}
|
||||
</div>
|
||||
% if is_rtl:
|
||||
<img class="message-author col col-2" src="${static.url(image_src)}"/>
|
||||
% endif
|
||||
</div>
|
||||
% endfor
|
||||
% endif
|
||||
@@ -2,10 +2,10 @@
|
||||
"""
|
||||
Tests for the course home page.
|
||||
"""
|
||||
import datetime
|
||||
from datetime import datetime, timedelta
|
||||
import ddt
|
||||
import mock
|
||||
import pytz
|
||||
from pytz import UTC
|
||||
from waffle.testutils import override_flag
|
||||
|
||||
from courseware.tests.factories import StaffFactory
|
||||
@@ -31,6 +31,10 @@ TEST_CHAPTER_NAME = 'Test Chapter'
|
||||
TEST_WELCOME_MESSAGE = '<h2>Welcome!</h2>'
|
||||
TEST_UPDATE_MESSAGE = '<h2>Test Update!</h2>'
|
||||
TEST_COURSE_UPDATES_TOOL = '/course/updates">'
|
||||
TEST_COURSE_HOME_MESSAGE = 'course-message'
|
||||
TEST_COURSE_HOME_MESSAGE_ANONYMOUS = '/login'
|
||||
TEST_COURSE_HOME_MESSAGE_UNENROLLED = 'Enroll now'
|
||||
TEST_COURSE_HOME_MESSAGE_PRE_START = 'Course starts in'
|
||||
|
||||
QUERY_COUNT_TABLE_BLACKLIST = WAFFLE_TABLES
|
||||
|
||||
@@ -73,7 +77,12 @@ class CourseHomePageTestCase(SharedModuleStoreTestCase):
|
||||
# pylint: disable=super-method-not-called
|
||||
with super(CourseHomePageTestCase, cls).setUpClassAndTestData():
|
||||
with cls.store.default_store(ModuleStoreEnum.Type.split):
|
||||
cls.course = CourseFactory.create(org='edX', number='test', display_name='Test Course')
|
||||
cls.course = CourseFactory.create(
|
||||
org='edX',
|
||||
number='test',
|
||||
display_name='Test Course',
|
||||
start=datetime.now(UTC) - timedelta(days=30),
|
||||
)
|
||||
with cls.store.bulk_operations(cls.course.id):
|
||||
chapter = ItemFactory.create(
|
||||
category='chapter',
|
||||
@@ -92,6 +101,15 @@ class CourseHomePageTestCase(SharedModuleStoreTestCase):
|
||||
cls.user = UserFactory(password=TEST_PASSWORD)
|
||||
CourseEnrollment.enroll(cls.user, cls.course.id)
|
||||
|
||||
def create_future_course(self, specific_date=None):
|
||||
"""
|
||||
Creates and returns a course in the future.
|
||||
"""
|
||||
return CourseFactory.create(
|
||||
display_name='Test Future Course',
|
||||
start=specific_date if specific_date else datetime.now(UTC) + timedelta(days=30),
|
||||
)
|
||||
|
||||
|
||||
class TestCourseHomePage(CourseHomePageTestCase):
|
||||
def setUp(self):
|
||||
@@ -152,18 +170,15 @@ class TestCourseHomePage(CourseHomePageTestCase):
|
||||
"""
|
||||
Verify that the course home page handles start dates correctly.
|
||||
"""
|
||||
now = datetime.datetime.now(pytz.UTC)
|
||||
tomorrow = now + datetime.timedelta(days=1)
|
||||
self.course.start = tomorrow
|
||||
|
||||
# The course home page should 404 for a course starting in the future
|
||||
url = course_home_url(self.course)
|
||||
future_course = self.create_future_course(datetime(2030, 1, 1, tzinfo=UTC))
|
||||
url = course_home_url(future_course)
|
||||
response = self.client.get(url)
|
||||
self.assertRedirects(response, '/dashboard?notlive=Jan+01%2C+2030')
|
||||
|
||||
# With the Waffle flag enabled, the course should be visible
|
||||
with override_flag(COURSE_PRE_START_ACCESS_FLAG.namespaced_flag_name, True):
|
||||
url = course_home_url(self.course)
|
||||
url = course_home_url(future_course)
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@@ -272,11 +287,12 @@ class TestCourseHomePageAccess(CourseHomePageTestCase):
|
||||
Ensure that a user accessing a non-live course sees a redirect to
|
||||
the student dashboard, not a 404.
|
||||
"""
|
||||
self.user = self.create_user_for_course(self.course, CourseUserType.ENROLLED)
|
||||
future_course = self.create_future_course()
|
||||
self.user = self.create_user_for_course(future_course, CourseUserType.ENROLLED)
|
||||
|
||||
url = course_home_url(self.course)
|
||||
url = course_home_url(future_course)
|
||||
response = self.client.get(url)
|
||||
start_date = strftime_localized(self.course.start, 'SHORT_DATE')
|
||||
start_date = strftime_localized(future_course.start, 'SHORT_DATE')
|
||||
expected_params = QueryDict(mutable=True)
|
||||
expected_params['notlive'] = start_date
|
||||
expected_url = '{url}?{params}'.format(
|
||||
@@ -292,12 +308,13 @@ class TestCourseHomePageAccess(CourseHomePageTestCase):
|
||||
Ensure that a user accessing a non-live course sees a redirect to
|
||||
the student dashboard, not a 404, even if the localized date is unicode
|
||||
"""
|
||||
self.user = self.create_user_for_course(self.course, CourseUserType.ENROLLED)
|
||||
future_course = self.create_future_course()
|
||||
self.user = self.create_user_for_course(future_course, CourseUserType.ENROLLED)
|
||||
|
||||
fake_unicode_start_time = u"üñîçø∂é_ßtå®t_tîµé"
|
||||
mock_strftime_localized.return_value = fake_unicode_start_time
|
||||
|
||||
url = course_home_url(self.course)
|
||||
url = course_home_url(future_course)
|
||||
response = self.client.get(url)
|
||||
expected_params = QueryDict(mutable=True)
|
||||
expected_params['notlive'] = fake_unicode_start_time
|
||||
@@ -316,3 +333,44 @@ class TestCourseHomePageAccess(CourseHomePageTestCase):
|
||||
url = course_home_url_from_string('not/a/course')
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
@override_waffle_flag(UNIFIED_COURSE_TAB_FLAG, active=True)
|
||||
@override_waffle_flag(COURSE_PRE_START_ACCESS_FLAG, active=True)
|
||||
def test_course_messaging(self):
|
||||
"""
|
||||
Ensure that the following four use cases work as expected
|
||||
|
||||
1) Anonymous users are shown a course message linking them to the login page
|
||||
2) Unenrolled users are shown a course message allowing them to enroll
|
||||
3) Enrolled users who show up on the course page after the course has begun
|
||||
are not shown a course message.
|
||||
4) Enrolled users who show up on the course page before the course begins
|
||||
are shown a message explaining when the course starts as well as a call to
|
||||
action button that allows them to add a calendar event.
|
||||
"""
|
||||
# Verify that anonymous users are shown a login link in the course message
|
||||
url = course_home_url(self.course)
|
||||
response = self.client.get(url)
|
||||
self.assertContains(response, TEST_COURSE_HOME_MESSAGE)
|
||||
self.assertContains(response, TEST_COURSE_HOME_MESSAGE_ANONYMOUS)
|
||||
|
||||
# Verify that unenrolled users are shown an enroll call to action message
|
||||
self.user = self.create_user_for_course(self.course, CourseUserType.UNENROLLED)
|
||||
url = course_home_url(self.course)
|
||||
response = self.client.get(url)
|
||||
self.assertContains(response, TEST_COURSE_HOME_MESSAGE)
|
||||
self.assertContains(response, TEST_COURSE_HOME_MESSAGE_UNENROLLED)
|
||||
|
||||
# Verify that enrolled users are not shown a message when enrolled and course has begun
|
||||
CourseEnrollment.enroll(self.user, self.course.id)
|
||||
url = course_home_url(self.course)
|
||||
response = self.client.get(url)
|
||||
self.assertNotContains(response, TEST_COURSE_HOME_MESSAGE)
|
||||
|
||||
# Verify that enrolled users are shown 'days until start' message before start date
|
||||
future_course = self.create_future_course()
|
||||
CourseEnrollment.enroll(self.user, future_course.id)
|
||||
url = course_home_url(future_course)
|
||||
response = self.client.get(url)
|
||||
self.assertContains(response, TEST_COURSE_HOME_MESSAGE)
|
||||
self.assertContains(response, TEST_COURSE_HOME_MESSAGE_PRE_START)
|
||||
|
||||
@@ -26,6 +26,7 @@ from web_fragments.fragment import Fragment
|
||||
|
||||
from ..utils import get_course_outline_block_tree
|
||||
from .course_dates import CourseDatesFragmentView
|
||||
from .course_home_messages import CourseHomeMessageFragmentView
|
||||
from .course_outline import CourseOutlineFragmentView
|
||||
from .course_sock import CourseSockFragmentView
|
||||
from .welcome_message import WelcomeMessageFragmentView
|
||||
@@ -113,9 +114,12 @@ class CourseHomeFragmentView(EdxFragmentView):
|
||||
|
||||
# Render the full content to enrolled users, as well as to course and global staff.
|
||||
# Unenrolled users who are not course or global staff are given only a subset.
|
||||
is_enrolled = CourseEnrollment.is_enrolled(request.user, course_key)
|
||||
is_staff = has_access(request.user, 'staff', course_key)
|
||||
if is_enrolled or is_staff:
|
||||
user_access = {
|
||||
'is_anonymous': request.user.is_anonymous(),
|
||||
'is_enrolled': CourseEnrollment.is_enrolled(request.user, course_key),
|
||||
'is_staff': has_access(request.user, 'staff', course_key),
|
||||
}
|
||||
if user_access['is_enrolled'] or user_access['is_staff']:
|
||||
outline_fragment = CourseOutlineFragmentView().render_to_fragment(request, course_id=course_id, **kwargs)
|
||||
welcome_message_fragment = WelcomeMessageFragmentView().render_to_fragment(
|
||||
request, course_id=course_id, **kwargs
|
||||
@@ -141,6 +145,11 @@ class CourseHomeFragmentView(EdxFragmentView):
|
||||
# Get the course tools enabled for this user and course
|
||||
course_tools = CourseToolsPluginManager.get_enabled_course_tools(request, course_key)
|
||||
|
||||
# Grab the course home messages fragment to render any relevant django messages
|
||||
course_home_message_fragment = CourseHomeMessageFragmentView().render_to_fragment(
|
||||
request, course_id=course_id, user_access=user_access, **kwargs
|
||||
)
|
||||
|
||||
# Render the course home fragment
|
||||
context = {
|
||||
'request': request,
|
||||
@@ -149,6 +158,7 @@ class CourseHomeFragmentView(EdxFragmentView):
|
||||
'course_key': course_key,
|
||||
'outline_fragment': outline_fragment,
|
||||
'handouts_html': handouts_html,
|
||||
'course_home_message_fragment': course_home_message_fragment,
|
||||
'has_visited_course': has_visited_course,
|
||||
'resume_course_url': resume_course_url,
|
||||
'course_tools': course_tools,
|
||||
|
||||
126
openedx/features/course_experience/views/course_home_messages.py
Normal file
126
openedx/features/course_experience/views/course_home_messages.py
Normal file
@@ -0,0 +1,126 @@
|
||||
"""
|
||||
View logic for handling course messages.
|
||||
"""
|
||||
|
||||
from babel.dates import format_date, format_timedelta
|
||||
from datetime import datetime
|
||||
|
||||
from courseware.courses import get_course_with_access
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.http import urlquote_plus
|
||||
from django.utils.timezone import UTC
|
||||
from django.utils.translation import get_language, to_locale
|
||||
from django.utils.translation import ugettext as _
|
||||
from openedx.core.djangolib.markup import Text, HTML
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from web_fragments.fragment import Fragment
|
||||
|
||||
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
|
||||
from openedx.features.course_experience import CourseHomeMessages
|
||||
|
||||
|
||||
class CourseHomeMessageFragmentView(EdxFragmentView):
|
||||
"""
|
||||
A fragment that displays a course message with an alert and call
|
||||
to action for three types of users:
|
||||
|
||||
1) Not logged in users are given a link to sign in or register.
|
||||
2) Unenrolled users are given a link to enroll.
|
||||
3) Enrolled users who get to the page before the course start date
|
||||
are given the option to add the start date to their calendar.
|
||||
|
||||
This fragment requires a user_access map as follows:
|
||||
|
||||
user_access = {
|
||||
'is_anonymous': True if the user is logged in, False otherwise
|
||||
'is_enrolled': True if the user is enrolled in the course, False otherwise
|
||||
'is_staff': True if the user is a staff member of the course, False otherwise
|
||||
}
|
||||
"""
|
||||
def render_to_fragment(self, request, course_id, user_access, **kwargs):
|
||||
"""
|
||||
Renders a course message fragment for the specified course.
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
course = get_course_with_access(request.user, 'load', course_key)
|
||||
|
||||
# Get time until the start date, if already started, or no start date, value will be zero or negative
|
||||
now = datetime.now(UTC())
|
||||
already_started = course.start and now > course.start
|
||||
days_until_start_string = "started" if already_started else format_timedelta(course.start - now, locale=to_locale(get_language()))
|
||||
course_start_data = {
|
||||
'course_start_date': format_date(course.start, locale=to_locale(get_language())),
|
||||
'already_started': already_started,
|
||||
'days_until_start_string': days_until_start_string
|
||||
}
|
||||
|
||||
# Register the course home messages to be loaded on the page
|
||||
self.register_course_home_messages(request, course, user_access, course_start_data)
|
||||
|
||||
# Grab the relevant messages
|
||||
course_home_messages = list(CourseHomeMessages.user_messages(request))
|
||||
|
||||
# Return None if user is enrolled and course has begun
|
||||
if user_access['is_enrolled'] and already_started:
|
||||
return None
|
||||
|
||||
# Grab the logo
|
||||
image_src = "course_experience/images/home_message_author.png"
|
||||
|
||||
context = {
|
||||
'course_home_messages': course_home_messages,
|
||||
'image_src': image_src,
|
||||
}
|
||||
|
||||
html = render_to_string('course_experience/course-messages-fragment.html', context)
|
||||
return Fragment(html)
|
||||
|
||||
@staticmethod
|
||||
def register_course_home_messages(request, course, user_access, course_start_data):
|
||||
"""
|
||||
Register messages to be shown in the course home content page.
|
||||
"""
|
||||
if user_access['is_anonymous']:
|
||||
CourseHomeMessages.register_info_message(
|
||||
request,
|
||||
Text(_(
|
||||
" {sign_in_link} or {register_link} and then enroll in this course."
|
||||
)).format(
|
||||
sign_in_link=HTML("<a href='/login?next={current_url}'>{sign_in_label}</a>").format(
|
||||
sign_in_label=_("Sign in"),
|
||||
current_url=urlquote_plus(request.path),
|
||||
),
|
||||
register_link=HTML("<a href='/register?next={current_url}'>{register_label}</a>").format(
|
||||
register_label=_("register"),
|
||||
current_url=urlquote_plus(request.path),
|
||||
)
|
||||
),
|
||||
title='You must be enrolled in the course to see course content.'
|
||||
)
|
||||
if not user_access['is_anonymous'] and not user_access['is_staff'] and not user_access['is_enrolled']:
|
||||
CourseHomeMessages.register_info_message(
|
||||
request,
|
||||
Text(_(
|
||||
"{open_enroll_link} Enroll now{close_enroll_link} to access the full course."
|
||||
)).format(
|
||||
open_enroll_link='',
|
||||
close_enroll_link=''
|
||||
),
|
||||
title=Text('Welcome to {course_display_name}').format(
|
||||
course_display_name=course.display_name
|
||||
)
|
||||
)
|
||||
if user_access['is_enrolled'] and not course_start_data['already_started']:
|
||||
CourseHomeMessages.register_info_message(
|
||||
request,
|
||||
Text(_(
|
||||
"{add_reminder_open_tag}Don't forget to add a calendar reminder!{add_reminder_close_tag}."
|
||||
)).format(
|
||||
add_reminder_open_tag='',
|
||||
add_reminder_close_tag=''
|
||||
),
|
||||
title=Text("Course starts in {days_until_start_string} on {course_start_date}.").format(
|
||||
days_until_start_string=course_start_data['days_until_start_string'],
|
||||
course_start_date=course_start_data['course_start_date']
|
||||
)
|
||||
)
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1020 B |
Reference in New Issue
Block a user