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):
...
@@ -26,10 +26,10 @@ class GlobalStatusMessage(ConfigurationModel):
msg
=
self
.
message
msg
=
self
.
message
if
course_key
:
if
course_key
:
try
:
try
:
course_message
=
self
.
coursemessage_set
.
get
(
course_key
=
course_key
)
course_
home_
message
=
self
.
coursemessage_set
.
get
(
course_key
=
course_key
)
# Don't add the message if course_message is blank.
# Don't add the message if course_
home_
message is blank.
if
course_message
:
if
course_
home_
message
:
msg
=
u"{} <br /> {}"
.
format
(
msg
,
course_message
.
message
)
msg
=
u"{} <br /> {}"
.
format
(
msg
,
course_
home_
message
.
message
)
except
CourseMessage
.
DoesNotExist
:
except
CourseMessage
.
DoesNotExist
:
# We don't have a course-specific message, so pass.
# We don't have a course-specific message, so pass.
pass
pass
...
...
lms/djangoapps/courseware/views/views.py
View file @
b523ac3f
...
@@ -81,7 +81,7 @@ from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
...
@@ -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.programs.utils
import
ProgramMarketingDataExtender
from
openedx.core.djangoapps.self_paced.models
import
SelfPacedConfiguration
from
openedx.core.djangoapps.self_paced.models
import
SelfPacedConfiguration
from
openedx.core.djangoapps.site_configuration
import
helpers
as
configuration_helpers
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.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
import
UNIFIED_COURSE_TAB_FLAG
,
course_home_url_name
from
openedx.features.course_experience.course_tools
import
CourseToolsPluginManager
from
openedx.features.course_experience.course_tools
import
CourseToolsPluginManager
...
@@ -447,7 +447,7 @@ class CourseTabView(EdxFragmentView):
...
@@ -447,7 +447,7 @@ class CourseTabView(EdxFragmentView):
is_enrolled
=
CourseEnrollment
.
is_enrolled
(
request
.
user
,
course_key
)
is_enrolled
=
CourseEnrollment
.
is_enrolled
(
request
.
user
,
course_key
)
is_staff
=
has_access
(
request
.
user
,
'staff'
,
course_key
)
is_staff
=
has_access
(
request
.
user
,
'staff'
,
course_key
)
if
request
.
user
.
is_anonymous
():
if
request
.
user
.
is_anonymous
():
register_warning_message
(
PageLevelMessages
.
register_warning_message
(
request
,
request
,
Text
(
_
(
"To see course content, {sign_in_link} or {register_link}."
))
.
format
(
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
(
sign_in_link
=
HTML
(
'<a href="/login?next={current_url}">{sign_in_label}</a>'
)
.
format
(
...
@@ -461,7 +461,7 @@ class CourseTabView(EdxFragmentView):
...
@@ -461,7 +461,7 @@ class CourseTabView(EdxFragmentView):
)
)
)
)
elif
not
is_enrolled
and
not
is_staff
:
elif
not
is_enrolled
and
not
is_staff
:
register_warning_message
(
PageLevelMessages
.
register_warning_message
(
request
,
request
,
Text
(
_
(
'You must be enrolled in the course to see course content. {enroll_link}.'
))
.
format
(
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
(
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
.welcome-message
{
.welcome-message
{
border
:
solid
1px
$lms-border-color
;
border
:
solid
1px
$lms-border-color
;
...
...
lms/static/sass/shared-v2/_variables.scss
View file @
b523ac3f
...
@@ -11,6 +11,10 @@
...
@@ -11,6 +11,10 @@
// ----------------------------
// ----------------------------
$lms-max-width
:
1180px
!
default
;
$lms-max-width
:
1180px
!
default
;
$grid-breakpoints-sm
:
576px
!
default
;
$grid-breakpoints-md
:
768px
!
default
;
$grid-breakpoints-lg
:
992px
!
default
;
// ----------------------------
// ----------------------------
// #COLORS
// #COLORS
// ----------------------------
// ----------------------------
...
...
lms/templates/page_banner.html
View file @
b523ac3f
...
@@ -7,11 +7,11 @@
...
@@ -7,11 +7,11 @@
<
%!
<
%!
from
django
.
utils
.
translation
import
ugettext
as
_
from
django
.
utils
.
translation
import
ugettext
as
_
from
openedx
.
core
.
djangolib
.
markup
import
HTML
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:
% if banner_messages:
...
...
openedx/core/djangoapps/debug/views.py
View file @
b523ac3f
...
@@ -7,12 +7,7 @@ from django.http import HttpResponseNotFound
...
@@ -7,12 +7,7 @@ from django.http import HttpResponseNotFound
from
django.utils.translation
import
ugettext
as
_
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
mako.exceptions
import
TopLevelLookupException
from
openedx.core.djangoapps.util.user_messages
import
(
from
openedx.core.djangoapps.util.user_messages
import
PageLevelMessages
register_error_message
,
register_info_message
,
register_success_message
,
register_warning_message
,
)
def
show_reference_template
(
request
,
template
):
def
show_reference_template
(
request
,
template
):
...
@@ -51,10 +46,10 @@ def show_reference_template(request, template):
...
@@ -51,10 +46,10 @@ def show_reference_template(request, template):
# Add some messages to the course skeleton pages
# Add some messages to the course skeleton pages
if
u'course-skeleton.html'
in
request
.
path
:
if
u'course-skeleton.html'
in
request
.
path
:
register_info_message
(
request
,
_
(
'This is a test message'
))
PageLevelMessages
.
register_info_message
(
request
,
_
(
'This is a test message'
))
register_success_message
(
request
,
_
(
'This is a success message'
))
PageLevelMessages
.
register_success_message
(
request
,
_
(
'This is a success message'
))
register_warning_message
(
request
,
_
(
'This is a test warning'
))
PageLevelMessages
.
register_warning_message
(
request
,
_
(
'This is a test warning'
))
register_error_message
(
request
,
_
(
'This is a test error'
))
PageLevelMessages
.
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
:
...
...
openedx/core/djangoapps/util/tests/test_user_messages.py
View file @
b523ac3f
...
@@ -10,15 +10,7 @@ from django.test import RequestFactory
...
@@ -10,15 +10,7 @@ from django.test import RequestFactory
from
openedx.core.djangolib.markup
import
HTML
,
Text
from
openedx.core.djangolib.markup
import
HTML
,
Text
from
student.tests.factories
import
UserFactory
from
student.tests.factories
import
UserFactory
from
..user_messages
import
(
from
..user_messages
import
PageLevelMessages
,
UserMessageType
register_error_message
,
register_info_message
,
register_success_message
,
register_user_message
,
register_warning_message
,
user_messages
,
UserMessageType
,
)
TEST_MESSAGE
=
'Test message'
TEST_MESSAGE
=
'Test message'
...
@@ -26,7 +18,7 @@ TEST_MESSAGE = 'Test message'
...
@@ -26,7 +18,7 @@ TEST_MESSAGE = 'Test message'
@ddt.ddt
@ddt.ddt
class
UserMessagesTestCase
(
TestCase
):
class
UserMessagesTestCase
(
TestCase
):
"""
"""
Unit tests for user messages.
Unit tests for
page level
user messages.
"""
"""
def
setUp
(
self
):
def
setUp
(
self
):
super
(
UserMessagesTestCase
,
self
)
.
setUp
()
super
(
UserMessagesTestCase
,
self
)
.
setUp
()
...
@@ -46,8 +38,8 @@ class UserMessagesTestCase(TestCase):
...
@@ -46,8 +38,8 @@ class UserMessagesTestCase(TestCase):
"""
"""
Verifies that a user message is escaped correctly.
Verifies that a user message is escaped correctly.
"""
"""
register_user_message
(
self
.
request
,
UserMessageType
.
INFO
,
message
)
PageLevelMessages
.
register_user_message
(
self
.
request
,
UserMessageType
.
INFO
,
message
)
messages
=
list
(
user_messages
(
self
.
request
))
messages
=
list
(
PageLevelMessages
.
user_messages
(
self
.
request
))
self
.
assertEqual
(
len
(
messages
),
1
)
self
.
assertEqual
(
len
(
messages
),
1
)
self
.
assertEquals
(
messages
[
0
]
.
message_html
,
expected_message_html
)
self
.
assertEquals
(
messages
[
0
]
.
message_html
,
expected_message_html
)
...
@@ -62,17 +54,17 @@ class UserMessagesTestCase(TestCase):
...
@@ -62,17 +54,17 @@ class UserMessagesTestCase(TestCase):
"""
"""
Verifies that a user message returns the correct CSS and icon classes.
Verifies that a user message returns the correct CSS and icon classes.
"""
"""
register_user_message
(
self
.
request
,
message_type
,
TEST_MESSAGE
)
PageLevelMessages
.
register_user_message
(
self
.
request
,
message_type
,
TEST_MESSAGE
)
messages
=
list
(
user_messages
(
self
.
request
))
messages
=
list
(
PageLevelMessages
.
user_messages
(
self
.
request
))
self
.
assertEqual
(
len
(
messages
),
1
)
self
.
assertEqual
(
len
(
messages
),
1
)
self
.
assertEquals
(
messages
[
0
]
.
css_class
,
expected_css_class
)
self
.
assertEquals
(
messages
[
0
]
.
css_class
,
expected_css_class
)
self
.
assertEquals
(
messages
[
0
]
.
icon_class
,
expected_icon_class
)
self
.
assertEquals
(
messages
[
0
]
.
icon_class
,
expected_icon_class
)
@ddt.data
(
@ddt.data
(
(
register_error_message
,
UserMessageType
.
ERROR
),
(
PageLevelMessages
.
register_error_message
,
UserMessageType
.
ERROR
),
(
register_info_message
,
UserMessageType
.
INFO
),
(
PageLevelMessages
.
register_info_message
,
UserMessageType
.
INFO
),
(
register_success_message
,
UserMessageType
.
SUCCESS
),
(
PageLevelMessages
.
register_success_message
,
UserMessageType
.
SUCCESS
),
(
register_warning_message
,
UserMessageType
.
WARNING
),
(
PageLevelMessages
.
register_warning_message
,
UserMessageType
.
WARNING
),
)
)
@ddt.unpack
@ddt.unpack
def
test_message_type
(
self
,
register_message_function
,
expected_message_type
):
def
test_message_type
(
self
,
register_message_function
,
expected_message_type
):
...
@@ -80,6 +72,6 @@ class UserMessagesTestCase(TestCase):
...
@@ -80,6 +72,6 @@ class UserMessagesTestCase(TestCase):
Verifies that each user message function returns the correct type.
Verifies that each user message function returns the correct type.
"""
"""
register_message_function
(
self
.
request
,
TEST_MESSAGE
)
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
.
assertEqual
(
len
(
messages
),
1
)
self
.
assertEquals
(
messages
[
0
]
.
type
,
expected_message_type
)
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:
...
@@ -14,12 +14,12 @@ There are two common use cases:
used to show a success message to the use.
used to show a success message to the use.
"""
"""
from
abc
import
abstractmethod
from
enum
import
Enum
from
enum
import
Enum
from
django.contrib
import
messages
from
django.contrib
import
messages
from
openedx.core.djangolib.markup
import
Text
from
django.utils.translation
import
ugettext
as
_
from
openedx.core.djangolib.markup
import
Text
,
HTML
EDX_USER_MESSAGE_TAG
=
'edx-user-message'
class
UserMessageType
(
Enum
):
class
UserMessageType
(
Enum
):
...
@@ -49,7 +49,7 @@ ICON_CLASSES = {
...
@@ -49,7 +49,7 @@ ICON_CLASSES = {
class
UserMessage
():
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
):
def
__init__
(
self
,
type
,
message_html
):
assert
isinstance
(
type
,
UserMessageType
)
assert
isinstance
(
type
,
UserMessageType
)
...
@@ -67,71 +67,124 @@ class UserMessage():
...
@@ -67,71 +67,124 @@ class UserMessage():
def
icon_class
(
self
):
def
icon_class
(
self
):
"""
"""
Returns the CSS icon class representing the message type.
Returns the CSS icon class representing the message type.
Returns:
"""
"""
return
ICON_CLASSES
[
self
.
type
]
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
)
@classmethod
messages
.
add_message
(
request
,
message_type
.
value
,
Text
(
message
),
extra_tags
=
EDX_USER_MESSAGE_TAG
)
@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
):
@classmethod
"""
def
get_message_html
(
self
,
body_html
,
title
=
None
):
Registers an information message to be shown to the user.
"""
"""
Returns the entire HTML snippet for the message.
register_user_message
(
request
,
UserMessageType
.
INFO
,
message
,
**
kwargs
)
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
):
Arguments:
"""
message_type (UserMessageType): the user message type
Registers a success message to be shown to the user.
body_html (str): body of the message in html format
"""
title (str): optional title for the message as plain text
register_user_message
(
request
,
UserMessageType
.
SUCCESS
,
message
,
**
kwargs
)
"""
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
):
@classmethod
"""
def
register_success_message
(
self
,
request
,
message
,
**
kwargs
):
Registers a warning message to be shown to the user.
"""
"""
Registers a success message to be shown to the user.
register_user_message
(
request
,
UserMessageType
.
WARNING
,
message
,
**
kwargs
)
"""
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
):
@classmethod
"""
def
register_error_message
(
self
,
request
,
message
,
**
kwargs
):
Registers an error message to be shown to the user.
"""
"""
Registers an error message to be shown to the user.
register_user_message
(
request
,
UserMessageType
.
ERROR
,
message
,
**
kwargs
)
"""
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
class
PageLevelMessages
(
UserMessageCollection
):
so they won't be returned in the next request.
"""
"""
def
_get_message_type_for_level
(
level
):
This set of messages appears as top page level messages.
"""
"""
Returns the user message type associated with a level.
NAMESPACE
=
'page_level_messages'
"""
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
):
@classmethod
def
get_namespace
(
self
):
"""
"""
Creates a user message from a Django message
.
Returns the namespace of the message collection
.
"""
"""
return
UserMessage
(
return
self
.
NAMESPACE
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
)
openedx/features/course_experience/__init__.py
View file @
b523ac3f
...
@@ -3,7 +3,8 @@ Unified course experience settings and helper methods.
...
@@ -3,7 +3,8 @@ Unified course experience settings and helper methods.
"""
"""
from
django.utils.translation
import
ugettext
as
_
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.
# Namespace for course experience waffle flags.
...
@@ -58,3 +59,17 @@ def course_home_url_name(course_key):
...
@@ -58,3 +59,17 @@ def course_home_url_name(course_key):
return
'openedx.course_experience.course_home'
return
'openedx.course_experience.course_home'
else
:
else
:
return
'info'
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
...
@@ -57,6 +57,10 @@ from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG, SHOW_REV
<div
class=
"page-content"
>
<div
class=
"page-content"
>
<div
class=
"layout layout-1t2t"
>
<div
class=
"layout layout-1t2t"
>
<main
class=
"layout-col layout-col-b"
>
<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):
% if welcome_message_fragment and UNIFIED_COURSE_TAB_FLAG.is_enabled(course.id):
<div
class=
"section section-dates"
>
<div
class=
"section section-dates"
>
${HTML(welcome_message_fragment.body_html())}
${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 @@
...
@@ -2,10 +2,10 @@
"""
"""
Tests for the course home page.
Tests for the course home page.
"""
"""
import
datetime
from
datetime
import
datetime
,
timedelta
import
ddt
import
ddt
import
mock
import
mock
import
pytz
from
pytz
import
UTC
from
waffle.testutils
import
override_flag
from
waffle.testutils
import
override_flag
from
courseware.tests.factories
import
StaffFactory
from
courseware.tests.factories
import
StaffFactory
...
@@ -31,6 +31,10 @@ TEST_CHAPTER_NAME = 'Test Chapter'
...
@@ -31,6 +31,10 @@ TEST_CHAPTER_NAME = 'Test Chapter'
TEST_WELCOME_MESSAGE
=
'<h2>Welcome!</h2>'
TEST_WELCOME_MESSAGE
=
'<h2>Welcome!</h2>'
TEST_UPDATE_MESSAGE
=
'<h2>Test Update!</h2>'
TEST_UPDATE_MESSAGE
=
'<h2>Test Update!</h2>'
TEST_COURSE_UPDATES_TOOL
=
'/course/updates">'
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
QUERY_COUNT_TABLE_BLACKLIST
=
WAFFLE_TABLES
...
@@ -73,7 +77,12 @@ class CourseHomePageTestCase(SharedModuleStoreTestCase):
...
@@ -73,7 +77,12 @@ class CourseHomePageTestCase(SharedModuleStoreTestCase):
# pylint: disable=super-method-not-called
# pylint: disable=super-method-not-called
with
super
(
CourseHomePageTestCase
,
cls
)
.
setUpClassAndTestData
():
with
super
(
CourseHomePageTestCase
,
cls
)
.
setUpClassAndTestData
():
with
cls
.
store
.
default_store
(
ModuleStoreEnum
.
Type
.
split
):
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
):
with
cls
.
store
.
bulk_operations
(
cls
.
course
.
id
):
chapter
=
ItemFactory
.
create
(
chapter
=
ItemFactory
.
create
(
category
=
'chapter'
,
category
=
'chapter'
,
...
@@ -92,6 +101,15 @@ class CourseHomePageTestCase(SharedModuleStoreTestCase):
...
@@ -92,6 +101,15 @@ class CourseHomePageTestCase(SharedModuleStoreTestCase):
cls
.
user
=
UserFactory
(
password
=
TEST_PASSWORD
)
cls
.
user
=
UserFactory
(
password
=
TEST_PASSWORD
)
CourseEnrollment
.
enroll
(
cls
.
user
,
cls
.
course
.
id
)
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
):
class
TestCourseHomePage
(
CourseHomePageTestCase
):
def
setUp
(
self
):
def
setUp
(
self
):
...
@@ -152,18 +170,15 @@ class TestCourseHomePage(CourseHomePageTestCase):
...
@@ -152,18 +170,15 @@ class TestCourseHomePage(CourseHomePageTestCase):
"""
"""
Verify that the course home page handles start dates correctly.
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
# 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
)
response
=
self
.
client
.
get
(
url
)
self
.
assertRedirects
(
response
,
'/dashboard?notlive=Jan+01
%2
C+2030'
)
self
.
assertRedirects
(
response
,
'/dashboard?notlive=Jan+01
%2
C+2030'
)
# With the Waffle flag enabled, the course should be visible
# With the Waffle flag enabled, the course should be visible
with
override_flag
(
COURSE_PRE_START_ACCESS_FLAG
.
namespaced_flag_name
,
True
):
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
)
response
=
self
.
client
.
get
(
url
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
status_code
,
200
)
...
@@ -272,11 +287,12 @@ class TestCourseHomePageAccess(CourseHomePageTestCase):
...
@@ -272,11 +287,12 @@ class TestCourseHomePageAccess(CourseHomePageTestCase):
Ensure that a user accessing a non-live course sees a redirect to
Ensure that a user accessing a non-live course sees a redirect to
the student dashboard, not a 404.
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
)
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
=
QueryDict
(
mutable
=
True
)
expected_params
[
'notlive'
]
=
start_date
expected_params
[
'notlive'
]
=
start_date
expected_url
=
'{url}?{params}'
.
format
(
expected_url
=
'{url}?{params}'
.
format
(
...
@@ -292,12 +308,13 @@ class TestCourseHomePageAccess(CourseHomePageTestCase):
...
@@ -292,12 +308,13 @@ class TestCourseHomePageAccess(CourseHomePageTestCase):
Ensure that a user accessing a non-live course sees a redirect to
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
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îµé"
fake_unicode_start_time
=
u"üñîçø∂é_ßtå®t_tîµé"
mock_strftime_localized
.
return_value
=
fake_unicode_start_time
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
)
response
=
self
.
client
.
get
(
url
)
expected_params
=
QueryDict
(
mutable
=
True
)
expected_params
=
QueryDict
(
mutable
=
True
)
expected_params
[
'notlive'
]
=
fake_unicode_start_time
expected_params
[
'notlive'
]
=
fake_unicode_start_time
...
@@ -316,3 +333,44 @@ class TestCourseHomePageAccess(CourseHomePageTestCase):
...
@@ -316,3 +333,44 @@ class TestCourseHomePageAccess(CourseHomePageTestCase):
url
=
course_home_url_from_string
(
'not/a/course'
)
url
=
course_home_url_from_string
(
'not/a/course'
)
response
=
self
.
client
.
get
(
url
)
response
=
self
.
client
.
get
(
url
)
self
.
assertEqual
(
response
.
status_code
,
404
)
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
...
@@ -26,6 +26,7 @@ from web_fragments.fragment import Fragment
from
..utils
import
get_course_outline_block_tree
from
..utils
import
get_course_outline_block_tree
from
.course_dates
import
CourseDatesFragmentView
from
.course_dates
import
CourseDatesFragmentView
from
.course_home_messages
import
CourseHomeMessageFragmentView
from
.course_outline
import
CourseOutlineFragmentView
from
.course_outline
import
CourseOutlineFragmentView
from
.course_sock
import
CourseSockFragmentView
from
.course_sock
import
CourseSockFragmentView
from
.welcome_message
import
WelcomeMessageFragmentView
from
.welcome_message
import
WelcomeMessageFragmentView
...
@@ -113,9 +114,12 @@ class CourseHomeFragmentView(EdxFragmentView):
...
@@ -113,9 +114,12 @@ class CourseHomeFragmentView(EdxFragmentView):
# Render the full content to enrolled users, as well as to course and global staff.
# 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.
# Unenrolled users who are not course or global staff are given only a subset.
is_enrolled
=
CourseEnrollment
.
is_enrolled
(
request
.
user
,
course_key
)
user_access
=
{
is_staff
=
has_access
(
request
.
user
,
'staff'
,
course_key
)
'is_anonymous'
:
request
.
user
.
is_anonymous
(),
if
is_enrolled
or
is_staff
:
'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
)
outline_fragment
=
CourseOutlineFragmentView
()
.
render_to_fragment
(
request
,
course_id
=
course_id
,
**
kwargs
)
welcome_message_fragment
=
WelcomeMessageFragmentView
()
.
render_to_fragment
(
welcome_message_fragment
=
WelcomeMessageFragmentView
()
.
render_to_fragment
(
request
,
course_id
=
course_id
,
**
kwargs
request
,
course_id
=
course_id
,
**
kwargs
...
@@ -141,6 +145,11 @@ class CourseHomeFragmentView(EdxFragmentView):
...
@@ -141,6 +145,11 @@ class CourseHomeFragmentView(EdxFragmentView):
# Get the course tools enabled for this user and course
# Get the course tools enabled for this user and course
course_tools
=
CourseToolsPluginManager
.
get_enabled_course_tools
(
request
,
course_key
)
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
# Render the course home fragment
context
=
{
context
=
{
'request'
:
request
,
'request'
:
request
,
...
@@ -149,6 +158,7 @@ class CourseHomeFragmentView(EdxFragmentView):
...
@@ -149,6 +158,7 @@ class CourseHomeFragmentView(EdxFragmentView):
'course_key'
:
course_key
,
'course_key'
:
course_key
,
'outline_fragment'
:
outline_fragment
,
'outline_fragment'
:
outline_fragment
,
'handouts_html'
:
handouts_html
,
'handouts_html'
:
handouts_html
,
'course_home_message_fragment'
:
course_home_message_fragment
,
'has_visited_course'
:
has_visited_course
,
'has_visited_course'
:
has_visited_course
,
'resume_course_url'
:
resume_course_url
,
'resume_course_url'
:
resume_course_url
,
'course_tools'
:
course_tools
,
'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