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
7dfe12a1
Commit
7dfe12a1
authored
Sep 27, 2017
by
Andy Armstrong
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Show course home messages for important course dates
LEARNER-2073
parent
1aff33dc
Show whitespace changes
Inline
Side-by-side
Showing
21 changed files
with
504 additions
and
180 deletions
+504
-180
common/static/sass/edx-pattern-library-shims/_buttons.scss
+29
-0
common/static/sass/edx-pattern-library-shims/base/_variables.scss
+12
-3
lms/djangoapps/commerce/utils.py
+15
-0
lms/djangoapps/courseware/date_summary.py
+170
-24
lms/djangoapps/courseware/tests/test_date_summary.py
+134
-40
lms/djangoapps/courseware/tests/test_views.py
+2
-2
lms/envs/common.py
+4
-0
lms/static/sass/_build-base-v1.scss
+4
-0
lms/static/sass/_build-lms-v2.scss
+1
-0
lms/static/sass/course/_info.scss
+0
-35
lms/static/sass/elements-v2/_buttons.scss
+42
-0
lms/static/sass/features/_course-experience.scss
+12
-2
lms/static/sass/features/_course-sock.scss
+0
-9
lms/static/sass/features/_course-upgrade-message.scss
+0
-7
openedx/features/course_experience/__init__.py
+4
-0
openedx/features/course_experience/templates/course_experience/course-home-fragment.html
+1
-1
openedx/features/course_experience/templates/course_experience/course-sock-fragment.html
+3
-3
openedx/features/course_experience/tests/views/test_course_home.py
+4
-6
openedx/features/course_experience/views/course_home.py
+3
-9
openedx/features/course_experience/views/course_home_messages.py
+58
-36
openedx/features/course_experience/views/course_sock.py
+6
-3
No files found.
common/static/sass/edx-pattern-library-shims/_buttons.scss
View file @
7dfe12a1
...
...
@@ -121,3 +121,32 @@
color
:
$btn-brand-disabled-color
;
}
}
// ----------------------------
// #UPGRADE
// ----------------------------
.btn-upgrade
{
@extend
%btn-shims
;
border-color
:
$btn-upgrade-border-color
;
background
:
$btn-upgrade-background
;
color
:
$btn-upgrade-color
;
// STATE: hover and focus
&
:hover
,
&
.is-hovered
,
&
:focus
,
&
.is-focused
{
border-color
:
$btn-upgrade-focus-border-color
;
background-color
:
$btn-upgrade-focus-background
;
color
:
$btn-upgrade-focus-color
;
}
// STATE: is disabled
&
:disabled
,
&
.is-disabled
{
border-color
:
$btn-disabled-border-color
;
background
:
$btn-brand-disabled-background
;
color
:
$btn-upgrade-color
;
}
}
common/static/sass/edx-pattern-library-shims/base/_variables.scss
View file @
7dfe12a1
...
...
@@ -143,9 +143,8 @@ $error-color: rgb(203, 7, 18) !default;
$success-color
:
rgb
(
0
,
155
,
0
)
!
default
;
$warning-color
:
rgb
(
255
,
192
,
31
)
!
default
;
$warning-color-accent
:
rgb
(
255
,
252
,
221
)
!
default
;
$general-color
:
$uxpl-blue-base
!
default
;;
$general-color-accent
:
$uxpl-blue-base
!
default
$general-color
:
$uxpl-blue-base
!
default
;
$general-color-accent
:
$uxpl-blue-base
!
default
;
// CAPA correctness color to be consistent with Alert styles above
$correct
:
$success-color
!
default
;
...
...
@@ -181,6 +180,16 @@ $btn-brand-active-background: $uxpl-blue-base !default;
$btn-brand-disabled-background
:
#f2f3f3
!
default
;
$btn-brand-disabled-color
:
#676666
!
default
;
// Upgrade button
$btn-upgrade-border-color
:
$uxpl-green-base
!
default
;
$btn-upgrade-background
:
$uxpl-green-base
!
default
;
$btn-upgrade-color
:
#fcfcfc
!
default
;
$btn-upgrade-focus-color
:
$btn-upgrade-color
!
default
;
$btn-upgrade-focus-border-color
:
rgb
(
0
,
155
,
0
)
!
default
;
$btn-upgrade-focus-background
:
rgb
(
0
,
155
,
0
)
!
default
;
$btn-upgrade-active-border-color
:
$uxpl-green-base
!
default
;
$btn-upgrade-active-background
:
$uxpl-green-base
!
default
;
// ----------------------------
// #SETTINGS
// ----------------------------
...
...
lms/djangoapps/commerce/utils.py
View file @
7dfe12a1
...
...
@@ -4,6 +4,8 @@ from urlparse import urljoin
import
waffle
from
django.conf
import
settings
from
django.core.urlresolvers
import
reverse
from
student.models
import
CourseEnrollment
from
commerce.models
import
CommerceConfiguration
from
openedx.core.djangoapps.site_configuration
import
helpers
as
configuration_helpers
...
...
@@ -93,3 +95,16 @@ class EcommerceService(object):
checkout_page_path
=
self
.
get_absolute_ecommerce_url
(
self
.
config
.
MULTIPLE_ITEMS_BASKET_PAGE_URL
),
skus
=
urlencode
({
'sku'
:
skus
},
doseq
=
True
),
)
def
upgrade_url
(
self
,
user
,
course_key
):
"""
Returns the URL for the user to upgrade, or None if not applicable.
"""
enrollment
=
CourseEnrollment
.
get_enrollment
(
user
,
course_key
)
verified_mode
=
enrollment
.
verified_mode
if
enrollment
else
None
if
verified_mode
:
if
self
.
is_enabled
(
user
):
return
self
.
get_checkout_page_url
(
verified_mode
.
sku
)
else
:
return
reverse
(
'verify_student_upgrade_and_verify'
,
args
=
(
course_key
,))
return
None
lms/djangoapps/courseware/date_summary.py
View file @
7dfe12a1
...
...
@@ -3,26 +3,45 @@ This module provides date summary blocks for the Course Info
page. Each block gives information about a particular
course-run-specific date which will be displayed to the user.
"""
import
crum
import
datetime
from
babel.dates
import
format_timedelta
from
django.conf
import
settings
from
django.core.urlresolvers
import
reverse
from
django.utils.functional
import
cached_property
from
django.utils.translation
import
get_language
,
to_locale
,
ugettext_lazy
from
django.utils.translation
import
ugettext
as
_
from
lazy
import
lazy
from
pytz
import
timezone
,
utc
from
pytz
import
utc
from
course_modes.models
import
CourseMode
from
course_modes.models
import
CourseMode
,
get_cosmetic_verified_display_price
from
lms.djangoapps.commerce.utils
import
EcommerceService
from
lms.djangoapps.verify_student.models
import
SoftwareSecurePhotoVerification
,
VerificationDeadline
from
openedx.core.djangoapps.certificates.api
import
can_show_certificate_available_date_field
from
openedx.core.djangolib.markup
import
HTML
,
Text
from
openedx.features.course_experience
import
CourseHomeMessages
,
UPGRADE_DEADLINE_MESSAGE
from
student.models
import
CourseEnrollment
from
.context_processor
import
user_timezone_locale_prefs
class
DateSummary
(
object
):
"""Base class for all date summary blocks."""
# A consistent representation of the current time.
_current_time
=
None
@property
def
current_time
(
self
):
"""
Returns a consistent current time.
"""
if
self
.
_current_time
is
None
:
self
.
_current_time
=
datetime
.
datetime
.
now
(
utc
)
return
self
.
_current_time
@property
def
css_class
(
self
):
"""
...
...
@@ -41,6 +60,12 @@ class DateSummary(object):
"""The detail text displayed by this summary."""
return
''
def
register_alerts
(
self
,
request
,
course
):
"""
Registers any relevant course alerts given the current request.
"""
pass
@property
def
date
(
self
):
"""This summary's date."""
...
...
@@ -64,15 +89,6 @@ class DateSummary(object):
"""The text of the link."""
return
''
@property
def
time_zone
(
self
):
"""
The time zone in which to display -- defaults to UTC
"""
return
timezone
(
self
.
user
.
preferences
.
model
.
get_value
(
self
.
user
,
"time_zone"
,
"UTC"
)
)
def
__init__
(
self
,
course
,
user
,
course_id
=
None
):
self
.
course
=
course
self
.
user
=
user
...
...
@@ -87,7 +103,7 @@ class DateSummary(object):
if
self
.
date
is
None
:
return
''
locale
=
to_locale
(
get_language
())
delta
=
self
.
date
-
datetime
.
datetime
.
now
(
utc
)
delta
=
self
.
date
-
self
.
current_time
try
:
relative_date
=
format_timedelta
(
delta
,
locale
=
locale
)
# Babel doesn't have translations for Esperanto, so we get
...
...
@@ -117,7 +133,7 @@ class DateSummary(object):
future.
"""
if
self
.
date
is
not
None
:
return
datetime
.
datetime
.
now
(
utc
)
.
date
()
<=
self
.
date
.
date
()
return
self
.
current_time
.
date
()
<=
self
.
date
.
date
()
return
False
def
deadline_has_passed
(
self
):
...
...
@@ -126,7 +142,52 @@ class DateSummary(object):
Returns False otherwise.
"""
deadline
=
self
.
date
return
deadline
is
not
None
and
deadline
<=
datetime
.
datetime
.
now
(
utc
)
return
deadline
is
not
None
and
deadline
<=
self
.
current_time
@property
def
time_remaining_string
(
self
):
"""
Returns the time remaining as a localized string.
"""
locale
=
to_locale
(
get_language
())
return
format_timedelta
(
self
.
date
-
self
.
current_time
,
locale
=
locale
)
def
date_html
(
self
,
date_format
=
'shortDate'
):
"""
Returns a representation of the date as HTML.
Note: this returns a span that will be localized on the client.
"""
locale
=
to_locale
(
get_language
())
user_timezone
=
user_timezone_locale_prefs
(
crum
.
get_current_request
())[
'user_timezone'
]
return
HTML
(
'<span class="date localized-datetime" data-format="{date_format}" data-datetime="{date_time}"'
' data-timezone="{user_timezone}" data-language="{user_language}">'
'</span>'
)
.
format
(
date_format
=
date_format
,
date_time
=
self
.
date
,
user_timezone
=
user_timezone
,
user_language
=
locale
,
)
@property
def
long_date_html
(
self
):
"""
Returns a long representation of the date as HTML.
Note: this returns a span that will be localized on the client.
"""
return
self
.
date_html
(
date_format
=
'shortDate'
)
@property
def
short_time_html
(
self
):
"""
Returns a short representation of the time as HTML.
Note: this returns a span that will be localized on the client.
"""
return
self
.
date_html
(
date_format
=
'shortTime'
)
def
__repr__
(
self
):
return
u'DateSummary: "{title}" {date} is_enabled={is_enabled}'
.
format
(
...
...
@@ -151,7 +212,7 @@ class TodaysDate(DateSummary):
@property
def
date
(
self
):
return
datetime
.
datetime
.
now
(
utc
)
return
self
.
current_time
@property
def
title
(
self
):
...
...
@@ -169,6 +230,35 @@ class CourseStartDate(DateSummary):
def
date
(
self
):
return
self
.
course
.
start
def
register_alerts
(
self
,
request
,
course
):
"""
Registers an alert if the course has not started yet.
"""
is_enrolled
=
CourseEnrollment
.
get_enrollment
(
request
.
user
,
course
.
id
)
if
not
course
.
start
or
not
is_enrolled
:
return
days_until_start
=
(
course
.
start
-
self
.
current_time
)
.
days
if
course
.
start
>
self
.
current_time
:
if
days_until_start
>
0
:
CourseHomeMessages
.
register_info_message
(
request
,
Text
(
_
(
"Don't forget to add a calendar reminder!"
)),
title
=
Text
(
_
(
"Course starts in {time_remaining_string} on {course_start_date}."
))
.
format
(
time_remaining_string
=
self
.
time_remaining_string
,
course_start_date
=
self
.
long_date_html
,
)
)
else
:
CourseHomeMessages
.
register_info_message
(
request
,
Text
(
_
(
"Course starts in {time_remaining_string} at {course_start_time}."
))
.
format
(
time_remaining_string
=
self
.
time_remaining_string
,
course_start_time
=
self
.
short_time_html
,
)
)
class
CourseEndDate
(
DateSummary
):
"""
...
...
@@ -183,7 +273,7 @@ class CourseEndDate(DateSummary):
@property
def
description
(
self
):
if
datetime
.
datetime
.
now
(
utc
)
<=
self
.
date
:
if
self
.
current_time
<=
self
.
date
:
mode
,
is_active
=
CourseEnrollment
.
enrollment_mode_for_user
(
self
.
user
,
self
.
course_id
)
if
is_active
and
CourseMode
.
is_eligible_for_certificate
(
mode
):
return
_
(
'To earn a certificate, you must complete all requirements before this date.'
)
...
...
@@ -195,6 +285,35 @@ class CourseEndDate(DateSummary):
def
date
(
self
):
return
self
.
course
.
end
def
register_alerts
(
self
,
request
,
course
):
"""
Registers an alert if the end date is approaching.
"""
is_enrolled
=
CourseEnrollment
.
get_enrollment
(
request
.
user
,
course
.
id
)
if
not
course
.
start
or
self
.
current_time
<
course
.
start
or
not
is_enrolled
:
return
days_until_end
=
(
course
.
end
-
self
.
current_time
)
.
days
if
course
.
end
>
self
.
current_time
and
days_until_end
<=
settings
.
COURSE_MESSAGE_ALERT_DURATION_IN_DAYS
:
if
days_until_end
>
0
:
CourseHomeMessages
.
register_info_message
(
request
,
Text
(
self
.
description
),
title
=
Text
(
_
(
'This course is ending in {time_remaining_string} on {course_end_date}.'
))
.
format
(
time_remaining_string
=
self
.
time_remaining_string
,
course_end_date
=
self
.
long_date_html
,
)
)
else
:
CourseHomeMessages
.
register_info_message
(
request
,
Text
(
self
.
description
),
title
=
Text
(
_
(
'This course is ending in {time_remaining_string} at {course_end_time}.'
))
.
format
(
time_remaining_string
=
self
.
time_remaining_string
,
course_end_time
=
self
.
short_time_html
,
)
)
class
CertificateAvailableDate
(
DateSummary
):
"""
...
...
@@ -216,7 +335,7 @@ class CertificateAvailableDate(DateSummary):
can_show_certificate_available_date_field
(
self
.
course
)
and
self
.
has_certificate_modes
and
self
.
date
is
not
None
and
datetime
.
datetime
.
now
(
utc
)
<=
self
.
date
and
self
.
current_time
<=
self
.
date
and
len
(
self
.
active_certificates
)
>
0
)
...
...
@@ -252,13 +371,7 @@ class VerifiedUpgradeDeadlineDate(DateSummary):
@property
def
link
(
self
):
ecommerce_service
=
EcommerceService
()
if
ecommerce_service
.
is_enabled
(
self
.
user
):
course_mode
=
CourseMode
.
objects
.
get
(
course_id
=
self
.
course_id
,
mode_slug
=
CourseMode
.
VERIFIED
)
return
ecommerce_service
.
get_checkout_page_url
(
course_mode
.
sku
)
return
reverse
(
'verify_student_upgrade_and_verify'
,
args
=
(
self
.
course_id
,))
return
EcommerceService
()
.
upgrade_url
(
self
.
user
,
self
.
course_id
)
@cached_property
def
enrollment
(
self
):
...
...
@@ -299,6 +412,39 @@ class VerifiedUpgradeDeadlineDate(DateSummary):
return
deadline
def
register_alerts
(
self
,
request
,
course
):
"""
Registers an alert if the verification deadline is approaching.
"""
upgrade_price
=
get_cosmetic_verified_display_price
(
course
)
if
not
UPGRADE_DEADLINE_MESSAGE
.
is_enabled
(
course
.
id
)
or
not
self
.
is_enabled
or
not
upgrade_price
:
return
days_left_to_upgrade
=
(
self
.
date
-
self
.
current_time
)
.
days
if
self
.
date
>
self
.
current_time
and
days_left_to_upgrade
<=
settings
.
COURSE_MESSAGE_ALERT_DURATION_IN_DAYS
:
CourseHomeMessages
.
register_info_message
(
request
,
Text
(
_
(
'In order to qualify for a certificate, you must meet all course grading '
'requirements, upgrade before the course deadline, and successfully verify '
'your identity on {platform_name} if you have not done so already.{button_panel}'
))
.
format
(
platform_name
=
settings
.
PLATFORM_NAME
,
button_panel
=
HTML
(
'<div class="message-actions">'
'<a class="btn btn-upgrade" href="{upgrade_url}">{upgrade_label}</a>'
'</div>'
)
.
format
(
upgrade_url
=
self
.
link
,
upgrade_label
=
Text
(
_
(
'Upgrade ({upgrade_price})'
))
.
format
(
upgrade_price
=
upgrade_price
),
)
),
title
=
Text
(
_
(
"Don't forget, you have {time_remaining_string} left to upgrade to a Verified Certificate."
))
.
format
(
time_remaining_string
=
self
.
time_remaining_string
,
)
)
class
VerificationDeadlineDate
(
DateSummary
):
"""
...
...
lms/djangoapps/courseware/tests/test_date_summary.py
View file @
7dfe12a1
...
...
@@ -4,7 +4,9 @@ from datetime import datetime, timedelta
import
ddt
import
waffle
from
django.contrib.messages.middleware
import
MessageMiddleware
from
django.core.urlresolvers
import
reverse
from
django.test
import
RequestFactory
,
TestCase
from
freezegun
import
freeze_time
from
mock
import
patch
from
nose.plugins.attrib
import
attr
...
...
@@ -31,7 +33,7 @@ from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
from
openedx.core.djangoapps.site_configuration.tests.factories
import
SiteFactory
from
openedx.core.djangoapps.user_api.preferences.api
import
set_user_preference
from
openedx.core.djangoapps.waffle_utils.testutils
import
override_waffle_flag
from
openedx.features.course_experience
import
UNIFIED_COURSE_TAB_FLAG
from
openedx.features.course_experience
import
CourseHomeMessages
,
UNIFIED_COURSE_TAB_FLAG
,
UPGRADE_DEADLINE_MESSAGE
from
student.tests.factories
import
CourseEnrollmentFactory
,
UserFactory
,
TEST_PASSWORD
from
xmodule.modulestore.tests.django_utils
import
SharedModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
...
...
@@ -46,20 +48,6 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
super
(
CourseDateSummaryTest
,
self
)
.
setUp
()
SelfPacedConfiguration
.
objects
.
create
(
enable_course_home_improvements
=
True
)
def
create_user
(
self
,
verification_status
=
None
):
""" Create a new User instance.
Arguments:
verification_status (str): User's verification status. If this value is set an instance of
SoftwareSecurePhotoVerification will be created for the user with the specified status.
"""
user
=
UserFactory
()
if
verification_status
is
not
None
:
SoftwareSecurePhotoVerificationFactory
.
create
(
user
=
user
,
status
=
verification_status
)
return
user
def
enable_course_certificates
(
self
,
course
):
""" Enable course certificate configuration """
course
.
certificates
=
{
...
...
@@ -74,7 +62,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
def
test_course_info_feature_flag
(
self
):
SelfPacedConfiguration
(
enable_course_home_improvements
=
False
)
.
save
()
course
=
create_course_run
()
user
=
self
.
create_user
()
user
=
create_user
()
CourseEnrollmentFactory
(
course_id
=
course
.
id
,
user
=
user
,
mode
=
CourseMode
.
VERIFIED
)
self
.
client
.
login
(
username
=
user
.
username
,
password
=
TEST_PASSWORD
)
...
...
@@ -144,7 +132,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
@ddt.unpack
def
test_enabled_block_types
(
self
,
course_kwargs
,
user_kwargs
,
expected_blocks
):
course
=
create_course_run
(
**
course_kwargs
)
user
=
self
.
create_user
(
**
user_kwargs
)
user
=
create_user
(
**
user_kwargs
)
CourseEnrollmentFactory
(
course_id
=
course
.
id
,
user
=
user
,
mode
=
CourseMode
.
VERIFIED
)
self
.
assert_block_types
(
course
,
user
,
expected_blocks
)
...
...
@@ -160,12 +148,12 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
@ddt.unpack
def
test_enabled_block_types_without_enrollment
(
self
,
course_kwargs
,
expected_blocks
):
course
=
create_course_run
(
**
course_kwargs
)
user
=
self
.
create_user
()
user
=
create_user
()
self
.
assert_block_types
(
course
,
user
,
expected_blocks
)
def
test_enabled_block_types_with_non_upgradeable_course_run
(
self
):
course
=
create_course_run
(
days_till_start
=-
10
,
days_till_verification_deadline
=
None
)
user
=
self
.
create_user
()
user
=
create_user
()
CourseMode
.
objects
.
get
(
course_id
=
course
.
id
,
mode_slug
=
CourseMode
.
VERIFIED
)
.
delete
()
CourseEnrollmentFactory
(
course_id
=
course
.
id
,
user
=
user
,
mode
=
CourseMode
.
AUDIT
)
self
.
assert_block_types
(
course
,
user
,
(
TodaysDate
,
CourseEndDate
))
...
...
@@ -177,7 +165,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
"""
with
freeze_time
(
'2015-01-02'
):
course
=
create_course_run
()
user
=
self
.
create_user
()
user
=
create_user
()
block
=
TodaysDate
(
course
,
user
)
self
.
assertTrue
(
block
.
is_enabled
)
self
.
assertEqual
(
block
.
date
,
datetime
.
now
(
utc
))
...
...
@@ -191,7 +179,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
def
test_todays_date_no_timezone
(
self
,
url_name
):
with
freeze_time
(
'2015-01-02'
):
course
=
create_course_run
()
user
=
self
.
create_user
()
user
=
create_user
()
self
.
client
.
login
(
username
=
user
.
username
,
password
=
TEST_PASSWORD
)
html_elements
=
[
...
...
@@ -216,7 +204,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
def
test_todays_date_timezone
(
self
,
url_name
):
with
freeze_time
(
'2015-01-02'
):
course
=
create_course_run
()
user
=
self
.
create_user
()
user
=
create_user
()
self
.
client
.
login
(
username
=
user
.
username
,
password
=
TEST_PASSWORD
)
set_user_preference
(
user
,
'time_zone'
,
'America/Los_Angeles'
)
url
=
reverse
(
url_name
,
args
=
(
course
.
id
,))
...
...
@@ -237,7 +225,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
## Tests Course Start Date
def
test_course_start_date
(
self
):
course
=
create_course_run
()
user
=
self
.
create_user
()
user
=
create_user
()
block
=
CourseStartDate
(
course
,
user
)
self
.
assertEqual
(
block
.
date
,
course
.
start
)
...
...
@@ -249,7 +237,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
def
test_start_date_render
(
self
,
url_name
):
with
freeze_time
(
'2015-01-02'
):
course
=
create_course_run
()
user
=
self
.
create_user
()
user
=
create_user
()
self
.
client
.
login
(
username
=
user
.
username
,
password
=
TEST_PASSWORD
)
url
=
reverse
(
url_name
,
args
=
(
course
.
id
,))
response
=
self
.
client
.
get
(
url
,
follow
=
True
)
...
...
@@ -268,7 +256,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
def
test_start_date_render_time_zone
(
self
,
url_name
):
with
freeze_time
(
'2015-01-02'
):
course
=
create_course_run
()
user
=
self
.
create_user
()
user
=
create_user
()
self
.
client
.
login
(
username
=
user
.
username
,
password
=
TEST_PASSWORD
)
set_user_preference
(
user
,
'time_zone'
,
'America/Los_Angeles'
)
url
=
reverse
(
url_name
,
args
=
(
course
.
id
,))
...
...
@@ -284,7 +272,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
## Tests Course End Date Block
def
test_course_end_date_for_certificate_eligible_mode
(
self
):
course
=
create_course_run
(
days_till_start
=-
1
)
user
=
self
.
create_user
()
user
=
create_user
()
CourseEnrollmentFactory
(
course_id
=
course
.
id
,
user
=
user
,
mode
=
CourseMode
.
VERIFIED
)
block
=
CourseEndDate
(
course
,
user
)
self
.
assertEqual
(
...
...
@@ -294,7 +282,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
def
test_course_end_date_for_non_certificate_eligible_mode
(
self
):
course
=
create_course_run
(
days_till_start
=-
1
)
user
=
self
.
create_user
()
user
=
create_user
()
CourseEnrollmentFactory
(
course_id
=
course
.
id
,
user
=
user
,
mode
=
CourseMode
.
AUDIT
)
block
=
CourseEndDate
(
course
,
user
)
self
.
assertEqual
(
...
...
@@ -305,7 +293,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
def
test_course_end_date_after_course
(
self
):
course
=
create_course_run
(
days_till_start
=-
2
,
days_till_end
=-
1
)
user
=
self
.
create_user
()
user
=
create_user
()
CourseEnrollmentFactory
(
course_id
=
course
.
id
,
user
=
user
,
mode
=
CourseMode
.
VERIFIED
)
block
=
CourseEndDate
(
course
,
user
)
self
.
assertEqual
(
...
...
@@ -319,7 +307,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
sku
=
'TESTSKU'
configuration
=
CommerceConfiguration
.
objects
.
create
(
checkout_on_ecommerce_service
=
True
)
course
=
create_course_run
()
user
=
self
.
create_user
()
user
=
create_user
()
course_mode
=
CourseMode
.
objects
.
get
(
course_id
=
course
.
id
,
mode_slug
=
CourseMode
.
VERIFIED
)
course_mode
.
sku
=
sku
course_mode
.
save
()
...
...
@@ -332,7 +320,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
@waffle.testutils.override_switch
(
'certificates.auto_certificate_generation'
,
True
)
def
test_no_certificate_available_date
(
self
):
course
=
create_course_run
(
days_till_start
=-
1
)
user
=
self
.
create_user
()
user
=
create_user
()
CourseEnrollmentFactory
(
course_id
=
course
.
id
,
user
=
user
,
mode
=
CourseMode
.
AUDIT
)
block
=
CertificateAvailableDate
(
course
,
user
)
self
.
assertEqual
(
block
.
date
,
None
)
...
...
@@ -342,7 +330,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
@waffle.testutils.override_switch
(
'certificates.auto_certificate_generation'
,
True
)
def
test_no_certificate_available_date_for_self_paced
(
self
):
course
=
create_self_paced_course_run
()
verified_user
=
self
.
create_user
()
verified_user
=
create_user
()
CourseEnrollmentFactory
(
course_id
=
course
.
id
,
user
=
verified_user
,
mode
=
CourseMode
.
VERIFIED
)
course
.
certificate_available_date
=
datetime
.
now
(
utc
)
+
timedelta
(
days
=
7
)
course
.
save
()
...
...
@@ -356,7 +344,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
if the course only has audit mode.
"""
course
=
create_course_run
()
audit_user
=
self
.
create_user
()
audit_user
=
create_user
()
# Enroll learner in the audit mode and verify the course only has 1 mode (audit)
CourseEnrollmentFactory
(
course_id
=
course
.
id
,
user
=
audit_user
,
mode
=
CourseMode
.
AUDIT
)
...
...
@@ -376,9 +364,9 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
@waffle.testutils.override_switch
(
'certificates.auto_certificate_generation'
,
True
)
def
test_certificate_available_date_defined
(
self
):
course
=
create_course_run
()
audit_user
=
self
.
create_user
()
audit_user
=
create_user
()
CourseEnrollmentFactory
(
course_id
=
course
.
id
,
user
=
audit_user
,
mode
=
CourseMode
.
AUDIT
)
verified_user
=
self
.
create_user
()
verified_user
=
create_user
()
CourseEnrollmentFactory
(
course_id
=
course
.
id
,
user
=
verified_user
,
mode
=
CourseMode
.
VERIFIED
)
course
.
certificate_available_date
=
datetime
.
now
(
utc
)
+
timedelta
(
days
=
7
)
self
.
enable_course_certificates
(
course
)
...
...
@@ -391,14 +379,14 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
## VerificationDeadlineDate
def
test_no_verification_deadline
(
self
):
course
=
create_course_run
(
days_till_start
=-
1
,
days_till_verification_deadline
=
None
)
user
=
self
.
create_user
()
user
=
create_user
()
CourseEnrollmentFactory
(
course_id
=
course
.
id
,
user
=
user
,
mode
=
CourseMode
.
VERIFIED
)
block
=
VerificationDeadlineDate
(
course
,
user
)
self
.
assertFalse
(
block
.
is_enabled
)
def
test_no_verified_enrollment
(
self
):
course
=
create_course_run
(
days_till_start
=-
1
)
user
=
self
.
create_user
()
user
=
create_user
()
CourseEnrollmentFactory
(
course_id
=
course
.
id
,
user
=
user
,
mode
=
CourseMode
.
AUDIT
)
block
=
VerificationDeadlineDate
(
course
,
user
)
self
.
assertFalse
(
block
.
is_enabled
)
...
...
@@ -406,7 +394,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
def
test_verification_deadline_date_upcoming
(
self
):
with
freeze_time
(
'2015-01-02'
):
course
=
create_course_run
(
days_till_start
=-
1
)
user
=
self
.
create_user
()
user
=
create_user
()
CourseEnrollmentFactory
(
course_id
=
course
.
id
,
user
=
user
,
mode
=
CourseMode
.
VERIFIED
)
block
=
VerificationDeadlineDate
(
course
,
user
)
...
...
@@ -423,7 +411,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
def
test_verification_deadline_date_retry
(
self
):
with
freeze_time
(
'2015-01-02'
):
course
=
create_course_run
(
days_till_start
=-
1
)
user
=
self
.
create_user
(
verification_status
=
'denied'
)
user
=
create_user
(
verification_status
=
'denied'
)
CourseEnrollmentFactory
(
course_id
=
course
.
id
,
user
=
user
,
mode
=
CourseMode
.
VERIFIED
)
block
=
VerificationDeadlineDate
(
course
,
user
)
...
...
@@ -440,7 +428,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
def
test_verification_deadline_date_denied
(
self
):
with
freeze_time
(
'2015-01-02'
):
course
=
create_course_run
(
days_till_start
=-
10
,
days_till_verification_deadline
=-
1
)
user
=
self
.
create_user
(
verification_status
=
'denied'
)
user
=
create_user
(
verification_status
=
'denied'
)
CourseEnrollmentFactory
(
course_id
=
course
.
id
,
user
=
user
,
mode
=
CourseMode
.
VERIFIED
)
block
=
VerificationDeadlineDate
(
course
,
user
)
...
...
@@ -462,7 +450,7 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
def
test_render_date_string_past
(
self
,
delta
,
expected_date_string
):
with
freeze_time
(
'2015-01-02'
):
course
=
create_course_run
(
days_till_start
=-
10
,
days_till_verification_deadline
=
delta
)
user
=
self
.
create_user
(
verification_status
=
'denied'
)
user
=
create_user
(
verification_status
=
'denied'
)
CourseEnrollmentFactory
(
course_id
=
course
.
id
,
user
=
user
,
mode
=
CourseMode
.
VERIFIED
)
block
=
VerificationDeadlineDate
(
course
,
user
)
...
...
@@ -470,6 +458,97 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase):
@attr
(
shard
=
1
)
@ddt.ddt
class
TestDateAlerts
(
SharedModuleStoreTestCase
):
"""
Unit tests for date alerts.
"""
def
setUp
(
self
):
super
(
TestDateAlerts
,
self
)
.
setUp
()
with
freeze_time
(
'2017-07-01 09:00:00'
):
self
.
course
=
create_course_run
(
days_till_start
=
0
)
self
.
enrollment
=
CourseEnrollmentFactory
(
course_id
=
self
.
course
.
id
,
mode
=
CourseMode
.
AUDIT
)
self
.
request
=
RequestFactory
()
.
request
()
self
.
request
.
session
=
{}
self
.
request
.
user
=
self
.
enrollment
.
user
MessageMiddleware
()
.
process_request
(
self
.
request
)
@ddt.data
(
[
'2017-01-01 09:00:00'
,
u'in 6 months on <span class="date localized-datetime" data-format="shortDate"'
],
[
'2017-06-17 09:00:00'
,
u'in 2 weeks on <span class="date localized-datetime" data-format="shortDate"'
],
[
'2017-06-30 10:00:00'
,
u'in 1 day at <span class="date localized-datetime" data-format="shortTime"'
],
[
'2017-07-01 08:00:00'
,
u'in 1 hour at <span class="date localized-datetime" data-format="shortTime"'
],
[
'2017-07-01 08:55:00'
,
u'in 5 minutes at <span class="date localized-datetime" data-format="shortTime"'
],
[
'2017-07-01 09:00:00'
,
None
],
[
'2017-08-01 09:00:00'
,
None
],
)
@ddt.unpack
def
test_start_date_alert
(
self
,
current_time
,
expected_message_html
):
"""
Verify that course start date alerts are registered.
"""
with
freeze_time
(
current_time
):
block
=
CourseStartDate
(
self
.
course
,
self
.
request
.
user
)
block
.
register_alerts
(
self
.
request
,
self
.
course
)
messages
=
list
(
CourseHomeMessages
.
user_messages
(
self
.
request
))
if
expected_message_html
:
self
.
assertEqual
(
len
(
messages
),
1
)
self
.
assertIn
(
expected_message_html
,
messages
[
0
]
.
message_html
)
else
:
self
.
assertEqual
(
len
(
messages
),
0
)
@ddt.data
(
[
'2017-06-30 09:00:00'
,
None
],
[
'2017-07-01 09:00:00'
,
u'in 2 weeks on <span class="date localized-datetime" data-format="shortDate"'
],
[
'2017-07-14 10:00:00'
,
u'in 1 day at <span class="date localized-datetime" data-format="shortTime"'
],
[
'2017-07-15 08:00:00'
,
u'in 1 hour at <span class="date localized-datetime" data-format="shortTime"'
],
[
'2017-07-15 08:55:00'
,
u'in 5 minutes at <span class="date localized-datetime" data-format="shortTime"'
],
[
'2017-07-15 09:00:00'
,
None
],
[
'2017-08-15 09:00:00'
,
None
],
)
@ddt.unpack
def
test_end_date_alert
(
self
,
current_time
,
expected_message_html
):
"""
Verify that course end date alerts are registered.
"""
with
freeze_time
(
current_time
):
block
=
CourseEndDate
(
self
.
course
,
self
.
request
.
user
)
block
.
register_alerts
(
self
.
request
,
self
.
course
)
messages
=
list
(
CourseHomeMessages
.
user_messages
(
self
.
request
))
if
expected_message_html
:
self
.
assertEqual
(
len
(
messages
),
1
)
self
.
assertIn
(
expected_message_html
,
messages
[
0
]
.
message_html
)
else
:
self
.
assertEqual
(
len
(
messages
),
0
)
@ddt.data
(
[
'2017-06-20 09:00:00'
,
None
],
[
'2017-06-21 09:00:00'
,
u'Don't forget, you have 2 weeks left to upgrade to a Verified Certificate.'
],
[
'2017-07-04 10:00:00'
,
u'Don't forget, you have 1 day left to upgrade to a Verified Certificate.'
],
[
'2017-07-05 08:00:00'
,
u'Don't forget, you have 1 hour left to upgrade to a Verified Certificate.'
],
[
'2017-07-05 08:55:00'
,
u'Don't forget, you have 5 minutes left to upgrade to a Verified Certificate.'
],
[
'2017-07-05 09:00:00'
,
None
],
[
'2017-08-05 09:00:00'
,
None
],
)
@ddt.unpack
@override_waffle_flag
(
UPGRADE_DEADLINE_MESSAGE
,
active
=
True
)
def
test_verified_upgrade_deadline_alert
(
self
,
current_time
,
expected_message_html
):
"""
Verify the verified upgrade deadline alerts.
"""
with
freeze_time
(
current_time
):
block
=
VerifiedUpgradeDeadlineDate
(
self
.
course
,
self
.
request
.
user
)
block
.
register_alerts
(
self
.
request
,
self
.
course
)
messages
=
list
(
CourseHomeMessages
.
user_messages
(
self
.
request
))
if
expected_message_html
:
self
.
assertEqual
(
len
(
messages
),
1
)
self
.
assertIn
(
expected_message_html
,
messages
[
0
]
.
message_html
)
else
:
self
.
assertEqual
(
len
(
messages
),
0
)
@attr
(
shard
=
1
)
class
TestScheduleOverrides
(
SharedModuleStoreTestCase
):
def
setUp
(
self
):
...
...
@@ -560,6 +639,21 @@ class TestScheduleOverrides(SharedModuleStoreTestCase):
self
.
assertEqual
(
block
.
date
,
expected
)
def
create_user
(
verification_status
=
None
):
""" Create a new User instance.
Arguments:
verification_status (str): User's verification status. If this value is set an instance of
SoftwareSecurePhotoVerification will be created for the user with the specified status.
"""
user
=
UserFactory
()
if
verification_status
is
not
None
:
SoftwareSecurePhotoVerificationFactory
.
create
(
user
=
user
,
status
=
verification_status
)
return
user
def
create_course_run
(
days_till_start
=
1
,
days_till_end
=
14
,
days_till_upgrade_deadline
=
4
,
days_till_verification_deadline
=
14
,
):
...
...
lms/djangoapps/courseware/tests/test_views.py
View file @
7dfe12a1
...
...
@@ -213,8 +213,8 @@ class IndexQueryTestCase(ModuleStoreTestCase):
NUM_PROBLEMS
=
20
@ddt.data
(
(
ModuleStoreEnum
.
Type
.
mongo
,
10
,
14
5
),
(
ModuleStoreEnum
.
Type
.
split
,
4
,
14
5
),
(
ModuleStoreEnum
.
Type
.
mongo
,
10
,
14
7
),
(
ModuleStoreEnum
.
Type
.
split
,
4
,
14
7
),
)
@ddt.unpack
def
test_index_query_counts
(
self
,
store_type
,
expected_mongo_query_count
,
expected_mysql_query_count
):
...
...
lms/envs/common.py
View file @
7dfe12a1
...
...
@@ -431,6 +431,9 @@ XQUEUE_WAITTIME_BETWEEN_REQUESTS = 5 # seconds
RETRY_ACTIVATION_EMAIL_MAX_ATTEMPTS
=
5
RETRY_ACTIVATION_EMAIL_TIMEOUT
=
0.5
# Deadline message configurations
COURSE_MESSAGE_ALERT_DURATION_IN_DAYS
=
14
############################# SET PATH INFORMATION #############################
PROJECT_ROOT
=
path
(
__file__
)
.
abspath
()
.
dirname
()
.
dirname
()
# /edx-platform/lms
REPO_ROOT
=
PROJECT_ROOT
.
dirname
()
...
...
@@ -2589,6 +2592,7 @@ MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS = 15 * 60
TIME_ZONE_DISPLAYED_FOR_DEADLINES
=
'UTC'
########################## VIDEO IMAGE STORAGE ############################
VIDEO_IMAGE_SETTINGS
=
dict
(
...
...
lms/static/sass/_build-base-v1.scss
View file @
7dfe12a1
...
...
@@ -7,3 +7,7 @@
@import
'base/variables'
;
@import
'base/mixins'
;
@import
'base/theme'
;
// Pattern Library shims
@import
'edx-pattern-library-shims/base/variables'
;
@import
'edx-pattern-library-shims/buttons'
;
lms/static/sass/_build-lms-v2.scss
View file @
7dfe12a1
...
...
@@ -21,6 +21,7 @@
// Elements
@import
'notifications'
;
@import
'elements/controls'
;
@import
'elements-v2/buttons'
;
@import
'elements-v2/pagination'
;
// Features
...
...
lms/static/sass/course/_info.scss
View file @
7dfe12a1
// Upgrade button
$btn-upgrade-border-color
:
$uxpl-green-base
!
default
;
$btn-upgrade-background
:
$uxpl-green-base
!
default
;
$btn-upgrade-color
:
#fcfcfc
!
default
;
$btn-upgrade-focus-color
:
$btn-upgrade-color
!
default
;
$btn-upgrade-focus-border-color
:
rgb
(
0
,
155
,
0
)
!
default
;
$btn-upgrade-focus-background
:
rgb
(
0
,
155
,
0
)
!
default
;
$btn-upgrade-active-border-color
:
$uxpl-green-base
!
default
;
$btn-upgrade-active-background
:
$uxpl-green-base
!
default
;
//// Notifications
// Upgrade
...
...
@@ -142,31 +132,6 @@ div.info-wrapper {
@include
margin
(
0
,
0
,
0
,
auto
);
padding
:
$baseline
/
2
$baseline
;
}
.btn-upgrade
{
@extend
%btn-shims
;
border-color
:
$btn-upgrade-border-color
;
background
:
$btn-upgrade-background
;
color
:
$btn-upgrade-color
;
// STATE: hover and focus
&
:hover
,
&
.is-hovered
,
&
:focus
,
&
.is-focused
{
border-color
:
$btn-upgrade-focus-border-color
;
background-color
:
$btn-upgrade-focus-background
;
color
:
$btn-upgrade-focus-color
;
}
// STATE: is disabled
&
:disabled
,
&
.is-disabled
{
border-color
:
$btn-disabled-border-color
;
background
:
$btn-brand-disabled-background
;
color
:
$btn-upgrade-color
;
}
}
}
}
...
...
lms/static/sass/elements-v2/_buttons.scss
0 → 100644
View file @
7dfe12a1
// ----------------------------
// #UPGRADE
// ----------------------------
$upgrade-color
:
#009b00
!
default
;
$upgrade-dark-color
:
#008100
!
default
;
.btn-upgrade
{
@extend
%btn
;
border-color
:
$upgrade-color
;
background
:
$upgrade-color
;
color
:
palette
(
primary
,
x-back
);
text-decoration
:
none
;
// STATE: hover and focus
&
:hover
,
&
.is-hovered
,
&
:focus
,
&
.is-focused
{
border-color
:
$upgrade-dark-color
;
background
:
$upgrade-dark-color
;
text-decoration
:
none
;
}
// STATE: is pressed or active
&
:active
,
&
.is-pressed
,
&
.is-active
{
border-color
:
$upgrade-dark-color
;
background
:
$upgrade-dark-color
;
text-decoration
:
none
;
}
// STATE: is disabled
&
:disabled
,
&
.is-disabled
{
border-color
:
$btn-disabled-border-color
;
background
:
$btn-disabled-background-color
;
color
:
$btn-disabled-text-color
;
text-decoration
:
none
;
}
}
lms/static/sass/features/_course-experience.scss
View file @
7dfe12a1
...
...
@@ -16,6 +16,7 @@
.message-content
{
@include
margin
(
0
,
0
,
$baseline
,
$baseline
);
position
:
relative
;
border
:
1px
solid
$lms-border-color
;
padding
:
$baseline
;
...
...
@@ -60,15 +61,17 @@
.message-header
{
font-weight
:
$font-semibold
;
margin-bottom
:
$baseline
/
2
;
width
:
calc
(
100%
-
40px
)
width
:
calc
(
100%
-
40px
)
;
}
a
{
a
:not
(
.btn
)
{
font-weight
:
$font-semibold
;
text-decoration
:
underline
;
}
.dismiss
{
@include
right
(
$baseline
/
4
);
top
:
$baseline
/
4
;
position
:
absolute
;
cursor
:
pointer
;
...
...
@@ -90,6 +93,7 @@
&
.dismissible
{
@include
right
(
$baseline
/
4
);
position
:
absolute
;
top
:
$baseline
/
2
;
font-size
:
font-size
(
small
);
...
...
@@ -103,6 +107,12 @@
}
}
}
.message-actions
{
display
:
flex
;
margin-top
:
$baseline
/
2
;
justify-content
:
flex-end
;
}
}
// Welcome message / Latest Update message
...
...
lms/static/sass/features/_course-sock.scss
View file @
7dfe12a1
...
...
@@ -111,10 +111,6 @@
.action-upgrade-certificate
{
position
:
absolute
;
right
:
$baseline
;
background-color
:
$success-color
;
border-color
:
$success-color
;
background-image
:
none
;
box-shadow
:
none
;
@media
(
max-width
:
960px
)
{
&
{
...
...
@@ -142,11 +138,6 @@
top
:
auto
;
}
}
&
:hover
{
background-color
:
$success-color-hover
;
border-color
:
$success-color-hover
;
}
}
}
}
...
...
lms/static/sass/features/_course-upgrade-message.scss
View file @
7dfe12a1
...
...
@@ -70,13 +70,6 @@ $upgrade-message-background-color: $blue-d1;
color
:
$white
;
}
// Upgrade Button
.btn-upgrade
{
@extend
%btn-primary-green
;
background
:
$uxpl-green-base
;
}
// Cert image
.vc-hero
{
@include
float
(
right
);
...
...
openedx/features/course_experience/__init__.py
View file @
7dfe12a1
...
...
@@ -28,8 +28,12 @@ SHOW_REVIEWS_TOOL_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'show_reviews_t
# Waffle flag to enable the setting of course goals.
ENABLE_COURSE_GOALS
=
CourseWaffleFlag
(
WAFFLE_FLAG_NAMESPACE
,
'enable_course_goals'
)
# Waffle flag to control the display of the hero
SHOW_UPGRADE_MSG_ON_COURSE_HOME
=
CourseWaffleFlag
(
WAFFLE_FLAG_NAMESPACE
,
'show_upgrade_msg_on_course_home'
)
# Waffle flag to control the display of the upgrade deadline message
UPGRADE_DEADLINE_MESSAGE
=
CourseWaffleFlag
(
WAFFLE_FLAG_NAMESPACE
,
'upgrade_deadline_message'
)
# Waffle flag to switch between the 'welcome message' and 'latest update' on the course home page.
# Important Admin Note: This is meant to be configured using waffle_utils course
# override only. Either do not create the actual waffle flag, or be sure to unset the
...
...
openedx/features/course_experience/templates/course_experience/course-home-fragment.html
View file @
7dfe12a1
...
...
@@ -82,7 +82,7 @@ from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG, SHOW_REV
</ul>
<div
class=
"vc-cta vc-fade vc-polite-only"
>
<a
class=
"btn-upgrade"
href=
"${ upgrade_url }"
>
${_("Upgrade ({price})").format(price=
'$' + str(upgrade_price)
)}
</a>
<a
class=
"btn-upgrade"
href=
"${ upgrade_url }"
>
${_("Upgrade ({price})").format(price=
upgrade_price
)}
</a>
</div>
</div>
</div>
...
...
openedx/features/course_experience/templates/course_experience/course-sock-fragment.html
View file @
7dfe12a1
...
...
@@ -55,9 +55,9 @@ from openedx.features.course_experience import DISPLAY_COURSE_SOCK_FLAG
</div>
</div>
<img
class=
"mini-cert"
src=
"${static.url('course_experience/images/verified-cert.png')}"
/>
<a
href=
"
/verify_student/upgrade/${course_id}/
"
>
<button
type=
"button"
class=
"btn btn-
brand
stuck-top focusable action-upgrade-certificate"
>
Upgrade
Now
(${HTML(course_price)})
<a
href=
"
${upgrade_url}
"
>
<button
type=
"button"
class=
"btn btn-
upgrade
stuck-top focusable action-upgrade-certificate"
>
Upgrade (${HTML(course_price)})
</button>
</a>
</div>
...
...
openedx/features/course_experience/tests/views/test_course_home.py
View file @
7dfe12a1
...
...
@@ -173,7 +173,7 @@ class TestCourseHomePage(CourseHomePageTestCase):
course_home_url
(
self
.
course
)
# Fetch the view and verify the query counts
with
self
.
assertNumQueries
(
4
4
,
table_blacklist
=
QUERY_COUNT_TABLE_BLACKLIST
):
with
self
.
assertNumQueries
(
4
5
,
table_blacklist
=
QUERY_COUNT_TABLE_BLACKLIST
):
with
check_mongo_calls
(
4
):
url
=
course_home_url
(
self
.
course
)
self
.
client
.
get
(
url
)
...
...
@@ -477,11 +477,9 @@ class CourseHomeFragmentViewTests(ModuleStoreTestCase):
response
=
self
.
client
.
get
(
self
.
url
)
self
.
assertIn
(
'vc-message'
,
response
.
content
)
url
=
EcommerceService
()
.
get_checkout_page_url
(
self
.
verified_mode
.
sku
)
expected
=
'<a class="btn-upgrade" href="{url}">Upgrade (${price})</a>'
.
format
(
url
=
url
,
price
=
self
.
verified_mode
.
min_price
)
self
.
assertIn
(
expected
,
response
.
content
)
self
.
assertIn
(
'<a class="btn-upgrade"'
,
response
.
content
)
self
.
assertIn
(
url
,
response
.
content
)
self
.
assertIn
(
'Upgrade (${price})</a>'
.
format
(
price
=
self
.
verified_mode
.
min_price
),
response
.
content
)
def
test_no_upgrade_message_if_logged_out
(
self
):
self
.
client
.
logout
()
...
...
openedx/features/course_experience/views/course_home.py
View file @
7dfe12a1
...
...
@@ -10,6 +10,7 @@ from django.views.decorators.cache import cache_control
from
django.views.decorators.csrf
import
ensure_csrf_cookie
from
commerce.utils
import
EcommerceService
from
course_modes.models
import
get_cosmetic_verified_display_price
from
courseware.access
import
has_access
from
courseware.courses
import
(
can_self_enroll_in_course
,
...
...
@@ -165,15 +166,8 @@ class CourseHomeFragmentView(EdxFragmentView):
# TODO Add switch to control deployment
if
SHOW_UPGRADE_MSG_ON_COURSE_HOME
.
is_enabled
(
course_key
)
and
enrollment
and
enrollment
.
upgrade_deadline
:
verified_mode
=
enrollment
.
verified_mode
if
verified_mode
:
upgrade_price
=
verified_mode
.
min_price
ecommerce_service
=
EcommerceService
()
if
ecommerce_service
.
is_enabled
(
request
.
user
):
upgrade_url
=
ecommerce_service
.
get_checkout_page_url
(
verified_mode
.
sku
)
else
:
upgrade_url
=
reverse
(
'verify_student_upgrade_and_verify'
,
args
=
(
course_key
,))
upgrade_url
=
EcommerceService
()
.
upgrade_url
(
request
.
user
,
course_key
)
upgrade_price
=
get_cosmetic_verified_display_price
(
course
)
# Render the course home fragment
context
=
{
...
...
openedx/features/course_experience/views/course_home_messages.py
View file @
7dfe12a1
...
...
@@ -17,7 +17,7 @@ from rest_framework.reverse import reverse
from
web_fragments.fragment
import
Fragment
from
course_modes.models
import
CourseMode
from
courseware.courses
import
get_course_with_access
from
courseware.courses
import
get_course_
date_blocks
,
get_course_
with_access
from
lms.djangoapps.course_goals.api
import
get_course_goal
from
lms.djangoapps.course_goals.models
import
GOAL_KEY_CHOICES
from
openedx.core.djangoapps.plugin_api.views
import
EdxFragmentView
...
...
@@ -64,7 +64,15 @@ class CourseHomeMessageFragmentView(EdxFragmentView):
}
# Register the course home messages to be loaded on the page
_register_course_home_messages
(
request
,
course_id
,
user_access
,
course_start_data
)
_register_course_home_messages
(
request
,
course
,
user_access
,
course_start_data
)
# Register course date alerts
for
course_date_block
in
get_course_date_blocks
(
course
,
request
.
user
):
course_date_block
.
register_alerts
(
request
,
course
)
# Register a course goal message, if appropriate
if
_should_show_course_goal_message
(
request
,
course
,
user_access
):
_register_course_goal_message
(
request
,
course
)
# Grab the relevant messages
course_home_messages
=
list
(
CourseHomeMessages
.
user_messages
(
request
))
...
...
@@ -73,7 +81,7 @@ class CourseHomeMessageFragmentView(EdxFragmentView):
goal_api_url
=
reverse
(
'course_goals_api:v0:course_goal-list'
,
request
=
request
)
# Grab the logo
image_src
=
"course_experience/images/home_message_author.png"
image_src
=
'course_experience/images/home_message_author.png'
context
=
{
'course_home_messages'
:
course_home_messages
,
...
...
@@ -87,24 +95,22 @@ class CourseHomeMessageFragmentView(EdxFragmentView):
return
Fragment
(
html
)
def
_register_course_home_messages
(
request
,
course
_id
,
user_access
,
course_start_data
):
def
_register_course_home_messages
(
request
,
course
,
user_access
,
course_start_data
):
"""
Register messages to be shown in the course home content page.
"""
course_key
=
CourseKey
.
from_string
(
course_id
)
course
=
get_course_with_access
(
request
.
user
,
'load'
,
course_key
)
if
user_access
[
'is_anonymous'
]:
CourseHomeMessages
.
register_info_message
(
request
,
Text
(
_
(
" {sign_in_link} or {register_link} and then enroll in this course."
'{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"
),
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"
),
register_link
=
HTML
(
'<a href="/register?next={current_url}">{register_label}</a>'
)
.
format
(
register_label
=
_
(
'register'
),
current_url
=
urlquote_plus
(
request
.
path
),
)
),
...
...
@@ -114,7 +120,7 @@ def _register_course_home_messages(request, course_id, user_access, course_start
CourseHomeMessages
.
register_info_message
(
request
,
Text
(
_
(
"{open_enroll_link} Enroll now{close_enroll_link} to access the full course."
'{open_enroll_link}Enroll now{close_enroll_link} to access the full course.'
))
.
format
(
open_enroll_link
=
''
,
close_enroll_link
=
''
...
...
@@ -123,26 +129,41 @@ def _register_course_home_messages(request, course_id, user_access, course_start
course_display_name
=
course
.
display_name
)
)
if
user_access
[
'is_enrolled'
]
and
not
course_start_data
[
'already_started'
]:
CourseHomeMessages
.
register_info_message
(
request
,
Text
(
_
(
"Don't forget to add a calendar reminder!"
)),
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'
]
)
)
# Only show the set course goal message for enrolled, unverified
# users that have not yet set a goal in a course that allows for
# verified statuses.
has_verified_mode
=
CourseMode
.
has_verified_mode
(
CourseMode
.
modes_for_course_dict
(
unicode
(
course
.
id
)))
is_already_verified
=
CourseEnrollment
.
is_enrolled_as_verified
(
request
.
user
,
course_key
)
user_goal
=
get_course_goal
(
auth
.
get_user
(
request
),
course_key
)
if
not
request
.
user
.
is_anonymous
()
else
None
if
user_access
[
'is_enrolled'
]
and
has_verified_mode
and
not
is_already_verified
and
not
user_goal
\
and
ENABLE_COURSE_GOALS
.
is_enabled
(
course_key
)
and
settings
.
FEATURES
.
get
(
'ENABLE_COURSE_GOALS'
):
def
_should_show_course_goal_message
(
request
,
course
,
user_access
):
"""
Returns true if the current learner should be shown a course goal message.
"""
course_key
=
course
.
id
# Don't show a message if course goals has not been enabled
if
not
ENABLE_COURSE_GOALS
.
is_enabled
(
course_key
)
or
not
settings
.
FEATURES
.
get
(
'ENABLE_COURSE_GOALS'
):
return
False
# Don't show a message if the user is not enrolled
if
not
user_access
[
'is_enrolled'
]:
return
False
# Don't show a message if the learner has already specified a goal
if
get_course_goal
(
auth
.
get_user
(
request
),
course_key
):
return
False
# Don't show a message if the course does not have a verified mode
if
not
CourseMode
.
has_verified_mode
(
CourseMode
.
modes_for_course_dict
(
unicode
(
course_key
))):
return
False
# Don't show a message if the learner has already verified
if
CourseEnrollment
.
is_enrolled_as_verified
(
request
.
user
,
course_key
):
return
False
return
True
def
_register_course_goal_message
(
request
,
course
):
"""
Register a message to let a learner specify a course goal.
"""
goal_choices_html
=
Text
(
_
(
'To start, set a course goal by selecting the option below that best describes '
'your learning plan. {goal_options_container}'
...
...
@@ -171,7 +192,11 @@ def _register_course_home_messages(request, course_id, user_access, course_start
# Add the option to set a goal to earn a certificate,
# complete the course or explore the course
goal_options
=
[
GOAL_KEY_CHOICES
.
certify
,
GOAL_KEY_CHOICES
.
complete
,
GOAL_KEY_CHOICES
.
explore
]
goal_options
=
[
GOAL_KEY_CHOICES
.
certify
,
GOAL_KEY_CHOICES
.
complete
,
GOAL_KEY_CHOICES
.
explore
]
for
goal_key
in
goal_options
:
goal_text
=
GOAL_KEY_CHOICES
[
goal_key
]
goal_choices_html
+=
HTML
(
...
...
@@ -193,10 +218,7 @@ def _register_course_home_messages(request, course_id, user_access, course_start
CourseHomeMessages
.
register_info_message
(
request
,
HTML
(
'{goal_choices_html}{closing_tag}'
)
.
format
(
goal_choices_html
=
goal_choices_html
,
closing_tag
=
HTML
(
'</div>'
)
),
goal_choices_html
,
title
=
Text
(
_
(
'Welcome to {course_display_name}'
))
.
format
(
course_display_name
=
course
.
display_name
)
...
...
openedx/features/course_experience/views/course_sock.py
View file @
7dfe12a1
...
...
@@ -6,10 +6,11 @@ from django.utils.translation import get_language
from
opaque_keys.edx.keys
import
CourseKey
from
web_fragments.fragment
import
Fragment
from
student.models
import
CourseEnrollment
from
commerce.utils
import
EcommerceService
from
course_modes.models
import
CourseMode
,
get_cosmetic_verified_display_price
from
courseware.date_summary
import
VerifiedUpgradeDeadlineDate
from
openedx.core.djangoapps.plugin_api.views
import
EdxFragmentView
from
student.models
import
CourseEnrollment
class
CourseSockFragmentView
(
EdxFragmentView
):
...
...
@@ -44,13 +45,15 @@ class CourseSockFragmentView(EdxFragmentView):
not
deadline_has_passed
and
get_language
()
==
'en'
)
# Get
the price of the course and format correctly
# Get
information about the upgrade
course_price
=
get_cosmetic_verified_display_price
(
course
)
upgrade_url
=
EcommerceService
()
.
upgrade_url
(
request
.
user
,
course_key
)
context
=
{
'show_course_sock'
:
show_course_sock
,
'course_price'
:
course_price
,
'course_id'
:
course
.
id
'course_id'
:
course
.
id
,
'upgrade_url'
:
upgrade_url
,
}
return
context
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