Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
b523ac3f
Commit
b523ac3f
authored
Jul 24, 2017
by
Harry Rein
Committed by
GitHub
Jul 24, 2017
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #15589 from edx/HarryRein/LEARNER-1894-in-course-messaging-xsy
Adding in-course messages on the home page.
parents
eb987ed6
08df53e1
Hide whitespace changes
Inline
Side-by-side
Showing
16 changed files
with
460 additions
and
107 deletions
+460
-107
common/djangoapps/status/models.py
+4
-4
lms/djangoapps/courseware/views/views.py
+3
-3
lms/static/sass/features/_course-experience.scss
+66
-0
lms/static/sass/shared-v2/_variables.scss
+4
-0
lms/templates/page_banner.html
+2
-2
openedx/core/djangoapps/debug/views.py
+5
-10
openedx/core/djangoapps/util/tests/test_user_messages.py
+11
-19
openedx/core/djangoapps/util/user_messages.py
+104
-51
openedx/features/course_experience/__init__.py
+16
-1
openedx/features/course_experience/static/course_experience/images/home_message_author.png
+0
-0
openedx/features/course_experience/templates/course_experience/course-home-fragment.html
+4
-0
openedx/features/course_experience/templates/course_experience/course-messages-fragment.html
+30
-0
openedx/features/course_experience/tests/views/test_course_home.py
+72
-14
openedx/features/course_experience/views/course_home.py
+13
-3
openedx/features/course_experience/views/course_home_messages.py
+126
-0
themes/edx.org/openedx/features/course_experience/static/course_experience/images/home_message_author.png
+0
-0
No files found.
common/djangoapps/status/models.py
View file @
b523ac3f
...
...
@@ -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
...
...
lms/djangoapps/courseware/views/views.py
View file @
b523ac3f
...
...
@@ -81,7 +81,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
...
...
@@ -447,7 +447,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
(
...
...
@@ -461,7 +461,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
(
...
...
lms/static/sass/features/_course-experience.scss
View file @
b523ac3f
// ------------------------------
// 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
;
...
...
lms/static/sass/shared-v2/_variables.scss
View file @
b523ac3f
...
...
@@ -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
// ----------------------------
...
...
lms/templates/page_banner.html
View file @
b523ac3f
...
...
@@ -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_m
essages
from
openedx
.
core
.
djangoapps
.
util
.
user_messages
import
PageLevelM
essages
%
>
<
%
banner_messages =
list(user_messages(request))
banner_messages =
list(
PageLevelMessages.
user_messages(request))
%
>
% if banner_messages:
...
...
openedx/core/djangoapps/debug/views.py
View file @
b523ac3f
...
...
@@ -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
):
...
...
@@ -51,10 +46,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
:
...
...
openedx/core/djangoapps/util/tests/test_user_messages.py
View file @
b523ac3f
...
...
@@ -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
)
openedx/core/djangoapps/util/user_messages.py
View file @
b523ac3f
...
...
@@ -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
)
@classmethod
@abstractmethod
def
get_namespace
(
self
):
"""
Returns the namespace of the message collection.
The name is used to namespace the subset of django messages.
For example, return 'course_home_messages'.
"""
raise
NotImplementedError
(
'Subclasses must define a namespace for messages.'
)
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
)
@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.
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
)
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
)
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
)
@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
)
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
)
@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
)
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.
class
PageLevelMessages
(
UserMessageCollection
):
"""
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
)
This set of messages appears as top page level messages.
"""
NAMESPACE
=
'page_level_messages'
def
_create_user_message
(
message
):
@classmethod
def
get_namespace
(
self
):
"""
Creates a user message from a Django message
.
Returns the namespace of the message collection
.
"""
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
)
return
self
.
NAMESPACE
openedx/features/course_experience/__init__.py
View file @
b523ac3f
...
...
@@ -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
openedx/features/course_experience/static/course_experience/images/home_message_author.png
0 → 100644
View file @
b523ac3f
5.48 KB
openedx/features/course_experience/templates/course_experience/course-home-fragment.html
View file @
b523ac3f
...
...
@@ -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())}
...
...
openedx/features/course_experience/templates/course_experience/course-messages-fragment.html
0 → 100644
View file @
b523ac3f
## 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
openedx/features/course_experience/tests/views/test_course_home.py
View file @
b523ac3f
...
...
@@ -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
%2
C+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
)
openedx/features/course_experience/views/course_home.py
View file @
b523ac3f
...
...
@@ -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
,
...
...
openedx/features/course_experience/views/course_home_messages.py
0 → 100644
View file @
b523ac3f
"""
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'
]
)
)
themes/edx.org/openedx/features/course_experience/static/course_experience/images/home_message_author.png
0 → 100644
View file @
b523ac3f
1020 Bytes
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment