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
12e1af27
Commit
12e1af27
authored
Oct 13, 2017
by
Calen Pennington
Committed by
GitHub
Oct 13, 2017
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #16092 from edx/upsell-nudges-for-unverified-users
Upsell nudges for unverified users
parents
92318ccc
40d3f4f2
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
21 changed files
with
397 additions
and
150 deletions
+397
-150
common/djangoapps/student/models.py
+37
-19
common/djangoapps/student/tests/test_models.py
+20
-0
lms/djangoapps/ccx/tests/test_field_override_performance.py
+27
-27
lms/djangoapps/ccx/tests/test_views.py
+3
-0
lms/djangoapps/courseware/date_summary.py
+60
-14
lms/djangoapps/courseware/migrations/0004_auto_20171010_1639.py
+19
-0
lms/djangoapps/courseware/models.py
+1
-1
lms/djangoapps/courseware/tests/test_date_summary.py
+0
-12
lms/djangoapps/courseware/tests/test_views.py
+11
-5
lms/djangoapps/courseware/testutils.py
+2
-0
lms/djangoapps/experiments/factories.py
+1
-0
lms/djangoapps/experiments/utils.py
+34
-11
openedx/core/djangoapps/schedules/management/commands/tests/test_send_recurring_nudge.py
+0
-0
openedx/core/djangoapps/schedules/management/commands/tests/test_send_upgrade_reminder.py
+54
-13
openedx/core/djangoapps/schedules/signals.py
+1
-1
openedx/core/djangoapps/schedules/tasks.py
+77
-34
openedx/core/djangoapps/schedules/templates/schedules/edx_ace/recurringnudge_day3/email/body.html
+37
-9
openedx/core/djangoapps/schedules/templates/schedules/edx_ace/recurringnudge_day3/email/body.txt
+9
-0
openedx/core/djangoapps/schedules/templates/schedules/edx_ace/upgradereminder/email/body.html
+2
-2
openedx/core/djangoapps/schedules/templates/schedules/edx_ace/upgradereminder/email/body.txt
+1
-1
openedx/features/course_experience/tests/views/test_course_updates.py
+1
-1
No files found.
common/djangoapps/student/models.py
View file @
12e1af27
...
@@ -1696,16 +1696,13 @@ class CourseEnrollment(models.Model):
...
@@ -1696,16 +1696,13 @@ class CourseEnrollment(models.Model):
def
verified_mode
(
self
):
def
verified_mode
(
self
):
return
CourseMode
.
verified_mode_for_course
(
self
.
course_id
)
return
CourseMode
.
verified_mode_for_course
(
self
.
course_id
)
@property
@
cached_
property
def
upgrade_deadline
(
self
):
def
upgrade_deadline
(
self
):
"""
"""
Returns the upgrade deadline for this enrollment, if it is upgradeable.
Returns the upgrade deadline for this enrollment, if it is upgradeable.
If the seat cannot be upgraded, None is returned.
If the seat cannot be upgraded, None is returned.
Note:
Note:
When loading this model, use `select_related` to retrieve the associated schedule object.
When loading this model, use `select_related` to retrieve the associated schedule object.
Returns:
Returns:
datetime|None
datetime|None
"""
"""
...
@@ -1717,39 +1714,60 @@ class CourseEnrollment(models.Model):
...
@@ -1717,39 +1714,60 @@ class CourseEnrollment(models.Model):
)
)
return
None
return
None
if
self
.
dynamic_upgrade_deadline
is
not
None
:
return
self
.
dynamic_upgrade_deadline
return
self
.
course_upgrade_deadline
@cached_property
def
dynamic_upgrade_deadline
(
self
):
try
:
try
:
schedule_driven_deadlines_enabled
=
(
course_overview
=
self
.
course
DynamicUpgradeDeadlineConfiguration
.
is_enabled
()
except
CourseOverview
.
DoesNotExist
:
or
CourseDynamicUpgradeDeadlineConfiguration
.
is_enabled
(
self
.
course_id
)
course_overview
=
self
.
course_overview
)
if
(
if
not
course_overview
.
self_paced
:
schedule_driven_deadlines_enabled
return
None
and
self
.
course_overview
.
self_paced
and
self
.
schedule
if
not
DynamicUpgradeDeadlineConfiguration
.
is_enabled
():
and
self
.
schedule
.
upgrade_deadline
is
not
None
return
None
):
course_config
=
CourseDynamicUpgradeDeadlineConfiguration
.
current
(
self
.
course_id
)
if
course_config
.
enabled
and
course_config
.
opt_out
:
return
None
try
:
if
not
self
.
schedule
:
return
None
log
.
debug
(
log
.
debug
(
'Schedules: Pulling upgrade deadline for CourseEnrollment
%
d from Schedule
%
d.'
,
'Schedules: Pulling upgrade deadline for CourseEnrollment
%
d from Schedule
%
d.'
,
self
.
id
,
self
.
schedule
.
id
self
.
id
,
self
.
schedule
.
id
)
)
return
self
.
schedule
.
upgrade_deadline
upgrade_deadline
=
self
.
schedule
.
upgrade_deadline
except
ObjectDoesNotExist
:
except
ObjectDoesNotExist
:
# NOTE: Schedule has a one-to-one mapping with CourseEnrollment. If no schedule is associated
# NOTE: Schedule has a one-to-one mapping with CourseEnrollment. If no schedule is associated
# with this enrollment, Django will raise an exception rather than return None.
# with this enrollment, Django will raise an exception rather than return None.
log
.
debug
(
'Schedules: No schedule exists for CourseEnrollment
%
d.'
,
self
.
id
)
log
.
debug
(
'Schedules: No schedule exists for CourseEnrollment
%
d.'
,
self
.
id
)
pass
return
None
if
upgrade_deadline
is
None
or
datetime
.
now
(
UTC
)
>=
upgrade_deadline
:
return
None
return
upgrade_deadline
@cached_property
def
course_upgrade_deadline
(
self
):
try
:
try
:
if
self
.
verified_mode
:
if
self
.
verified_mode
:
log
.
debug
(
'Schedules: Defaulting to verified mode expiration date-time for
%
s.'
,
self
.
course_id
)
log
.
debug
(
'Schedules: Defaulting to verified mode expiration date-time for
%
s.'
,
self
.
course_id
)
return
self
.
verified_mode
.
expiration_datetime
return
self
.
verified_mode
.
expiration_datetime
else
:
else
:
log
.
debug
(
'Schedules: No verified mode located for
%
s.'
,
self
.
course_id
)
log
.
debug
(
'Schedules: No verified mode located for
%
s.'
,
self
.
course_id
)
return
None
except
CourseMode
.
DoesNotExist
:
except
CourseMode
.
DoesNotExist
:
log
.
debug
(
'Schedules:
%
s has no verified mode.'
,
self
.
course_id
)
log
.
debug
(
'Schedules:
%
s has no verified mode.'
,
self
.
course_id
)
pass
log
.
debug
(
'Schedules: Returning default of `None`'
)
return
None
return
None
def
is_verified_enrollment
(
self
):
def
is_verified_enrollment
(
self
):
...
...
common/djangoapps/student/tests/test_models.py
View file @
12e1af27
...
@@ -14,6 +14,7 @@ from django.db.models.functions import Lower
...
@@ -14,6 +14,7 @@ from django.db.models.functions import Lower
from
course_modes.models
import
CourseMode
from
course_modes.models
import
CourseMode
from
course_modes.tests.factories
import
CourseModeFactory
from
course_modes.tests.factories
import
CourseModeFactory
from
courseware.models
import
DynamicUpgradeDeadlineConfiguration
from
courseware.models
import
DynamicUpgradeDeadlineConfiguration
from
openedx.core.djangoapps.content.course_overviews.models
import
CourseOverview
from
openedx.core.djangoapps.schedules.models
import
Schedule
from
openedx.core.djangoapps.schedules.models
import
Schedule
from
openedx.core.djangoapps.schedules.tests.factories
import
ScheduleFactory
from
openedx.core.djangoapps.schedules.tests.factories
import
ScheduleFactory
from
openedx.core.djangolib.testing.utils
import
skip_unless_lms
from
openedx.core.djangolib.testing.utils
import
skip_unless_lms
...
@@ -132,6 +133,25 @@ class CourseEnrollmentTests(SharedModuleStoreTestCase):
...
@@ -132,6 +133,25 @@ class CourseEnrollmentTests(SharedModuleStoreTestCase):
self
.
assertEqual
(
Schedule
.
objects
.
all
()
.
count
(),
0
)
self
.
assertEqual
(
Schedule
.
objects
.
all
()
.
count
(),
0
)
self
.
assertEqual
(
enrollment
.
upgrade_deadline
,
course_mode
.
expiration_datetime
)
self
.
assertEqual
(
enrollment
.
upgrade_deadline
,
course_mode
.
expiration_datetime
)
@skip_unless_lms
# NOTE: We mute the post_save signal to prevent Schedules from being created for new enrollments
@factory.django.mute_signals
(
signals
.
post_save
)
def
test_upgrade_deadline_with_schedule
(
self
):
""" The property should use either the CourseMode or related Schedule to determine the deadline. """
course
=
CourseFactory
(
self_paced
=
True
)
CourseModeFactory
(
course_id
=
course
.
id
,
mode_slug
=
CourseMode
.
VERIFIED
,
# This must be in the future to ensure it is returned by downstream code.
expiration_datetime
=
datetime
.
datetime
.
now
(
pytz
.
UTC
)
+
datetime
.
timedelta
(
days
=
30
),
)
course_overview
=
CourseOverview
.
load_from_module_store
(
course
.
id
)
enrollment
=
CourseEnrollmentFactory
(
course_id
=
course
.
id
,
mode
=
CourseMode
.
AUDIT
,
course
=
course_overview
,
)
# The schedule's upgrade deadline should be used if a schedule exists
# The schedule's upgrade deadline should be used if a schedule exists
DynamicUpgradeDeadlineConfiguration
.
objects
.
create
(
enabled
=
True
)
DynamicUpgradeDeadlineConfiguration
.
objects
.
create
(
enabled
=
True
)
schedule
=
ScheduleFactory
(
enrollment
=
enrollment
)
schedule
=
ScheduleFactory
(
enrollment
=
enrollment
)
...
...
lms/djangoapps/ccx/tests/test_field_override_performance.py
View file @
12e1af27
...
@@ -237,18 +237,18 @@ class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase):
...
@@ -237,18 +237,18 @@ class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase):
# # of sql queries to default,
# # of sql queries to default,
# # of mongo queries,
# # of mongo queries,
# )
# )
(
'no_overrides'
,
1
,
True
,
False
):
(
1
9
,
1
),
(
'no_overrides'
,
1
,
True
,
False
):
(
1
6
,
1
),
(
'no_overrides'
,
2
,
True
,
False
):
(
1
9
,
1
),
(
'no_overrides'
,
2
,
True
,
False
):
(
1
6
,
1
),
(
'no_overrides'
,
3
,
True
,
False
):
(
1
9
,
1
),
(
'no_overrides'
,
3
,
True
,
False
):
(
1
6
,
1
),
(
'ccx'
,
1
,
True
,
False
):
(
1
9
,
1
),
(
'ccx'
,
1
,
True
,
False
):
(
1
6
,
1
),
(
'ccx'
,
2
,
True
,
False
):
(
1
9
,
1
),
(
'ccx'
,
2
,
True
,
False
):
(
1
6
,
1
),
(
'ccx'
,
3
,
True
,
False
):
(
1
9
,
1
),
(
'ccx'
,
3
,
True
,
False
):
(
1
6
,
1
),
(
'no_overrides'
,
1
,
False
,
False
):
(
1
9
,
1
),
(
'no_overrides'
,
1
,
False
,
False
):
(
1
6
,
1
),
(
'no_overrides'
,
2
,
False
,
False
):
(
1
9
,
1
),
(
'no_overrides'
,
2
,
False
,
False
):
(
1
6
,
1
),
(
'no_overrides'
,
3
,
False
,
False
):
(
1
9
,
1
),
(
'no_overrides'
,
3
,
False
,
False
):
(
1
6
,
1
),
(
'ccx'
,
1
,
False
,
False
):
(
1
9
,
1
),
(
'ccx'
,
1
,
False
,
False
):
(
1
6
,
1
),
(
'ccx'
,
2
,
False
,
False
):
(
1
9
,
1
),
(
'ccx'
,
2
,
False
,
False
):
(
1
6
,
1
),
(
'ccx'
,
3
,
False
,
False
):
(
1
9
,
1
),
(
'ccx'
,
3
,
False
,
False
):
(
1
6
,
1
),
}
}
...
@@ -260,19 +260,19 @@ class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase):
...
@@ -260,19 +260,19 @@ class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase):
__test__
=
True
__test__
=
True
TEST_DATA
=
{
TEST_DATA
=
{
(
'no_overrides'
,
1
,
True
,
False
):
(
1
9
,
3
),
(
'no_overrides'
,
1
,
True
,
False
):
(
1
6
,
3
),
(
'no_overrides'
,
2
,
True
,
False
):
(
1
9
,
3
),
(
'no_overrides'
,
2
,
True
,
False
):
(
1
6
,
3
),
(
'no_overrides'
,
3
,
True
,
False
):
(
1
9
,
3
),
(
'no_overrides'
,
3
,
True
,
False
):
(
1
6
,
3
),
(
'ccx'
,
1
,
True
,
False
):
(
1
9
,
3
),
(
'ccx'
,
1
,
True
,
False
):
(
1
6
,
3
),
(
'ccx'
,
2
,
True
,
False
):
(
1
9
,
3
),
(
'ccx'
,
2
,
True
,
False
):
(
1
6
,
3
),
(
'ccx'
,
3
,
True
,
False
):
(
1
9
,
3
),
(
'ccx'
,
3
,
True
,
False
):
(
1
6
,
3
),
(
'ccx'
,
1
,
True
,
True
):
(
20
,
3
),
(
'ccx'
,
1
,
True
,
True
):
(
17
,
3
),
(
'ccx'
,
2
,
True
,
True
):
(
20
,
3
),
(
'ccx'
,
2
,
True
,
True
):
(
17
,
3
),
(
'ccx'
,
3
,
True
,
True
):
(
20
,
3
),
(
'ccx'
,
3
,
True
,
True
):
(
17
,
3
),
(
'no_overrides'
,
1
,
False
,
False
):
(
1
9
,
3
),
(
'no_overrides'
,
1
,
False
,
False
):
(
1
6
,
3
),
(
'no_overrides'
,
2
,
False
,
False
):
(
1
9
,
3
),
(
'no_overrides'
,
2
,
False
,
False
):
(
1
6
,
3
),
(
'no_overrides'
,
3
,
False
,
False
):
(
1
9
,
3
),
(
'no_overrides'
,
3
,
False
,
False
):
(
1
6
,
3
),
(
'ccx'
,
1
,
False
,
False
):
(
1
9
,
3
),
(
'ccx'
,
1
,
False
,
False
):
(
1
6
,
3
),
(
'ccx'
,
2
,
False
,
False
):
(
1
9
,
3
),
(
'ccx'
,
2
,
False
,
False
):
(
1
6
,
3
),
(
'ccx'
,
3
,
False
,
False
):
(
1
9
,
3
),
(
'ccx'
,
3
,
False
,
False
):
(
1
6
,
3
),
}
}
lms/djangoapps/ccx/tests/test_views.py
View file @
12e1af27
...
@@ -36,6 +36,7 @@ from lms.djangoapps.ccx.utils import ccx_course, is_email
...
@@ -36,6 +36,7 @@ from lms.djangoapps.ccx.utils import ccx_course, is_email
from
lms.djangoapps.ccx.views
import
get_date
from
lms.djangoapps.ccx.views
import
get_date
from
lms.djangoapps.grades.tasks
import
compute_all_grades_for_course
from
lms.djangoapps.grades.tasks
import
compute_all_grades_for_course
from
lms.djangoapps.instructor.access
import
allow_access
,
list_with_level
from
lms.djangoapps.instructor.access
import
allow_access
,
list_with_level
from
openedx.core.djangoapps.content.course_overviews.models
import
CourseOverview
from
request_cache.middleware
import
RequestCache
from
request_cache.middleware
import
RequestCache
from
student.models
import
CourseEnrollment
,
CourseEnrollmentAllowed
from
student.models
import
CourseEnrollment
,
CourseEnrollmentAllowed
from
student.roles
import
CourseCcxCoachRole
,
CourseInstructorRole
,
CourseStaffRole
from
student.roles
import
CourseCcxCoachRole
,
CourseInstructorRole
,
CourseStaffRole
...
@@ -1061,6 +1062,7 @@ class TestCCXGrades(FieldOverrideTestMixin, SharedModuleStoreTestCase, LoginEnro
...
@@ -1061,6 +1062,7 @@ class TestCCXGrades(FieldOverrideTestMixin, SharedModuleStoreTestCase, LoginEnro
def
setUpClass
(
cls
):
def
setUpClass
(
cls
):
super
(
TestCCXGrades
,
cls
)
.
setUpClass
()
super
(
TestCCXGrades
,
cls
)
.
setUpClass
()
cls
.
_course
=
course
=
CourseFactory
.
create
(
enable_ccx
=
True
)
cls
.
_course
=
course
=
CourseFactory
.
create
(
enable_ccx
=
True
)
CourseOverview
.
load_from_module_store
(
course
.
id
)
# Create a course outline
# Create a course outline
cls
.
mooc_start
=
start
=
datetime
.
datetime
(
cls
.
mooc_start
=
start
=
datetime
.
datetime
(
...
@@ -1122,6 +1124,7 @@ class TestCCXGrades(FieldOverrideTestMixin, SharedModuleStoreTestCase, LoginEnro
...
@@ -1122,6 +1124,7 @@ class TestCCXGrades(FieldOverrideTestMixin, SharedModuleStoreTestCase, LoginEnro
# which emulates how a student would get access.
# which emulates how a student would get access.
self
.
ccx_key
=
CCXLocator
.
from_course_locator
(
self
.
_course
.
id
,
unicode
(
ccx
.
id
))
self
.
ccx_key
=
CCXLocator
.
from_course_locator
(
self
.
_course
.
id
,
unicode
(
ccx
.
id
))
self
.
course
=
get_course_by_id
(
self
.
ccx_key
,
depth
=
None
)
self
.
course
=
get_course_by_id
(
self
.
ccx_key
,
depth
=
None
)
CourseOverview
.
load_from_module_store
(
self
.
course
.
id
)
setup_students_and_grades
(
self
)
setup_students_and_grades
(
self
)
self
.
client
.
login
(
username
=
coach
.
username
,
password
=
"test"
)
self
.
client
.
login
(
username
=
coach
.
username
,
password
=
"test"
)
self
.
addCleanup
(
RequestCache
.
clear_request_cache
)
self
.
addCleanup
(
RequestCache
.
clear_request_cache
)
...
...
lms/djangoapps/courseware/date_summary.py
View file @
12e1af27
...
@@ -20,6 +20,7 @@ from course_modes.models import CourseMode, get_cosmetic_verified_display_price
...
@@ -20,6 +20,7 @@ from course_modes.models import CourseMode, get_cosmetic_verified_display_price
from
lms.djangoapps.commerce.utils
import
EcommerceService
from
lms.djangoapps.commerce.utils
import
EcommerceService
from
lms.djangoapps.verify_student.models
import
SoftwareSecurePhotoVerification
,
VerificationDeadline
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.djangoapps.certificates.api
import
can_show_certificate_available_date_field
from
openedx.core.djangoapps.content.course_overviews.models
import
CourseOverview
from
openedx.core.djangolib.markup
import
HTML
,
Text
from
openedx.core.djangolib.markup
import
HTML
,
Text
from
openedx.features.course_experience
import
CourseHomeMessages
,
UPGRADE_DEADLINE_MESSAGE
from
openedx.features.course_experience
import
CourseHomeMessages
,
UPGRADE_DEADLINE_MESSAGE
from
student.models
import
CourseEnrollment
from
student.models
import
CourseEnrollment
...
@@ -380,6 +381,63 @@ class CertificateAvailableDate(DateSummary):
...
@@ -380,6 +381,63 @@ class CertificateAvailableDate(DateSummary):
)
)
def
verified_upgrade_deadline_link
(
user
,
course
=
None
,
course_id
=
None
):
"""
Format the correct verified upgrade link for the specified ``user``
in a course.
One of ``course`` or ``course_id`` must be supplied. If both are specified,
``course`` will take priority.
Arguments:
user (:class:`~django.contrib.auth.models.User`): The user to display
the link for.
course (:class:`.CourseOverview`): The course to render a link for.
course_id (:class:`.CourseKey`): The course_id of the course to render for.
Returns:
The formatted link that will allow the user to upgrade to verified
in this course.
"""
if
course
is
not
None
:
course_id
=
course
.
id
ecommerce_service
=
EcommerceService
()
if
ecommerce_service
.
is_enabled
(
user
):
if
course
is
not
None
and
isinstance
(
course
,
CourseOverview
):
course_mode
=
course
.
modes
.
get
(
mode_slug
=
CourseMode
.
VERIFIED
)
else
:
course_mode
=
CourseMode
.
objects
.
get
(
course_id
=
course_id
,
mode_slug
=
CourseMode
.
VERIFIED
)
return
ecommerce_service
.
get_checkout_page_url
(
course_mode
.
sku
)
return
reverse
(
'verify_student_upgrade_and_verify'
,
args
=
(
course_id
,))
def
verified_upgrade_link_is_valid
(
enrollment
=
None
):
"""
Return whether this enrollment can be upgraded.
Arguments:
enrollment (:class:`.CourseEnrollment`): The enrollment under consideration.
If None, then the enrollment is considered to be upgradeable.
"""
# Return `true` if user is not enrolled in course
if
enrollment
is
None
:
return
False
upgrade_deadline
=
enrollment
.
upgrade_deadline
if
upgrade_deadline
is
None
:
return
False
if
datetime
.
datetime
.
now
(
utc
)
.
date
()
>
upgrade_deadline
.
date
():
return
False
# Show the summary if user enrollment is in which allow user to upsell
return
enrollment
.
is_active
and
enrollment
.
mode
in
CourseMode
.
UPSELL_TO_VERIFIED_MODES
class
VerifiedUpgradeDeadlineDate
(
DateSummary
):
class
VerifiedUpgradeDeadlineDate
(
DateSummary
):
"""
"""
Displays the date before which learners must upgrade to the
Displays the date before which learners must upgrade to the
...
@@ -395,7 +453,7 @@ class VerifiedUpgradeDeadlineDate(DateSummary):
...
@@ -395,7 +453,7 @@ class VerifiedUpgradeDeadlineDate(DateSummary):
@property
@property
def
link
(
self
):
def
link
(
self
):
return
EcommerceService
()
.
upgrade_url
(
self
.
user
,
self
.
course_id
)
return
verified_upgrade_deadline_link
(
self
.
user
,
self
.
course
,
self
.
course_id
)
@cached_property
@cached_property
def
enrollment
(
self
):
def
enrollment
(
self
):
...
@@ -413,19 +471,7 @@ class VerifiedUpgradeDeadlineDate(DateSummary):
...
@@ -413,19 +471,7 @@ class VerifiedUpgradeDeadlineDate(DateSummary):
if
not
is_enabled
:
if
not
is_enabled
:
return
False
return
False
enrollment_mode
=
None
return
verified_upgrade_link_is_valid
(
self
.
enrollment
)
is_active
=
None
if
self
.
enrollment
:
enrollment_mode
=
self
.
enrollment
.
mode
is_active
=
self
.
enrollment
.
is_active
# Return `true` if user is not enrolled in course
if
enrollment_mode
is
None
and
is_active
is
None
:
return
True
# Show the summary if user enrollment is in which allow user to upsell
return
is_active
and
enrollment_mode
in
CourseMode
.
UPSELL_TO_VERIFIED_MODES
@lazy
@lazy
def
date
(
self
):
def
date
(
self
):
...
...
lms/djangoapps/courseware/migrations/0004_auto_20171010_1639.py
0 → 100644
View file @
12e1af27
# -*- coding: utf-8 -*-
from
__future__
import
unicode_literals
from
django.db
import
migrations
,
models
class
Migration
(
migrations
.
Migration
):
dependencies
=
[
(
'courseware'
,
'0003_auto_20170825_0935'
),
]
operations
=
[
migrations
.
AlterField
(
model_name
=
'coursedynamicupgradedeadlineconfiguration'
,
name
=
'opt_out'
,
field
=
models
.
BooleanField
(
default
=
False
,
help_text
=
'Disable the dynamic upgrade deadline for this course run.'
),
),
]
lms/djangoapps/courseware/models.py
View file @
12e1af27
...
@@ -398,5 +398,5 @@ class CourseDynamicUpgradeDeadlineConfiguration(ConfigurationModel):
...
@@ -398,5 +398,5 @@ class CourseDynamicUpgradeDeadlineConfiguration(ConfigurationModel):
)
)
opt_out
=
models
.
BooleanField
(
opt_out
=
models
.
BooleanField
(
default
=
False
,
default
=
False
,
help_text
=
_
(
'
This does not do anything and is no longer used. Setting enabled=False has the same effect
.'
)
help_text
=
_
(
'
Disable the dynamic upgrade deadline for this course run
.'
)
)
)
lms/djangoapps/courseware/tests/test_date_summary.py
View file @
12e1af27
...
@@ -618,18 +618,6 @@ class TestScheduleOverrides(SharedModuleStoreTestCase):
...
@@ -618,18 +618,6 @@ class TestScheduleOverrides(SharedModuleStoreTestCase):
self
.
assertEqual
(
block
.
date
,
expected
)
self
.
assertEqual
(
block
.
date
,
expected
)
@override_waffle_flag
(
CREATE_SCHEDULE_WAFFLE_FLAG
,
True
)
@override_waffle_flag
(
CREATE_SCHEDULE_WAFFLE_FLAG
,
True
)
def
test_date_with_self_paced_with_single_course
(
self
):
""" If the global switch is off, a single course can still be enabled. """
course
=
create_self_paced_course_run
(
days_till_start
=-
1
)
DynamicUpgradeDeadlineConfiguration
.
objects
.
create
(
enabled
=
False
)
course_config
=
CourseDynamicUpgradeDeadlineConfiguration
.
objects
.
create
(
enabled
=
True
,
course_id
=
course
.
id
)
enrollment
=
CourseEnrollmentFactory
(
course_id
=
course
.
id
,
mode
=
CourseMode
.
AUDIT
)
block
=
VerifiedUpgradeDeadlineDate
(
course
,
enrollment
.
user
)
expected
=
enrollment
.
created
+
timedelta
(
days
=
course_config
.
deadline_days
)
self
.
assertEqual
(
block
.
date
,
expected
)
@override_waffle_flag
(
CREATE_SCHEDULE_WAFFLE_FLAG
,
True
)
def
test_date_with_existing_schedule
(
self
):
def
test_date_with_existing_schedule
(
self
):
""" If a schedule is created while deadlines are disabled, they shouldn't magically appear once the feature is
""" If a schedule is created while deadlines are disabled, they shouldn't magically appear once the feature is
turned on. """
turned on. """
...
...
lms/djangoapps/courseware/tests/test_views.py
View file @
12e1af27
...
@@ -213,8 +213,8 @@ class IndexQueryTestCase(ModuleStoreTestCase):
...
@@ -213,8 +213,8 @@ class IndexQueryTestCase(ModuleStoreTestCase):
NUM_PROBLEMS
=
20
NUM_PROBLEMS
=
20
@ddt.data
(
@ddt.data
(
(
ModuleStoreEnum
.
Type
.
mongo
,
10
,
14
7
),
(
ModuleStoreEnum
.
Type
.
mongo
,
10
,
14
5
),
(
ModuleStoreEnum
.
Type
.
split
,
4
,
14
7
),
(
ModuleStoreEnum
.
Type
.
split
,
4
,
14
5
),
)
)
@ddt.unpack
@ddt.unpack
def
test_index_query_counts
(
self
,
store_type
,
expected_mongo_query_count
,
expected_mysql_query_count
):
def
test_index_query_counts
(
self
,
store_type
,
expected_mongo_query_count
,
expected_mysql_query_count
):
...
@@ -1047,6 +1047,7 @@ class BaseDueDateTests(ModuleStoreTestCase):
...
@@ -1047,6 +1047,7 @@ class BaseDueDateTests(ModuleStoreTestCase):
course
=
modulestore
()
.
get_course
(
course
.
id
)
course
=
modulestore
()
.
get_course
(
course
.
id
)
self
.
assertIsNotNone
(
course
.
get_children
()[
0
]
.
get_children
()[
0
]
.
due
)
self
.
assertIsNotNone
(
course
.
get_children
()[
0
]
.
get_children
()[
0
]
.
due
)
CourseEnrollmentFactory
(
user
=
self
.
user
,
course_id
=
course
.
id
)
CourseEnrollmentFactory
(
user
=
self
.
user
,
course_id
=
course
.
id
)
CourseOverview
.
load_from_module_store
(
course
.
id
)
return
course
return
course
def
setUp
(
self
):
def
setUp
(
self
):
...
@@ -1456,13 +1457,13 @@ class ProgressPageTests(ProgressPageBaseTests):
...
@@ -1456,13 +1457,13 @@ class ProgressPageTests(ProgressPageBaseTests):
"""Test that query counts remain the same for self-paced and instructor-paced courses."""
"""Test that query counts remain the same for self-paced and instructor-paced courses."""
SelfPacedConfiguration
(
enabled
=
self_paced_enabled
)
.
save
()
SelfPacedConfiguration
(
enabled
=
self_paced_enabled
)
.
save
()
self
.
setup_course
(
self_paced
=
self_paced
)
self
.
setup_course
(
self_paced
=
self_paced
)
with
self
.
assertNumQueries
(
3
6
,
table_blacklist
=
QUERY_COUNT_TABLE_BLACKLIST
),
check_mongo_calls
(
1
):
with
self
.
assertNumQueries
(
3
4
if
self_paced
else
33
,
table_blacklist
=
QUERY_COUNT_TABLE_BLACKLIST
),
check_mongo_calls
(
1
):
self
.
_get_progress_page
()
self
.
_get_progress_page
()
@patch.dict
(
settings
.
FEATURES
,
{
'ASSUME_ZERO_GRADE_IF_ABSENT_FOR_ALL_TESTS'
:
False
})
@patch.dict
(
settings
.
FEATURES
,
{
'ASSUME_ZERO_GRADE_IF_ABSENT_FOR_ALL_TESTS'
:
False
})
@ddt.data
(
@ddt.data
(
(
False
,
4
3
,
27
),
(
False
,
4
0
,
26
),
(
True
,
3
6
,
23
)
(
True
,
3
3
,
22
)
)
)
@ddt.unpack
@ddt.unpack
def
test_progress_queries
(
self
,
enable_waffle
,
initial
,
subsequent
):
def
test_progress_queries
(
self
,
enable_waffle
,
initial
,
subsequent
):
...
@@ -2213,6 +2214,7 @@ class TestIndexView(ModuleStoreTestCase):
...
@@ -2213,6 +2214,7 @@ class TestIndexView(ModuleStoreTestCase):
state
=
json
.
dumps
({
'state'
:
unicode
(
item
.
scope_ids
.
usage_id
)})
state
=
json
.
dumps
({
'state'
:
unicode
(
item
.
scope_ids
.
usage_id
)})
)
)
CourseOverview
.
load_from_module_store
(
course
.
id
)
CourseEnrollmentFactory
(
user
=
user
,
course_id
=
course
.
id
)
CourseEnrollmentFactory
(
user
=
user
,
course_id
=
course
.
id
)
self
.
assertTrue
(
self
.
client
.
login
(
username
=
user
.
username
,
password
=
'test'
))
self
.
assertTrue
(
self
.
client
.
login
(
username
=
user
.
username
,
password
=
'test'
))
...
@@ -2241,6 +2243,7 @@ class TestIndexView(ModuleStoreTestCase):
...
@@ -2241,6 +2243,7 @@ class TestIndexView(ModuleStoreTestCase):
vertical
=
ItemFactory
.
create
(
parent
=
section
,
category
=
'vertical'
,
display_name
=
"Vertical"
)
vertical
=
ItemFactory
.
create
(
parent
=
section
,
category
=
'vertical'
,
display_name
=
"Vertical"
)
ItemFactory
.
create
(
parent
=
vertical
,
category
=
'id_checker'
,
display_name
=
"ID Checker"
)
ItemFactory
.
create
(
parent
=
vertical
,
category
=
'id_checker'
,
display_name
=
"ID Checker"
)
CourseOverview
.
load_from_module_store
(
course
.
id
)
CourseEnrollmentFactory
(
user
=
user
,
course_id
=
course
.
id
)
CourseEnrollmentFactory
(
user
=
user
,
course_id
=
course
.
id
)
self
.
assertTrue
(
self
.
client
.
login
(
username
=
user
.
username
,
password
=
'test'
))
self
.
assertTrue
(
self
.
client
.
login
(
username
=
user
.
username
,
password
=
'test'
))
...
@@ -2281,6 +2284,8 @@ class TestIndexViewWithVerticalPositions(ModuleStoreTestCase):
...
@@ -2281,6 +2284,8 @@ class TestIndexViewWithVerticalPositions(ModuleStoreTestCase):
ItemFactory
.
create
(
parent
=
self
.
section
,
category
=
'vertical'
,
display_name
=
"Vertical2"
)
ItemFactory
.
create
(
parent
=
self
.
section
,
category
=
'vertical'
,
display_name
=
"Vertical2"
)
ItemFactory
.
create
(
parent
=
self
.
section
,
category
=
'vertical'
,
display_name
=
"Vertical3"
)
ItemFactory
.
create
(
parent
=
self
.
section
,
category
=
'vertical'
,
display_name
=
"Vertical3"
)
CourseOverview
.
load_from_module_store
(
self
.
course
.
id
)
self
.
client
.
login
(
username
=
self
.
user
,
password
=
'test'
)
self
.
client
.
login
(
username
=
self
.
user
,
password
=
'test'
)
CourseEnrollmentFactory
(
user
=
self
.
user
,
course_id
=
self
.
course
.
id
)
CourseEnrollmentFactory
(
user
=
self
.
user
,
course_id
=
self
.
course
.
id
)
...
@@ -2505,6 +2510,7 @@ class EnterpriseConsentTestCase(EnterpriseTestConsentRequired, ModuleStoreTestCa
...
@@ -2505,6 +2510,7 @@ class EnterpriseConsentTestCase(EnterpriseTestConsentRequired, ModuleStoreTestCa
self
.
user
=
UserFactory
.
create
()
self
.
user
=
UserFactory
.
create
()
self
.
assertTrue
(
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
'test'
))
self
.
assertTrue
(
self
.
client
.
login
(
username
=
self
.
user
.
username
,
password
=
'test'
))
self
.
course
=
CourseFactory
.
create
()
self
.
course
=
CourseFactory
.
create
()
CourseOverview
.
load_from_module_store
(
self
.
course
.
id
)
CourseEnrollmentFactory
(
user
=
self
.
user
,
course_id
=
self
.
course
.
id
)
CourseEnrollmentFactory
(
user
=
self
.
user
,
course_id
=
self
.
course
.
id
)
def
test_consent_required
(
self
):
def
test_consent_required
(
self
):
...
...
lms/djangoapps/courseware/testutils.py
View file @
12e1af27
...
@@ -12,6 +12,7 @@ from mock import patch
...
@@ -12,6 +12,7 @@ from mock import patch
from
lms.djangoapps.courseware.field_overrides
import
OverrideModulestoreFieldData
from
lms.djangoapps.courseware.field_overrides
import
OverrideModulestoreFieldData
from
lms.djangoapps.courseware.url_helpers
import
get_redirect_url
from
lms.djangoapps.courseware.url_helpers
import
get_redirect_url
from
openedx.core.djangoapps.content.course_overviews.models
import
CourseOverview
from
student.tests.factories
import
AdminFactory
,
CourseEnrollmentFactory
,
UserFactory
from
student.tests.factories
import
AdminFactory
,
CourseEnrollmentFactory
,
UserFactory
from
xmodule.modulestore
import
ModuleStoreEnum
from
xmodule.modulestore
import
ModuleStoreEnum
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.django
import
modulestore
...
@@ -104,6 +105,7 @@ class RenderXBlockTestMixin(object):
...
@@ -104,6 +105,7 @@ class RenderXBlockTestMixin(object):
category
=
'html'
,
category
=
'html'
,
data
=
"<p>Test HTML Content<p>"
data
=
"<p>Test HTML Content<p>"
)
)
CourseOverview
.
load_from_module_store
(
self
.
course
.
id
)
# block_name_to_be_tested can be `html_block` or `vertical_block`.
# block_name_to_be_tested can be `html_block` or `vertical_block`.
# These attributes help ensure the positive and negative tests are in sync.
# These attributes help ensure the positive and negative tests are in sync.
...
...
lms/djangoapps/experiments/factories.py
View file @
12e1af27
import
factory
import
factory
import
factory.fuzzy
from
experiments.models
import
ExperimentData
,
ExperimentKeyValue
from
experiments.models
import
ExperimentData
,
ExperimentKeyValue
from
student.tests.factories
import
UserFactory
from
student.tests.factories
import
UserFactory
...
...
lms/djangoapps/experiments/utils.py
View file @
12e1af27
...
@@ -3,21 +3,41 @@ from course_modes.models import (
...
@@ -3,21 +3,41 @@ from course_modes.models import (
get_cosmetic_verified_display_price
get_cosmetic_verified_display_price
)
)
from
courseware.date_summary
import
(
from
courseware.date_summary
import
(
VerifiedUpgradeDeadlineDate
verified_upgrade_deadline_link
,
verified_upgrade_link_is_valid
)
)
def
check_and_get_upgrade_link
(
user
,
course_id
):
def
check_and_get_upgrade_link
_and_date
(
user
,
enrollment
=
None
,
course
=
None
):
"""
"""
For an authenticated user, return a link to allow them to upgrade
For an authenticated user, return a link to allow them to upgrade
in the specified course.
in the specified course.
"""
"""
if
user
.
is_authenticated
():
if
enrollment
is
None
and
course
is
None
:
upgrade_data
=
VerifiedUpgradeDeadlineDate
(
None
,
user
,
course_id
=
course_id
)
raise
ValueError
(
"Must specify either an enrollment or a course"
)
if
upgrade_data
.
is_enabled
:
return
upgrade_data
return
None
if
enrollment
:
if
course
is
None
:
course
=
enrollment
.
course
elif
enrollment
.
course_id
!=
course
.
id
:
raise
ValueError
(
"{} refers to a different course than {} which was supplied"
.
format
(
enrollment
,
course
))
if
enrollment
.
user_id
!=
user
.
id
:
raise
ValueError
(
"{} refers to a different user than {} which was supplied"
.
format
(
enrollment
,
user
))
if
enrollment
is
None
:
enrollment
=
CourseEnrollment
.
get_enrollment
(
user
,
course
.
id
)
if
user
.
is_authenticated
()
and
verified_upgrade_link_is_valid
(
enrollment
):
return
(
verified_upgrade_deadline_link
(
user
,
course
),
enrollment
.
upgrade_deadline
)
return
(
None
,
None
)
def
get_experiment_user_metadata_context
(
course
,
user
):
def
get_experiment_user_metadata_context
(
course
,
user
):
...
@@ -26,23 +46,26 @@ def get_experiment_user_metadata_context(course, user):
...
@@ -26,23 +46,26 @@ def get_experiment_user_metadata_context(course, user):
"""
"""
enrollment_mode
=
None
enrollment_mode
=
None
enrollment_time
=
None
enrollment_time
=
None
enrollment
=
None
try
:
try
:
enrollment
=
CourseEnrollment
.
objects
.
get
(
user_id
=
user
.
id
,
course_id
=
course
.
id
)
enrollment
=
CourseEnrollment
.
objects
.
select_related
(
'course'
)
.
get
(
user_id
=
user
.
id
,
course_id
=
course
.
id
)
if
enrollment
.
is_active
:
if
enrollment
.
is_active
:
enrollment_mode
=
enrollment
.
mode
enrollment_mode
=
enrollment
.
mode
enrollment_time
=
enrollment
.
created
enrollment_time
=
enrollment
.
created
except
CourseEnrollment
.
DoesNotExist
:
except
CourseEnrollment
.
DoesNotExist
:
pass
# Not enrolled, used the default None values
pass
# Not enrolled, used the default None values
upgrade_
data
=
check_and_get_upgrade_link
(
user
,
course
.
id
)
upgrade_
link
,
upgrade_date
=
check_and_get_upgrade_link_and_date
(
user
,
enrollment
,
course
)
return
{
return
{
'upgrade_link'
:
upgrade_
data
and
upgrade_data
.
link
,
'upgrade_link'
:
upgrade_link
,
'upgrade_price'
:
unicode
(
get_cosmetic_verified_display_price
(
course
)),
'upgrade_price'
:
unicode
(
get_cosmetic_verified_display_price
(
course
)),
'enrollment_mode'
:
enrollment_mode
,
'enrollment_mode'
:
enrollment_mode
,
'enrollment_time'
:
enrollment_time
,
'enrollment_time'
:
enrollment_time
,
'pacing_type'
:
'self_paced'
if
course
.
self_paced
else
'instructor_paced'
,
'pacing_type'
:
'self_paced'
if
course
.
self_paced
else
'instructor_paced'
,
'upgrade_deadline'
:
upgrade_dat
a
and
upgrade_data
.
dat
e
,
'upgrade_deadline'
:
upgrade_date
,
'course_key'
:
course
.
id
,
'course_key'
:
course
.
id
,
'course_start'
:
course
.
start
,
'course_start'
:
course
.
start
,
'course_end'
:
course
.
end
,
'course_end'
:
course
.
end
,
...
...
openedx/core/djangoapps/schedules/management/commands/tests/test_send_recurring_nudge.py
View file @
12e1af27
This diff is collapsed.
Click to expand it.
openedx/core/djangoapps/schedules/management/commands/tests/test_send_upgrade_reminder.py
View file @
12e1af27
...
@@ -14,21 +14,41 @@ from mock import Mock, patch
...
@@ -14,21 +14,41 @@ from mock import Mock, patch
from
opaque_keys.edx.keys
import
CourseKey
from
opaque_keys.edx.keys
import
CourseKey
from
opaque_keys.edx.locator
import
CourseLocator
from
opaque_keys.edx.locator
import
CourseLocator
from
course_modes.models
import
CourseMode
from
course_modes.tests.factories
import
CourseModeFactory
from
courseware.models
import
DynamicUpgradeDeadlineConfiguration
from
openedx.core.djangoapps.schedules
import
resolvers
,
tasks
from
openedx.core.djangoapps.schedules
import
resolvers
,
tasks
from
openedx.core.djangoapps.schedules.management.commands
import
send_upgrade_reminder
as
reminder
from
openedx.core.djangoapps.schedules.management.commands
import
send_upgrade_reminder
as
reminder
from
openedx.core.djangoapps.schedules.tests.factories
import
ScheduleConfigFactory
,
ScheduleFactory
from
openedx.core.djangoapps.schedules.tests.factories
import
ScheduleConfigFactory
,
ScheduleFactory
from
openedx.core.djangoapps.site_configuration.tests.factories
import
SiteConfigurationFactory
,
SiteFactory
from
openedx.core.djangoapps.site_configuration.tests.factories
import
SiteConfigurationFactory
,
SiteFactory
from
openedx.core.djangolib.testing.utils
import
CacheIsolationTestCase
,
skip_unless_lms
from
openedx.core.djangoapps.waffle_utils.testutils
import
WAFFLE_TABLES
from
openedx.core.djangolib.testing.utils
import
CacheIsolationTestCase
,
skip_unless_lms
,
FilteredQueryCountMixin
from
student.tests.factories
import
UserFactory
from
student.tests.factories
import
UserFactory
# 1) Load the current django site
# 2) Query the schedules to find all of the template context information
NUM_QUERIES_NO_MATCHING_SCHEDULES
=
2
# 3) Query all course modes for all courses in returned schedules
NUM_QUERIES_WITH_MATCHES
=
NUM_QUERIES_NO_MATCHING_SCHEDULES
+
1
# 1) Global dynamic deadline switch
# 2) E-commerce configuration
NUM_QUERIES_WITH_DEADLINE
=
2
NUM_COURSE_MODES_QUERIES
=
1
@ddt.ddt
@ddt.ddt
@skip_unless_lms
@skip_unless_lms
@skipUnless
(
'openedx.core.djangoapps.schedules.apps.SchedulesConfig'
in
settings
.
INSTALLED_APPS
,
@skipUnless
(
'openedx.core.djangoapps.schedules.apps.SchedulesConfig'
in
settings
.
INSTALLED_APPS
,
"Can't test schedules if the app isn't installed"
)
"Can't test schedules if the app isn't installed"
)
class
TestUpgradeReminder
(
CacheIsolationTestCase
):
class
TestUpgradeReminder
(
FilteredQueryCountMixin
,
CacheIsolationTestCase
):
# pylint: disable=protected-access
# pylint: disable=protected-access
ENABLED_CACHES
=
[
'default'
]
def
setUp
(
self
):
def
setUp
(
self
):
super
(
TestUpgradeReminder
,
self
)
.
setUp
()
super
(
TestUpgradeReminder
,
self
)
.
setUp
()
...
@@ -74,20 +94,26 @@ class TestUpgradeReminder(CacheIsolationTestCase):
...
@@ -74,20 +94,26 @@ class TestUpgradeReminder(CacheIsolationTestCase):
schedules
=
[
schedules
=
[
ScheduleFactory
.
create
(
ScheduleFactory
.
create
(
upgrade_deadline
=
datetime
.
datetime
(
2017
,
8
,
3
,
18
,
44
,
30
,
tzinfo
=
pytz
.
UTC
),
upgrade_deadline
=
datetime
.
datetime
(
2017
,
8
,
3
,
18
,
44
,
30
,
tzinfo
=
pytz
.
UTC
),
enrollment__user
=
UserFactory
.
create
(),
enrollment__course__id
=
CourseLocator
(
'edX'
,
'toy'
,
'Bin'
)
enrollment__course__id
=
CourseLocator
(
'edX'
,
'toy'
,
'Bin'
)
)
for
_
in
range
(
schedule_count
)
)
for
i
in
range
(
schedule_count
)
]
]
bins_in_use
=
frozenset
((
s
.
enrollment
.
user
.
id
%
tasks
.
UPGRADE_REMINDER_NUM_BINS
)
for
s
in
schedules
)
test_time
=
datetime
.
datetime
(
2017
,
8
,
3
,
18
,
tzinfo
=
pytz
.
UTC
)
test_time
=
datetime
.
datetime
(
2017
,
8
,
3
,
18
,
tzinfo
=
pytz
.
UTC
)
test_time_str
=
serialize
(
test_time
)
test_time_str
=
serialize
(
test_time
)
for
b
in
range
(
tasks
.
UPGRADE_REMINDER_NUM_BINS
):
for
b
in
range
(
tasks
.
UPGRADE_REMINDER_NUM_BINS
):
# waffle flag takes an extra query before it is cached
expected_queries
=
NUM_QUERIES_NO_MATCHING_SCHEDULES
with
self
.
assertNumQueries
(
3
if
b
==
0
else
2
):
if
b
in
bins_in_use
:
# to fetch course modes for valid schedules
expected_queries
+=
NUM_COURSE_MODES_QUERIES
with
self
.
assertNumQueries
(
expected_queries
,
table_blacklist
=
WAFFLE_TABLES
):
tasks
.
upgrade_reminder_schedule_bin
(
tasks
.
upgrade_reminder_schedule_bin
(
self
.
site_config
.
site
.
id
,
target_day_str
=
test_time_str
,
day_offset
=
2
,
bin_num
=
b
,
self
.
site_config
.
site
.
id
,
target_day_str
=
test_time_str
,
day_offset
=
2
,
bin_num
=
b
,
org_list
=
[
schedules
[
0
]
.
enrollment
.
course
.
org
],
org_list
=
[
schedules
[
0
]
.
enrollment
.
course
.
org
],
)
)
self
.
assertEqual
(
mock_schedule_send
.
apply_async
.
call_count
,
schedule_count
)
self
.
assertEqual
(
mock_schedule_send
.
apply_async
.
call_count
,
schedule_count
)
self
.
assertFalse
(
mock_ace
.
send
.
called
)
self
.
assertFalse
(
mock_ace
.
send
.
called
)
...
@@ -103,8 +129,7 @@ class TestUpgradeReminder(CacheIsolationTestCase):
...
@@ -103,8 +129,7 @@ class TestUpgradeReminder(CacheIsolationTestCase):
test_time
=
datetime
.
datetime
(
2017
,
8
,
3
,
20
,
tzinfo
=
pytz
.
UTC
)
test_time
=
datetime
.
datetime
(
2017
,
8
,
3
,
20
,
tzinfo
=
pytz
.
UTC
)
test_time_str
=
serialize
(
test_time
)
test_time_str
=
serialize
(
test_time
)
for
b
in
range
(
tasks
.
UPGRADE_REMINDER_NUM_BINS
):
for
b
in
range
(
tasks
.
UPGRADE_REMINDER_NUM_BINS
):
# waffle flag takes an extra query before it is cached
with
self
.
assertNumQueries
(
NUM_QUERIES_NO_MATCHING_SCHEDULES
,
table_blacklist
=
WAFFLE_TABLES
):
with
self
.
assertNumQueries
(
3
if
b
==
0
else
2
):
tasks
.
upgrade_reminder_schedule_bin
(
tasks
.
upgrade_reminder_schedule_bin
(
self
.
site_config
.
site
.
id
,
target_day_str
=
test_time_str
,
day_offset
=
2
,
bin_num
=
b
,
self
.
site_config
.
site
.
id
,
target_day_str
=
test_time_str
,
day_offset
=
2
,
bin_num
=
b
,
org_list
=
[
schedule
.
enrollment
.
course
.
org
],
org_list
=
[
schedule
.
enrollment
.
course
.
org
],
...
@@ -176,7 +201,7 @@ class TestUpgradeReminder(CacheIsolationTestCase):
...
@@ -176,7 +201,7 @@ class TestUpgradeReminder(CacheIsolationTestCase):
test_time
=
datetime
.
datetime
(
2017
,
8
,
3
,
17
,
tzinfo
=
pytz
.
UTC
)
test_time
=
datetime
.
datetime
(
2017
,
8
,
3
,
17
,
tzinfo
=
pytz
.
UTC
)
test_time_str
=
serialize
(
test_time
)
test_time_str
=
serialize
(
test_time
)
with
self
.
assertNumQueries
(
3
):
with
self
.
assertNumQueries
(
NUM_QUERIES_WITH_MATCHES
,
table_blacklist
=
WAFFLE_TABLES
):
tasks
.
upgrade_reminder_schedule_bin
(
tasks
.
upgrade_reminder_schedule_bin
(
limited_config
.
site
.
id
,
target_day_str
=
test_time_str
,
day_offset
=
2
,
bin_num
=
0
,
limited_config
.
site
.
id
,
target_day_str
=
test_time_str
,
day_offset
=
2
,
bin_num
=
0
,
org_list
=
org_list
,
exclude_orgs
=
exclude_orgs
,
org_list
=
org_list
,
exclude_orgs
=
exclude_orgs
,
...
@@ -200,7 +225,7 @@ class TestUpgradeReminder(CacheIsolationTestCase):
...
@@ -200,7 +225,7 @@ class TestUpgradeReminder(CacheIsolationTestCase):
test_time
=
datetime
.
datetime
(
2017
,
8
,
3
,
19
,
44
,
30
,
tzinfo
=
pytz
.
UTC
)
test_time
=
datetime
.
datetime
(
2017
,
8
,
3
,
19
,
44
,
30
,
tzinfo
=
pytz
.
UTC
)
test_time_str
=
serialize
(
test_time
)
test_time_str
=
serialize
(
test_time
)
with
self
.
assertNumQueries
(
3
):
with
self
.
assertNumQueries
(
NUM_QUERIES_WITH_MATCHES
,
table_blacklist
=
WAFFLE_TABLES
):
tasks
.
upgrade_reminder_schedule_bin
(
tasks
.
upgrade_reminder_schedule_bin
(
self
.
site_config
.
site
.
id
,
target_day_str
=
test_time_str
,
day_offset
=
2
,
self
.
site_config
.
site
.
id
,
target_day_str
=
test_time_str
,
day_offset
=
2
,
bin_num
=
user
.
id
%
tasks
.
UPGRADE_REMINDER_NUM_BINS
,
bin_num
=
user
.
id
%
tasks
.
UPGRADE_REMINDER_NUM_BINS
,
...
@@ -212,18 +237,31 @@ class TestUpgradeReminder(CacheIsolationTestCase):
...
@@ -212,18 +237,31 @@ class TestUpgradeReminder(CacheIsolationTestCase):
@ddt.data
(
*
itertools
.
product
((
1
,
10
,
100
),
(
2
,
10
)))
@ddt.data
(
*
itertools
.
product
((
1
,
10
,
100
),
(
2
,
10
)))
@ddt.unpack
@ddt.unpack
def
test_templates
(
self
,
message_count
,
day
):
def
test_templates
(
self
,
message_count
,
day
):
DynamicUpgradeDeadlineConfiguration
.
objects
.
create
(
enabled
=
True
)
now
=
datetime
.
datetime
.
now
(
pytz
.
UTC
)
future_date
=
now
+
datetime
.
timedelta
(
days
=
21
)
user
=
UserFactory
.
create
()
user
=
UserFactory
.
create
()
schedules
=
[
schedules
=
[
ScheduleFactory
.
create
(
ScheduleFactory
.
create
(
upgrade_deadline
=
datetime
.
datetime
(
2017
,
8
,
3
,
19
,
44
,
30
,
tzinfo
=
pytz
.
UTC
)
,
upgrade_deadline
=
future_date
,
enrollment__user
=
user
,
enrollment__user
=
user
,
enrollment__course__id
=
CourseLocator
(
'edX'
,
'toy'
,
'Course{}'
.
format
(
course_num
))
enrollment__course__id
=
CourseLocator
(
'edX'
,
'toy'
,
'Course{}'
.
format
(
course_num
))
)
)
for
course_num
in
range
(
message_count
)
for
course_num
in
range
(
message_count
)
]
]
test_time
=
datetime
.
datetime
(
2017
,
8
,
3
,
19
,
tzinfo
=
pytz
.
UTC
)
for
schedule
in
schedules
:
schedule
.
enrollment
.
course
.
self_paced
=
True
schedule
.
enrollment
.
course
.
save
()
CourseModeFactory
(
course_id
=
schedule
.
enrollment
.
course
.
id
,
mode_slug
=
CourseMode
.
VERIFIED
,
expiration_datetime
=
future_date
)
test_time
=
future_date
test_time_str
=
serialize
(
test_time
)
test_time_str
=
serialize
(
test_time
)
patch_policies
(
self
,
[
StubPolicy
([
ChannelType
.
PUSH
])])
patch_policies
(
self
,
[
StubPolicy
([
ChannelType
.
PUSH
])])
...
@@ -241,7 +279,10 @@ class TestUpgradeReminder(CacheIsolationTestCase):
...
@@ -241,7 +279,10 @@ class TestUpgradeReminder(CacheIsolationTestCase):
with
patch
.
object
(
tasks
,
'_upgrade_reminder_schedule_send'
)
as
mock_schedule_send
:
with
patch
.
object
(
tasks
,
'_upgrade_reminder_schedule_send'
)
as
mock_schedule_send
:
mock_schedule_send
.
apply_async
=
lambda
args
,
*
_a
,
**
_kw
:
sent_messages
.
append
(
args
)
mock_schedule_send
.
apply_async
=
lambda
args
,
*
_a
,
**
_kw
:
sent_messages
.
append
(
args
)
with
self
.
assertNumQueries
(
3
):
# we execute one query per course to see if it's opted out of dynamic upgrade deadlines, however,
# since we create a new course for each schedule in this test, we expect there to be one per message
num_expected_queries
=
NUM_QUERIES_WITH_MATCHES
+
NUM_QUERIES_WITH_DEADLINE
+
message_count
with
self
.
assertNumQueries
(
num_expected_queries
,
table_blacklist
=
WAFFLE_TABLES
):
tasks
.
upgrade_reminder_schedule_bin
(
tasks
.
upgrade_reminder_schedule_bin
(
self
.
site_config
.
site
.
id
,
target_day_str
=
test_time_str
,
day_offset
=
day
,
self
.
site_config
.
site
.
id
,
target_day_str
=
test_time_str
,
day_offset
=
day
,
bin_num
=
user
.
id
%
tasks
.
UPGRADE_REMINDER_NUM_BINS
,
bin_num
=
user
.
id
%
tasks
.
UPGRADE_REMINDER_NUM_BINS
,
...
...
openedx/core/djangoapps/schedules/signals.py
View file @
12e1af27
...
@@ -112,7 +112,7 @@ def _get_upgrade_deadline_delta_setting(course_id):
...
@@ -112,7 +112,7 @@ def _get_upgrade_deadline_delta_setting(course_id):
# Check if the course has a deadline
# Check if the course has a deadline
course_config
=
CourseDynamicUpgradeDeadlineConfiguration
.
current
(
course_id
)
course_config
=
CourseDynamicUpgradeDeadlineConfiguration
.
current
(
course_id
)
if
course_config
.
enabled
:
if
course_config
.
enabled
and
not
course_config
.
opt_out
:
delta
=
course_config
.
deadline_days
delta
=
course_config
.
deadline_days
return
delta
return
delta
openedx/core/djangoapps/schedules/tasks.py
View file @
12e1af27
...
@@ -12,6 +12,7 @@ from django.core.urlresolvers import reverse
...
@@ -12,6 +12,7 @@ from django.core.urlresolvers import reverse
from
django.db.models
import
F
,
Min
from
django.db.models
import
F
,
Min
from
django.db.utils
import
DatabaseError
from
django.db.utils
import
DatabaseError
from
django.utils.formats
import
dateformat
,
get_format
from
django.utils.formats
import
dateformat
,
get_format
import
pytz
from
edx_ace
import
ace
from
edx_ace
import
ace
from
edx_ace.message
import
Message
from
edx_ace.message
import
Message
...
@@ -19,6 +20,8 @@ from edx_ace.recipient import Recipient
...
@@ -19,6 +20,8 @@ from edx_ace.recipient import Recipient
from
edx_ace.utils.date
import
deserialize
from
edx_ace.utils.date
import
deserialize
from
opaque_keys.edx.keys
import
CourseKey
from
opaque_keys.edx.keys
import
CourseKey
from
courseware.date_summary
import
verified_upgrade_deadline_link
,
verified_upgrade_link_is_valid
from
edxmako.shortcuts
import
marketing_link
from
edxmako.shortcuts
import
marketing_link
from
openedx.core.djangoapps.schedules.message_type
import
ScheduleMessageType
from
openedx.core.djangoapps.schedules.message_type
import
ScheduleMessageType
from
openedx.core.djangoapps.schedules.models
import
Schedule
,
ScheduleConfig
from
openedx.core.djangoapps.schedules.models
import
Schedule
,
ScheduleConfig
...
@@ -105,6 +108,40 @@ def _recurring_nudge_schedule_send(site_id, msg_str):
...
@@ -105,6 +108,40 @@ def _recurring_nudge_schedule_send(site_id, msg_str):
# TODO: delete once _recurring_nudge_schedules_for_bin is fully rolled out
# TODO: delete once _recurring_nudge_schedules_for_bin is fully rolled out
def
_recurring_nudge_schedules_for_hour
(
site
,
target_hour
,
org_list
,
exclude_orgs
=
False
):
def
_recurring_nudge_schedules_for_hour
(
site
,
target_hour
,
org_list
,
exclude_orgs
=
False
):
users
,
schedules
=
_gather_users_and_schedules_for_target_hour
(
target_hour
,
org_list
,
exclude_orgs
)
dashboard_relative_url
=
reverse
(
'dashboard'
)
for
(
user
,
user_schedules
)
in
groupby
(
schedules
,
lambda
s
:
s
.
enrollment
.
user
):
user_schedules
=
list
(
user_schedules
)
course_id_strs
=
[
str
(
schedule
.
enrollment
.
course_id
)
for
schedule
in
user_schedules
]
first_schedule
=
user_schedules
[
0
]
template_context
=
{
'student_name'
:
user
.
profile
.
name
,
'course_name'
:
first_schedule
.
enrollment
.
course
.
display_name
,
'course_url'
:
absolute_url
(
site
,
reverse
(
'course_root'
,
args
=
[
str
(
first_schedule
.
enrollment
.
course_id
)])),
# This is used by the bulk email optout policy
'course_ids'
:
course_id_strs
,
# Platform information
'homepage_url'
:
encode_url
(
marketing_link
(
'ROOT'
)),
'dashboard_url'
:
absolute_url
(
site
,
dashboard_relative_url
),
'template_revision'
:
settings
.
EDX_PLATFORM_REVISION
,
'platform_name'
:
settings
.
PLATFORM_NAME
,
'contact_mailing_address'
:
settings
.
CONTACT_MAILING_ADDRESS
,
'social_media_urls'
:
encode_urls_in_dict
(
getattr
(
settings
,
'SOCIAL_MEDIA_FOOTER_URLS'
,
{})),
'mobile_store_urls'
:
encode_urls_in_dict
(
getattr
(
settings
,
'MOBILE_STORE_URLS'
,
{})),
}
# Information for including upsell messaging in template.
_add_upsell_button_information_to_template_context
(
user
,
first_schedule
,
template_context
)
yield
(
user
,
first_schedule
.
enrollment
.
course
.
language
,
template_context
)
def
_gather_users_and_schedules_for_target_hour
(
target_hour
,
org_list
,
exclude_orgs
):
beginning_of_day
=
target_hour
.
replace
(
hour
=
0
,
minute
=
0
,
second
=
0
)
beginning_of_day
=
target_hour
.
replace
(
hour
=
0
,
minute
=
0
,
second
=
0
)
users
=
User
.
objects
.
filter
(
users
=
User
.
objects
.
filter
(
courseenrollment__schedule__start__gte
=
beginning_of_day
,
courseenrollment__schedule__start__gte
=
beginning_of_day
,
...
@@ -120,6 +157,8 @@ def _recurring_nudge_schedules_for_hour(site, target_hour, org_list, exclude_org
...
@@ -120,6 +157,8 @@ def _recurring_nudge_schedules_for_hour(site, target_hour, org_list, exclude_org
schedules
=
Schedule
.
objects
.
select_related
(
schedules
=
Schedule
.
objects
.
select_related
(
'enrollment__user__profile'
,
'enrollment__user__profile'
,
'enrollment__course'
,
'enrollment__course'
,
)
.
prefetch_related
(
'enrollment__course__modes'
)
.
filter
(
)
.
filter
(
enrollment__user__in
=
users
,
enrollment__user__in
=
users
,
start__gte
=
beginning_of_day
,
start__gte
=
beginning_of_day
,
...
@@ -138,32 +177,7 @@ def _recurring_nudge_schedules_for_hour(site, target_hour, org_list, exclude_org
...
@@ -138,32 +177,7 @@ def _recurring_nudge_schedules_for_hour(site, target_hour, org_list, exclude_org
LOG
.
debug
(
'Scheduled Nudge: Query =
%
r'
,
schedules
.
query
.
sql_with_params
())
LOG
.
debug
(
'Scheduled Nudge: Query =
%
r'
,
schedules
.
query
.
sql_with_params
())
dashboard_relative_url
=
reverse
(
'dashboard'
)
return
users
,
schedules
for
(
user
,
user_schedules
)
in
groupby
(
schedules
,
lambda
s
:
s
.
enrollment
.
user
):
user_schedules
=
list
(
user_schedules
)
course_id_strs
=
[
str
(
schedule
.
enrollment
.
course_id
)
for
schedule
in
user_schedules
]
first_schedule
=
user_schedules
[
0
]
template_context
=
{
'student_name'
:
user
.
profile
.
name
,
'course_name'
:
first_schedule
.
enrollment
.
course
.
display_name
,
'course_url'
:
absolute_url
(
site
,
reverse
(
'course_root'
,
args
=
[
str
(
first_schedule
.
enrollment
.
course_id
)])),
# This is used by the bulk email optout policy
'course_ids'
:
course_id_strs
,
# Platform information
'homepage_url'
:
encode_url
(
marketing_link
(
'ROOT'
)),
'dashboard_url'
:
absolute_url
(
site
,
dashboard_relative_url
),
'template_revision'
:
settings
.
EDX_PLATFORM_REVISION
,
'platform_name'
:
settings
.
PLATFORM_NAME
,
'contact_mailing_address'
:
settings
.
CONTACT_MAILING_ADDRESS
,
'social_media_urls'
:
encode_urls_in_dict
(
getattr
(
settings
,
'SOCIAL_MEDIA_FOOTER_URLS'
,
{})),
'mobile_store_urls'
:
encode_urls_in_dict
(
getattr
(
settings
,
'MOBILE_STORE_URLS'
,
{})),
}
yield
(
user
,
first_schedule
.
enrollment
.
course
.
language
,
template_context
)
@task
(
ignore_result
=
True
,
routing_key
=
ROUTING_KEY
)
@task
(
ignore_result
=
True
,
routing_key
=
ROUTING_KEY
)
...
@@ -219,6 +233,10 @@ def _recurring_nudge_schedules_for_bin(site, target_day, bin_num, org_list, excl
...
@@ -219,6 +233,10 @@ def _recurring_nudge_schedules_for_bin(site, target_day, bin_num, org_list, excl
# This is used by the bulk email optout policy
# This is used by the bulk email optout policy
'course_ids'
:
course_id_strs
,
'course_ids'
:
course_id_strs
,
})
})
# Information for including upsell messaging in template.
_add_upsell_button_information_to_template_context
(
user
,
first_schedule
,
template_context
)
yield
(
user
,
first_schedule
.
enrollment
.
course
.
language
,
template_context
)
yield
(
user
,
first_schedule
.
enrollment
.
course
.
language
,
template_context
)
...
@@ -289,14 +307,6 @@ def _upgrade_reminder_schedules_for_bin(site, target_day, bin_num, org_list, exc
...
@@ -289,14 +307,6 @@ def _upgrade_reminder_schedules_for_bin(site, target_day, bin_num, org_list, exc
template_context
.
update
({
template_context
.
update
({
'student_name'
:
user
.
profile
.
name
,
'student_name'
:
user
.
profile
.
name
,
'user_personal_address'
:
user
.
profile
.
name
if
user
.
profile
.
name
else
user
.
username
,
'user_personal_address'
:
user
.
profile
.
name
if
user
.
profile
.
name
else
user
.
username
,
'user_schedule_upgrade_deadline_time'
:
dateformat
.
format
(
schedule
.
upgrade_deadline
,
get_format
(
'DATE_FORMAT'
,
lang
=
first_schedule
.
enrollment
.
course
.
language
,
use_l10n
=
True
)
),
'course_name'
:
first_schedule
.
enrollment
.
course
.
display_name
,
'course_name'
:
first_schedule
.
enrollment
.
course
.
display_name
,
'course_url'
:
absolute_url
(
site
,
reverse
(
'course_root'
,
args
=
[
str
(
first_schedule
.
enrollment
.
course_id
)])),
'course_url'
:
absolute_url
(
site
,
reverse
(
'course_root'
,
args
=
[
str
(
first_schedule
.
enrollment
.
course_id
)])),
...
@@ -306,6 +316,8 @@ def _upgrade_reminder_schedules_for_bin(site, target_day, bin_num, org_list, exc
...
@@ -306,6 +316,8 @@ def _upgrade_reminder_schedules_for_bin(site, target_day, bin_num, org_list, exc
'cert_image'
:
absolute_url
(
site
,
static
(
'course_experience/images/verified-cert.png'
)),
'cert_image'
:
absolute_url
(
site
,
static
(
'course_experience/images/verified-cert.png'
)),
})
})
_add_upsell_button_information_to_template_context
(
user
,
first_schedule
,
template_context
)
yield
(
user
,
first_schedule
.
enrollment
.
course
.
language
,
template_context
)
yield
(
user
,
first_schedule
.
enrollment
.
course
.
language
,
template_context
)
...
@@ -344,6 +356,8 @@ def get_schedules_with_target_date_by_bin_and_orgs(schedule_date_field, target_d
...
@@ -344,6 +356,8 @@ def get_schedules_with_target_date_by_bin_and_orgs(schedule_date_field, target_d
schedules
=
Schedule
.
objects
.
select_related
(
schedules
=
Schedule
.
objects
.
select_related
(
'enrollment__user__profile'
,
'enrollment__user__profile'
,
'enrollment__course'
,
'enrollment__course'
,
)
.
prefetch_related
(
'enrollment__course__modes'
)
.
filter
(
)
.
filter
(
enrollment__user__in
=
users
,
enrollment__user__in
=
users
,
enrollment__is_active
=
True
,
enrollment__is_active
=
True
,
...
@@ -360,3 +374,32 @@ def get_schedules_with_target_date_by_bin_and_orgs(schedule_date_field, target_d
...
@@ -360,3 +374,32 @@ def get_schedules_with_target_date_by_bin_and_orgs(schedule_date_field, target_d
schedules
=
schedules
.
using
(
"read_replica"
)
schedules
=
schedules
.
using
(
"read_replica"
)
return
schedules
return
schedules
def
_add_upsell_button_information_to_template_context
(
user
,
schedule
,
template_context
):
enrollment
=
schedule
.
enrollment
course
=
enrollment
.
course
verified_upgrade_link
=
_get_link_to_purchase_verified_certificate
(
user
,
schedule
)
has_verified_upgrade_link
=
verified_upgrade_link
is
not
None
if
has_verified_upgrade_link
:
template_context
[
'upsell_link'
]
=
verified_upgrade_link
template_context
[
'user_schedule_upgrade_deadline_time'
]
=
dateformat
.
format
(
enrollment
.
dynamic_upgrade_deadline
,
get_format
(
'DATE_FORMAT'
,
lang
=
course
.
language
,
use_l10n
=
True
)
)
template_context
[
'show_upsell'
]
=
has_verified_upgrade_link
def
_get_link_to_purchase_verified_certificate
(
a_user
,
a_schedule
):
enrollment
=
a_schedule
.
enrollment
if
enrollment
.
dynamic_upgrade_deadline
is
None
or
not
verified_upgrade_link_is_valid
(
enrollment
):
return
None
return
verified_upgrade_deadline_link
(
a_user
,
enrollment
.
course
)
openedx/core/djangoapps/schedules/templates/schedules/edx_ace/recurringnudge_day3/email/body.html
View file @
12e1af27
...
@@ -36,7 +36,7 @@
...
@@ -36,7 +36,7 @@
</p>
</p>
<p>
<p>
<!-- email client support for style sheets is pretty spotty, so we have to inline all of these styles -->
{# email client support for style sheets is pretty spotty, so we have to inline all of these styles #}
<a
<a
{%
if
course_ids
|
length
>
1 %}
{%
if
course_ids
|
length
>
1 %}
href="{{ dashboard_url }}"
href="{{ dashboard_url }}"
...
@@ -46,20 +46,48 @@
...
@@ -46,20 +46,48 @@
style="
style="
color: #ffffff;
color: #ffffff;
text-decoration: none;
text-decoration: none;
border-radius:
4px
;
border-radius:
.3rem
;
-webkit-border-radius:
4px
;
-webkit-border-radius:
.3rem
;
-moz-border-radius:
4px
;
-moz-border-radius:
.3rem
;
background-color: #005686;
background-color: #005686;
border-top:
10px
solid #005686;
border-top:
.15rem
solid #005686;
border-bottom:
10px
solid #005686;
border-bottom:
.15rem
solid #005686;
border-right:
16px
solid #005686;
border-right:
.15rem
solid #005686;
border-left:
16px
solid #005686;
border-left:
.15rem
solid #005686;
display: inline-block;
display: inline-block;
padding: 1rem 5rem;
">
">
<!-- old email clients require the use of the font tag :( -->
{# old email clients require the use of the font tag :( #}
<font
color=
"#ffffff"
><b>
{% trans "Start learning now" %}
</b></font>
<font
color=
"#ffffff"
><b>
{% trans "Start learning now" %}
</b></font>
</a>
</a>
</p>
</p>
{% if show_upsell %}
<p>
{% blocktrans trimmed %}
Don't miss the opportunity to highlight your new knowledge and skills by earning a verified
certificate. Upgrade by {{ user_schedule_upgrade_deadline_time }}.
{% endblocktrans %}
</p>
<p>
<a
href=
"{{ upsell_link }}"
style=
"
color: #1e8142;
text-decoration: none;
border-radius: .3rem;
-webkit-border-radius: .3rem;
-moz-border-radius: .3rem;
background-color: #FFFFFF;
border-top: .15rem solid #1e8142;
border-bottom: .15rem solid #1e8142;
border-right: .15rem solid #1e8142;
border-left: .15rem solid #1e8142;
display: inline-block;
padding: 1rem 6.1rem;
"
>
<font
color=
"#1e8142"
><b>
{% trans "Upgrade Now" %}
</b></font>
</a>
</p>
{% endif %}
</td>
</td>
</tr>
</tr>
</table>
</table>
...
...
openedx/core/djangoapps/schedules/templates/schedules/edx_ace/recurringnudge_day3/email/body.txt
View file @
12e1af27
...
@@ -15,3 +15,12 @@
...
@@ -15,3 +15,12 @@
{% trans "Start learning now" %} <{{ course_url }}>
{% trans "Start learning now" %} <{{ course_url }}>
{% endif %}
{% endif %}
{% if show_upsell %}
{% blocktrans trimmed %}
Don't miss the opportunity to highlight your new knowledge and skills by earning a verified
certificate. Upgrade by {{ user_schedule_upgrade_deadline_time }}.
Upgrade Now! <{{ upsell_link }}>
{% endblocktrans %}
{% endif %}
openedx/core/djangoapps/schedules/templates/schedules/edx_ace/upgradereminder/email/body.html
View file @
12e1af27
...
@@ -30,7 +30,7 @@
...
@@ -30,7 +30,7 @@
{% endblocktrans %}
{% endblocktrans %}
</p>
</p>
<a
href=
"{{
course_url
}}"
>
<a
href=
"{{
upsell_link
}}"
>
<img
<img
src=
"{{ cert_image }}"
src=
"{{ cert_image }}"
alt=
"{% blocktrans %}Example print-out of a verified certificate{% endblocktrans %}"
alt=
"{% blocktrans %}Example print-out of a verified certificate{% endblocktrans %}"
...
@@ -50,7 +50,7 @@
...
@@ -50,7 +50,7 @@
<p>
<p>
<!-- email client support for style sheets is pretty spotty, so we have to inline all of these styles -->
<!-- email client support for style sheets is pretty spotty, so we have to inline all of these styles -->
<a
<a
href=
"{{
course_url
}}"
href=
"{{
upsell_link
}}"
style=
"
style=
"
color: #ffffff;
color: #ffffff;
text-decoration: none;
text-decoration: none;
...
...
openedx/core/djangoapps/schedules/templates/schedules/edx_ace/upgradereminder/email/body.txt
View file @
12e1af27
...
@@ -11,4 +11,4 @@ Dear {{ user_personal_address }},
...
@@ -11,4 +11,4 @@ Dear {{ user_personal_address }},
Upgrade by {{ user_schedule_upgrade_deadline_time }}.
Upgrade by {{ user_schedule_upgrade_deadline_time }}.
{% endblocktrans %}
{% endblocktrans %}
{% trans "Upgrade now at" %} <{{
course_url
}}>
{% trans "Upgrade now at" %} <{{
upsell_link
}}>
openedx/features/course_experience/tests/views/test_course_updates.py
View file @
12e1af27
...
@@ -126,7 +126,7 @@ class TestCourseUpdatesPage(SharedModuleStoreTestCase):
...
@@ -126,7 +126,7 @@ class TestCourseUpdatesPage(SharedModuleStoreTestCase):
course_updates_url
(
self
.
course
)
course_updates_url
(
self
.
course
)
# Fetch the view and verify that the query counts haven't changed
# Fetch the view and verify that the query counts haven't changed
with
self
.
assertNumQueries
(
3
3
,
table_blacklist
=
QUERY_COUNT_TABLE_BLACKLIST
):
with
self
.
assertNumQueries
(
3
0
,
table_blacklist
=
QUERY_COUNT_TABLE_BLACKLIST
):
with
check_mongo_calls
(
4
):
with
check_mongo_calls
(
4
):
url
=
course_updates_url
(
self
.
course
)
url
=
course_updates_url
(
self
.
course
)
self
.
client
.
get
(
url
)
self
.
client
.
get
(
url
)
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