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
2c80b1b4
Commit
2c80b1b4
authored
Oct 27, 2017
by
Nimisha Asthagiri
Committed by
GitHub
Oct 27, 2017
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #16339 from edx/cale/dry-schedule-tests
Dry schedule tests
parents
764e598f
b461ce0c
Show whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
451 additions
and
717 deletions
+451
-717
openedx/core/djangoapps/schedules/management/commands/__init__.py
+5
-3
openedx/core/djangoapps/schedules/management/commands/send_course_update.py
+2
-8
openedx/core/djangoapps/schedules/management/commands/send_recurring_nudge.py
+2
-8
openedx/core/djangoapps/schedules/management/commands/send_upgrade_reminder.py
+2
-7
openedx/core/djangoapps/schedules/management/commands/tests/send_email_base.py
+377
-0
openedx/core/djangoapps/schedules/management/commands/tests/test_send_email_base_command.py
+16
-1
openedx/core/djangoapps/schedules/management/commands/tests/test_send_recurring_nudge.py
+21
-310
openedx/core/djangoapps/schedules/management/commands/tests/test_send_upgrade_reminder.py
+26
-380
No files found.
openedx/core/djangoapps/schedules/management/commands/__init__.py
View file @
2c80b1b4
...
...
@@ -9,6 +9,7 @@ from openedx.core.djangoapps.schedules.utils import PrefixedDebugLoggerMixin
class
SendEmailBaseCommand
(
PrefixedDebugLoggerMixin
,
BaseCommand
):
async_send_task
=
None
# define in subclass
offsets
=
()
# define in subclass
def
add_arguments
(
self
,
parser
):
parser
.
add_argument
(
...
...
@@ -37,9 +38,6 @@ class SendEmailBaseCommand(PrefixedDebugLoggerMixin, BaseCommand):
override_recipient_email
=
options
.
get
(
'override_recipient_email'
)
self
.
send_emails
(
site
,
current_date
,
override_recipient_email
)
def
send_emails
(
self
,
*
args
,
**
kwargs
):
raise
NotImplementedError
def
enqueue
(
self
,
day_offset
,
site
,
current_date
,
override_recipient_email
=
None
):
self
.
async_send_task
.
enqueue
(
site
,
...
...
@@ -47,3 +45,7 @@ class SendEmailBaseCommand(PrefixedDebugLoggerMixin, BaseCommand):
day_offset
,
override_recipient_email
,
)
def
send_emails
(
self
,
*
args
,
**
kwargs
):
for
offset
in
self
.
offsets
:
self
.
enqueue
(
offset
,
*
args
,
**
kwargs
)
openedx/core/djangoapps/schedules/management/commands/send_course_update.py
View file @
2c80b1b4
...
...
@@ -4,11 +4,5 @@ from openedx.core.djangoapps.schedules.tasks import ScheduleCourseUpdate
class
Command
(
SendEmailBaseCommand
):
async_send_task
=
ScheduleCourseUpdate
def
__init__
(
self
,
*
args
,
**
kwargs
):
super
(
Command
,
self
)
.
__init__
(
*
args
,
**
kwargs
)
self
.
log_prefix
=
'Upgrade Reminder'
def
send_emails
(
self
,
*
args
,
**
kwargs
):
for
day_offset
in
xrange
(
-
7
,
-
77
,
-
7
):
self
.
enqueue
(
day_offset
,
*
args
,
**
kwargs
)
log_prefix
=
'Course Update'
offsets
=
xrange
(
-
7
,
-
77
,
-
7
)
openedx/core/djangoapps/schedules/management/commands/send_recurring_nudge.py
View file @
2c80b1b4
...
...
@@ -4,11 +4,5 @@ from openedx.core.djangoapps.schedules.tasks import ScheduleRecurringNudge
class
Command
(
SendEmailBaseCommand
):
async_send_task
=
ScheduleRecurringNudge
def
__init__
(
self
,
*
args
,
**
kwargs
):
super
(
Command
,
self
)
.
__init__
(
*
args
,
**
kwargs
)
self
.
log_prefix
=
'Scheduled Nudge'
def
send_emails
(
self
,
*
args
,
**
kwargs
):
for
day_offset
in
(
-
3
,
-
10
):
self
.
enqueue
(
day_offset
,
*
args
,
**
kwargs
)
log_prefix
=
'Scheduled Nudge'
offsets
=
(
-
3
,
-
10
)
openedx/core/djangoapps/schedules/management/commands/send_upgrade_reminder.py
View file @
2c80b1b4
...
...
@@ -4,10 +4,5 @@ from openedx.core.djangoapps.schedules.tasks import ScheduleUpgradeReminder
class
Command
(
SendEmailBaseCommand
):
async_send_task
=
ScheduleUpgradeReminder
def
__init__
(
self
,
*
args
,
**
kwargs
):
super
(
Command
,
self
)
.
__init__
(
*
args
,
**
kwargs
)
self
.
log_prefix
=
'Upgrade Reminder'
def
send_emails
(
self
,
*
args
,
**
kwargs
):
self
.
enqueue
(
2
,
*
args
,
**
kwargs
)
log_prefix
=
'Upgrade Reminder'
offsets
=
(
2
,)
openedx/core/djangoapps/schedules/management/commands/tests/send_email_base.py
0 → 100644
View file @
2c80b1b4
from
copy
import
deepcopy
import
datetime
import
ddt
import
logging
import
attr
from
django.conf
import
settings
from
freezegun
import
freeze_time
from
mock
import
Mock
,
patch
import
pytz
from
courseware.models
import
DynamicUpgradeDeadlineConfiguration
from
edx_ace.channel
import
ChannelType
from
edx_ace.utils.date
import
serialize
from
edx_ace.test_utils
import
StubPolicy
,
patch_channels
,
patch_policies
from
opaque_keys.edx.keys
import
CourseKey
from
openedx.core.djangoapps.site_configuration.tests.factories
import
SiteConfigurationFactory
,
SiteFactory
from
openedx.core.djangoapps.schedules
import
resolvers
,
tasks
from
openedx.core.djangoapps.schedules.resolvers
import
_get_datetime_beginning_of_day
from
openedx.core.djangoapps.schedules.tests.factories
import
ScheduleConfigFactory
,
ScheduleFactory
from
openedx.core.djangoapps.waffle_utils.testutils
import
WAFFLE_TABLES
from
student.tests.factories
import
UserFactory
from
xmodule.modulestore.tests.django_utils
import
SharedModuleStoreTestCase
SITE_QUERY
=
1
ORG_DEADLINE_QUERY
=
1
SCHEDULES_QUERY
=
1
COURSE_MODES_QUERY
=
1
GLOBAL_DEADLINE_SWITCH_QUERY
=
1
COMMERCE_CONFIG_QUERY
=
1
NUM_QUERIES_NO_ORG_LIST
=
1
NUM_QUERIES_NO_MATCHING_SCHEDULES
=
SITE_QUERY
+
SCHEDULES_QUERY
NUM_QUERIES_WITH_MATCHES
=
(
NUM_QUERIES_NO_MATCHING_SCHEDULES
+
COURSE_MODES_QUERY
)
NUM_QUERIES_FIRST_MATCH
=
(
NUM_QUERIES_WITH_MATCHES
+
GLOBAL_DEADLINE_SWITCH_QUERY
+
ORG_DEADLINE_QUERY
+
COMMERCE_CONFIG_QUERY
)
LOG
=
logging
.
getLogger
(
__name__
)
@ddt.ddt
@freeze_time
(
'2017-08-01 00:00:00'
,
tz_offset
=
0
,
tick
=
True
)
class
ScheduleSendEmailTestBase
(
SharedModuleStoreTestCase
):
__test__
=
False
ENABLED_CACHES
=
[
'default'
]
has_course_queries
=
False
def
setUp
(
self
):
super
(
ScheduleSendEmailTestBase
,
self
)
.
setUp
()
site
=
SiteFactory
.
create
()
self
.
site_config
=
SiteConfigurationFactory
.
create
(
site
=
site
)
ScheduleConfigFactory
.
create
(
site
=
self
.
site_config
.
site
)
DynamicUpgradeDeadlineConfiguration
.
objects
.
create
(
enabled
=
True
)
def
_calculate_bin_for_user
(
self
,
user
):
return
user
.
id
%
self
.
tested_task
.
num_bins
def
_get_dates
(
self
,
offset
=
None
):
current_day
=
_get_datetime_beginning_of_day
(
datetime
.
datetime
.
now
(
pytz
.
UTC
))
offset
=
offset
or
self
.
expected_offsets
[
0
]
target_day
=
current_day
+
datetime
.
timedelta
(
days
=
offset
)
return
current_day
,
offset
,
target_day
def
_get_template_overrides
(
self
):
templates_override
=
deepcopy
(
settings
.
TEMPLATES
)
templates_override
[
0
][
'OPTIONS'
][
'string_if_invalid'
]
=
"TEMPLATE WARNING - MISSING VARIABLE [
%
s]"
return
templates_override
def
test_command_task_binding
(
self
):
self
.
assertEqual
(
self
.
tested_command
.
async_send_task
,
self
.
tested_task
)
def
test_handle
(
self
):
with
patch
.
object
(
self
.
tested_command
,
'async_send_task'
)
as
mock_send
:
test_day
=
datetime
.
datetime
(
2017
,
8
,
1
,
tzinfo
=
pytz
.
UTC
)
self
.
tested_command
()
.
handle
(
date
=
'2017-08-01'
,
site_domain_name
=
self
.
site_config
.
site
.
domain
)
for
offset
in
self
.
expected_offsets
:
mock_send
.
enqueue
.
assert_any_call
(
self
.
site_config
.
site
,
test_day
,
offset
,
None
)
@patch.object
(
tasks
,
'ace'
)
def
test_resolver_send
(
self
,
mock_ace
):
current_day
,
offset
,
target_day
=
self
.
_get_dates
()
with
patch
.
object
(
self
.
tested_task
,
'apply_async'
)
as
mock_apply_async
:
self
.
tested_task
.
enqueue
(
self
.
site_config
.
site
,
current_day
,
offset
)
mock_apply_async
.
assert_any_call
(
(
self
.
site_config
.
site
.
id
,
serialize
(
target_day
),
offset
,
0
,
None
),
retry
=
False
,
)
mock_apply_async
.
assert_any_call
(
(
self
.
site_config
.
site
.
id
,
serialize
(
target_day
),
offset
,
self
.
tested_task
.
num_bins
-
1
,
None
),
retry
=
False
,
)
self
.
assertFalse
(
mock_ace
.
send
.
called
)
@ddt.data
(
1
,
10
,
100
)
@patch.object
(
tasks
,
'ace'
)
@patch.object
(
resolvers
,
'set_custom_metric'
)
def
test_schedule_bin
(
self
,
schedule_count
,
mock_metric
,
mock_ace
):
with
patch
.
object
(
self
.
tested_task
,
'async_send_task'
)
as
mock_schedule_send
:
current_day
,
offset
,
target_day
=
self
.
_get_dates
()
schedules
=
[
ScheduleFactory
.
create
(
start
=
target_day
,
upgrade_deadline
=
target_day
,
enrollment__course__self_paced
=
True
,
)
for
_
in
range
(
schedule_count
)
]
bins_in_use
=
frozenset
((
self
.
_calculate_bin_for_user
(
s
.
enrollment
.
user
))
for
s
in
schedules
)
is_first_match
=
True
course_queries
=
len
(
set
(
s
.
enrollment
.
course
.
id
for
s
in
schedules
))
if
self
.
has_course_queries
else
0
target_day_str
=
serialize
(
target_day
)
for
b
in
range
(
self
.
tested_task
.
num_bins
):
LOG
.
debug
(
'Running bin
%
d'
,
b
)
expected_queries
=
NUM_QUERIES_NO_MATCHING_SCHEDULES
if
b
in
bins_in_use
:
if
is_first_match
:
expected_queries
=
(
# Since this is the first match, we need to cache all of the config models, so we run a
# query for each of those...
NUM_QUERIES_FIRST_MATCH
+
course_queries
)
is_first_match
=
False
else
:
expected_queries
=
NUM_QUERIES_WITH_MATCHES
expected_queries
+=
NUM_QUERIES_NO_ORG_LIST
with
self
.
assertNumQueries
(
expected_queries
,
table_blacklist
=
WAFFLE_TABLES
):
self
.
tested_task
.
apply
(
kwargs
=
dict
(
site_id
=
self
.
site_config
.
site
.
id
,
target_day_str
=
target_day_str
,
day_offset
=
offset
,
bin_num
=
b
,
))
num_schedules
=
mock_metric
.
call_args
[
0
][
1
]
if
b
in
bins_in_use
:
self
.
assertGreater
(
num_schedules
,
0
)
else
:
self
.
assertEqual
(
num_schedules
,
0
)
self
.
assertEqual
(
mock_schedule_send
.
apply_async
.
call_count
,
schedule_count
)
self
.
assertFalse
(
mock_ace
.
send
.
called
)
def
test_no_course_overview
(
self
):
current_day
,
offset
,
target_day
=
self
.
_get_dates
()
schedule
=
ScheduleFactory
.
create
(
start
=
target_day
,
upgrade_deadline
=
target_day
,
enrollment__course__self_paced
=
True
,
)
schedule
.
enrollment
.
course_id
=
CourseKey
.
from_string
(
'edX/toy/Not_2012_Fall'
)
schedule
.
enrollment
.
save
()
with
patch
.
object
(
self
.
tested_task
,
'async_send_task'
)
as
mock_schedule_send
:
for
b
in
range
(
self
.
tested_task
.
num_bins
):
self
.
tested_task
.
apply
(
kwargs
=
dict
(
site_id
=
self
.
site_config
.
site
.
id
,
target_day_str
=
serialize
(
target_day
),
day_offset
=
offset
,
bin_num
=
b
,
))
# There is no database constraint that enforces that enrollment.course_id points
# to a valid CourseOverview object. However, in that case, schedules isn't going
# to attempt to address it, and will instead simply skip those users.
# This happens 'transparently' because django generates an inner-join between
# enrollment and course_overview, and thus will skip any rows where course_overview
# is null.
self
.
assertEqual
(
mock_schedule_send
.
apply_async
.
call_count
,
0
)
@ddt.data
(
True
,
False
)
@patch.object
(
tasks
,
'ace'
)
@patch.object
(
tasks
,
'Message'
)
def
test_deliver_config
(
self
,
is_enabled
,
mock_message
,
mock_ace
):
schedule_config_kwargs
=
{
'site'
:
self
.
site_config
.
site
,
self
.
deliver_config
:
is_enabled
,
}
ScheduleConfigFactory
.
create
(
**
schedule_config_kwargs
)
mock_msg
=
Mock
()
self
.
deliver_task
(
self
.
site_config
.
site
.
id
,
mock_msg
)
if
is_enabled
:
self
.
assertTrue
(
mock_ace
.
send
.
called
)
else
:
self
.
assertFalse
(
mock_ace
.
send
.
called
)
@ddt.data
(
True
,
False
)
def
test_enqueue_config
(
self
,
is_enabled
):
schedule_config_kwargs
=
{
'site'
:
self
.
site_config
.
site
,
self
.
enqueue_config
:
is_enabled
,
}
ScheduleConfigFactory
.
create
(
**
schedule_config_kwargs
)
current_datetime
=
datetime
.
datetime
(
2017
,
8
,
1
,
tzinfo
=
pytz
.
UTC
)
with
patch
.
object
(
self
.
tested_task
,
'apply_async'
)
as
mock_apply_async
:
self
.
tested_task
.
enqueue
(
self
.
site_config
.
site
,
current_datetime
,
3
)
if
is_enabled
:
self
.
assertTrue
(
mock_apply_async
.
called
)
else
:
self
.
assertFalse
(
mock_apply_async
.
called
)
@patch.object
(
tasks
,
'ace'
)
@ddt.data
(
(([
'filtered_org'
],
[],
1
)),
(([],
[
'filtered_org'
],
2
))
)
@ddt.unpack
def
test_site_config
(
self
,
this_org_list
,
other_org_list
,
expected_message_count
,
mock_ace
):
filtered_org
=
'filtered_org'
unfiltered_org
=
'unfiltered_org'
this_config
=
SiteConfigurationFactory
.
create
(
values
=
{
'course_org_filter'
:
this_org_list
})
other_config
=
SiteConfigurationFactory
.
create
(
values
=
{
'course_org_filter'
:
other_org_list
})
for
config
in
(
this_config
,
other_config
):
ScheduleConfigFactory
.
create
(
site
=
config
.
site
)
user1
=
UserFactory
.
create
(
id
=
self
.
tested_task
.
num_bins
)
user2
=
UserFactory
.
create
(
id
=
self
.
tested_task
.
num_bins
*
2
)
current_day
,
offset
,
target_day
=
self
.
_get_dates
()
ScheduleFactory
.
create
(
upgrade_deadline
=
target_day
,
start
=
target_day
,
enrollment__course__org
=
filtered_org
,
enrollment__course__self_paced
=
True
,
enrollment__user
=
user1
,
)
ScheduleFactory
.
create
(
upgrade_deadline
=
target_day
,
start
=
target_day
,
enrollment__course__org
=
unfiltered_org
,
enrollment__course__self_paced
=
True
,
enrollment__user
=
user1
,
)
ScheduleFactory
.
create
(
upgrade_deadline
=
target_day
,
start
=
target_day
,
enrollment__course__org
=
unfiltered_org
,
enrollment__course__self_paced
=
True
,
enrollment__user
=
user2
,
)
with
patch
.
object
(
self
.
tested_task
,
'async_send_task'
)
as
mock_schedule_send
:
self
.
tested_task
.
apply
(
kwargs
=
dict
(
site_id
=
this_config
.
site
.
id
,
target_day_str
=
serialize
(
target_day
),
day_offset
=
offset
,
bin_num
=
0
))
self
.
assertEqual
(
mock_schedule_send
.
apply_async
.
call_count
,
expected_message_count
)
self
.
assertFalse
(
mock_ace
.
send
.
called
)
@ddt.data
(
True
,
False
)
def
test_course_end
(
self
,
has_course_ended
):
user1
=
UserFactory
.
create
(
id
=
self
.
tested_task
.
num_bins
)
current_day
,
offset
,
target_day
=
self
.
_get_dates
()
schedule
=
ScheduleFactory
.
create
(
start
=
target_day
,
upgrade_deadline
=
target_day
,
enrollment__course__self_paced
=
True
,
enrollment__user
=
user1
,
)
schedule
.
enrollment
.
course
.
start
=
current_day
-
datetime
.
timedelta
(
days
=
30
)
end_date_offset
=
-
2
if
has_course_ended
else
2
schedule
.
enrollment
.
course
.
end
=
current_day
+
datetime
.
timedelta
(
days
=
end_date_offset
)
schedule
.
enrollment
.
course
.
save
()
with
patch
.
object
(
self
.
tested_task
,
'async_send_task'
)
as
mock_schedule_send
:
self
.
tested_task
.
apply
(
kwargs
=
dict
(
site_id
=
self
.
site_config
.
site
.
id
,
target_day_str
=
serialize
(
target_day
),
day_offset
=
offset
,
bin_num
=
0
,
))
if
has_course_ended
:
self
.
assertFalse
(
mock_schedule_send
.
apply_async
.
called
)
else
:
self
.
assertTrue
(
mock_schedule_send
.
apply_async
.
called
)
@patch.object
(
tasks
,
'ace'
)
def
test_multiple_enrollments
(
self
,
mock_ace
):
user
=
UserFactory
.
create
()
current_day
,
offset
,
target_day
=
self
.
_get_dates
()
num_courses
=
3
for
course_index
in
range
(
num_courses
):
ScheduleFactory
.
create
(
start
=
target_day
,
upgrade_deadline
=
target_day
,
enrollment__course__self_paced
=
True
,
enrollment__user
=
user
,
enrollment__course__id
=
CourseKey
.
from_string
(
'edX/toy/course{}'
.
format
(
course_index
))
)
course_queries
=
num_courses
if
self
.
has_course_queries
else
0
expected_query_count
=
NUM_QUERIES_FIRST_MATCH
+
course_queries
+
NUM_QUERIES_NO_ORG_LIST
with
self
.
assertNumQueries
(
expected_query_count
,
table_blacklist
=
WAFFLE_TABLES
):
with
patch
.
object
(
self
.
tested_task
,
'async_send_task'
)
as
mock_schedule_send
:
self
.
tested_task
.
apply
(
kwargs
=
dict
(
site_id
=
self
.
site_config
.
site
.
id
,
target_day_str
=
serialize
(
target_day
),
day_offset
=
offset
,
bin_num
=
self
.
_calculate_bin_for_user
(
user
),
))
self
.
assertEqual
(
mock_schedule_send
.
apply_async
.
call_count
,
1
)
self
.
assertFalse
(
mock_ace
.
send
.
called
)
@ddt.data
(
1
,
10
,
100
)
def
test_templates
(
self
,
message_count
):
for
offset
in
self
.
expected_offsets
:
self
.
_assert_template_for_offset
(
offset
,
message_count
)
self
.
clear_caches
()
def
_assert_template_for_offset
(
self
,
offset
,
message_count
):
current_day
,
offset
,
target_day
=
self
.
_get_dates
(
offset
)
user
=
UserFactory
.
create
()
for
course_index
in
range
(
message_count
):
ScheduleFactory
.
create
(
start
=
target_day
,
upgrade_deadline
=
target_day
,
enrollment__course__self_paced
=
True
,
enrollment__user
=
user
,
enrollment__course__id
=
CourseKey
.
from_string
(
'edX/toy/course{}'
.
format
(
course_index
))
)
patch_policies
(
self
,
[
StubPolicy
([
ChannelType
.
PUSH
])])
mock_channel
=
Mock
(
name
=
'test_channel'
,
channel_type
=
ChannelType
.
EMAIL
)
patch_channels
(
self
,
[
mock_channel
])
sent_messages
=
[]
with
self
.
settings
(
TEMPLATES
=
self
.
_get_template_overrides
()):
with
patch
.
object
(
self
.
tested_task
,
'async_send_task'
)
as
mock_schedule_send
:
mock_schedule_send
.
apply_async
=
lambda
args
,
*
_a
,
**
_kw
:
sent_messages
.
append
(
args
)
num_expected_queries
=
NUM_QUERIES_NO_ORG_LIST
+
NUM_QUERIES_FIRST_MATCH
if
self
.
has_course_queries
:
num_expected_queries
+=
message_count
with
self
.
assertNumQueries
(
num_expected_queries
,
table_blacklist
=
WAFFLE_TABLES
):
self
.
tested_task
.
apply
(
kwargs
=
dict
(
site_id
=
self
.
site_config
.
site
.
id
,
target_day_str
=
serialize
(
target_day
),
day_offset
=
offset
,
bin_num
=
self
.
_calculate_bin_for_user
(
user
),
))
self
.
assertEqual
(
len
(
sent_messages
),
1
)
with
self
.
assertNumQueries
(
2
):
for
args
in
sent_messages
:
self
.
deliver_task
(
*
args
)
self
.
assertEqual
(
mock_channel
.
deliver
.
call_count
,
1
)
for
(
_name
,
(
_msg
,
email
),
_kwargs
)
in
mock_channel
.
deliver
.
mock_calls
:
for
template
in
attr
.
astuple
(
email
):
self
.
assertNotIn
(
"TEMPLATE WARNING"
,
template
)
self
.
assertNotIn
(
"{{"
,
template
)
self
.
assertNotIn
(
"}}"
,
template
)
openedx/core/djangoapps/schedules/management/commands/tests/test_
base
.py
→
openedx/core/djangoapps/schedules/management/commands/tests/test_
send_email_base_command
.py
View file @
2c80b1b4
...
...
@@ -4,7 +4,7 @@ from unittest import skipUnless
import
ddt
import
pytz
from
django.conf
import
settings
from
mock
import
patch
from
mock
import
patch
,
DEFAULT
,
Mock
from
openedx.core.djangoapps.schedules.management.commands
import
SendEmailBaseCommand
from
openedx.core.djangoapps.site_configuration.tests.factories
import
SiteFactory
,
SiteConfigurationFactory
...
...
@@ -29,3 +29,18 @@ class TestSendEmailBaseCommand(CacheIsolationTestCase):
datetime
.
datetime
(
2017
,
9
,
29
,
tzinfo
=
pytz
.
UTC
),
None
)
def
test_send_emails
(
self
):
with
patch
.
multiple
(
self
.
command
,
offsets
=
(
1
,
3
,
5
),
enqueue
=
DEFAULT
,
):
arg
=
Mock
(
name
=
'arg'
)
kwarg
=
Mock
(
name
=
'kwarg'
)
self
.
command
.
send_emails
(
arg
,
kwarg
=
kwarg
)
self
.
assertFalse
(
arg
.
called
)
self
.
assertFalse
(
kwarg
.
called
)
for
offset
in
self
.
command
.
offsets
:
self
.
command
.
enqueue
.
assert_any_call
(
offset
,
arg
,
kwarg
=
kwarg
)
openedx/core/djangoapps/schedules/management/commands/tests/test_send_recurring_nudge.py
View file @
2c80b1b4
import
datetime
import
itertools
from
copy
import
deepcopy
from
unittest
import
skipUnless
import
attr
import
ddt
import
pytz
from
django.conf
import
settings
from
edx_ace.channel
import
ChannelType
from
edx_ace.test_utils
import
StubPolicy
,
patch_channels
,
patch_policies
from
edx_ace.utils.date
import
serialize
from
edx_ace.message
import
Message
from
mock
import
Mock
,
patch
from
opaque_keys.edx.keys
import
CourseKey
from
mock
import
patch
from
opaque_keys.edx.locator
import
CourseLocator
import
pytz
from
course_modes.models
import
CourseMode
from
course_modes.tests.factories
import
CourseModeFactory
from
courseware.models
import
DynamicUpgradeDeadlineConfiguration
from
openedx.core.djangoapps.content.course_overviews.tests.factories
import
CourseOverviewFactory
from
openedx.core.djangoapps.schedules
import
resolvers
,
tasks
from
openedx.core.djangoapps.schedules
import
tasks
from
openedx.core.djangoapps.schedules.management.commands
import
send_recurring_nudge
as
nudge
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.waffle_utils.testutils
import
WAFFLE_TABLES
from
openedx.core.djangolib.testing.utils
import
CacheIsolationTestCase
,
skip_unless_lms
,
FilteredQueryCountMixin
from
openedx.core.djangoapps.schedules.management.commands.tests.send_email_base
import
ScheduleSendEmailTestBase
from
openedx.core.djangoapps.schedules.tests.factories
import
ScheduleFactory
from
openedx.core.djangolib.testing.utils
import
skip_unless_lms
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
# 4) Load the non-matching site configurations
NUM_QUERIES_NO_ORG_LIST
=
1
NUM_COURSE_MODES_QUERIES
=
1
@ddt.ddt
@skip_unless_lms
@skipUnless
(
'openedx.core.djangoapps.schedules.apps.SchedulesConfig'
in
settings
.
INSTALLED_APPS
,
"Can't test schedules if the app isn't installed"
)
class
TestSendRecurringNudge
(
FilteredQueryCountMixin
,
CacheIsolationTestCase
):
# pylint: disable=protected-access
ENABLED_CACHES
=
[
'default'
]
def
setUp
(
self
):
super
(
TestSendRecurringNudge
,
self
)
.
setUp
()
ScheduleFactory
.
create
(
start
=
datetime
.
datetime
(
2017
,
8
,
1
,
15
,
44
,
30
,
tzinfo
=
pytz
.
UTC
))
ScheduleFactory
.
create
(
start
=
datetime
.
datetime
(
2017
,
8
,
1
,
17
,
34
,
30
,
tzinfo
=
pytz
.
UTC
))
ScheduleFactory
.
create
(
start
=
datetime
.
datetime
(
2017
,
8
,
2
,
15
,
34
,
30
,
tzinfo
=
pytz
.
UTC
))
site
=
SiteFactory
.
create
()
self
.
site_config
=
SiteConfigurationFactory
.
create
(
site
=
site
)
ScheduleConfigFactory
.
create
(
site
=
self
.
site_config
.
site
)
DynamicUpgradeDeadlineConfiguration
.
objects
.
create
(
enabled
=
True
)
@patch.object
(
nudge
.
Command
,
'async_send_task'
)
def
test_handle
(
self
,
mock_send
):
test_day
=
datetime
.
datetime
(
2017
,
8
,
1
,
tzinfo
=
pytz
.
UTC
)
nudge
.
Command
()
.
handle
(
date
=
'2017-08-01'
,
site_domain_name
=
self
.
site_config
.
site
.
domain
)
for
day
in
(
-
3
,
-
10
):
mock_send
.
enqueue
.
assert_any_call
(
self
.
site_config
.
site
,
test_day
,
day
,
None
)
@patch.object
(
tasks
,
'ace'
)
def
test_resolver_send
(
self
,
mock_ace
):
current_day
=
datetime
.
datetime
(
2017
,
8
,
1
,
tzinfo
=
pytz
.
UTC
)
with
patch
.
object
(
tasks
.
ScheduleRecurringNudge
,
'apply_async'
)
as
mock_apply_async
:
tasks
.
ScheduleRecurringNudge
.
enqueue
(
self
.
site_config
.
site
,
current_day
,
-
3
)
test_day
=
current_day
+
datetime
.
timedelta
(
days
=-
3
)
mock_apply_async
.
assert_any_call
(
(
self
.
site_config
.
site
.
id
,
serialize
(
test_day
),
-
3
,
0
,
None
),
retry
=
False
,
)
mock_apply_async
.
assert_any_call
(
(
self
.
site_config
.
site
.
id
,
serialize
(
test_day
),
-
3
,
resolvers
.
RECURRING_NUDGE_NUM_BINS
-
1
,
None
),
retry
=
False
,
)
self
.
assertFalse
(
mock_ace
.
send
.
called
)
@ddt.data
(
1
,
10
,
100
)
@patch.object
(
tasks
,
'ace'
)
@patch.object
(
tasks
.
ScheduleRecurringNudge
,
'async_send_task'
)
def
test_schedule_bin
(
self
,
schedule_count
,
mock_schedule_send
,
mock_ace
):
schedules
=
[
ScheduleFactory
.
create
(
start
=
datetime
.
datetime
(
2017
,
8
,
3
,
18
,
44
,
30
,
tzinfo
=
pytz
.
UTC
),
enrollment__course__id
=
CourseLocator
(
'edX'
,
'toy'
,
'Bin'
)
)
for
i
in
range
(
schedule_count
)
]
bins_in_use
=
frozenset
((
s
.
enrollment
.
user
.
id
%
resolvers
.
RECURRING_NUDGE_NUM_BINS
)
for
s
in
schedules
)
test_datetime
=
datetime
.
datetime
(
2017
,
8
,
3
,
18
,
tzinfo
=
pytz
.
UTC
)
test_datetime_str
=
serialize
(
test_datetime
)
for
b
in
range
(
resolvers
.
RECURRING_NUDGE_NUM_BINS
):
expected_queries
=
NUM_QUERIES_NO_MATCHING_SCHEDULES
+
NUM_QUERIES_NO_ORG_LIST
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
.
ScheduleRecurringNudge
.
apply
(
kwargs
=
dict
(
site_id
=
self
.
site_config
.
site
.
id
,
target_day_str
=
test_datetime_str
,
day_offset
=-
3
,
bin_num
=
b
,
))
self
.
assertEqual
(
mock_schedule_send
.
apply_async
.
call_count
,
schedule_count
)
self
.
assertFalse
(
mock_ace
.
send
.
called
)
@patch.object
(
tasks
.
ScheduleRecurringNudge
,
'async_send_task'
)
def
test_no_course_overview
(
self
,
mock_schedule_send
):
schedule
=
ScheduleFactory
.
create
(
start
=
datetime
.
datetime
(
2017
,
8
,
3
,
20
,
34
,
30
,
tzinfo
=
pytz
.
UTC
),
enrollment__user
=
UserFactory
.
create
(),
)
schedule
.
enrollment
.
course_id
=
CourseKey
.
from_string
(
'edX/toy/Not_2012_Fall'
)
schedule
.
enrollment
.
save
()
test_datetime
=
datetime
.
datetime
(
2017
,
8
,
3
,
20
,
tzinfo
=
pytz
.
UTC
)
test_datetime_str
=
serialize
(
test_datetime
)
for
b
in
range
(
resolvers
.
RECURRING_NUDGE_NUM_BINS
):
with
self
.
assertNumQueries
(
NUM_QUERIES_NO_MATCHING_SCHEDULES
+
NUM_QUERIES_NO_ORG_LIST
,
table_blacklist
=
WAFFLE_TABLES
):
tasks
.
ScheduleRecurringNudge
.
apply
(
kwargs
=
dict
(
site_id
=
self
.
site_config
.
site
.
id
,
target_day_str
=
test_datetime_str
,
day_offset
=-
3
,
bin_num
=
b
))
# There is no database constraint that enforces that enrollment.course_id points
# to a valid CourseOverview object. However, in that case, schedules isn't going
# to attempt to address it, and will instead simply skip those users.
# This happens 'transparently' because django generates an inner-join between
# enrollment and course_overview, and thus will skip any rows where course_overview
# is null.
self
.
assertEqual
(
mock_schedule_send
.
apply_async
.
call_count
,
0
)
@patch.object
(
tasks
.
ScheduleRecurringNudge
,
'async_send_task'
)
def
test_send_after_course_end
(
self
,
mock_schedule_send
):
user1
=
UserFactory
.
create
(
id
=
resolvers
.
RECURRING_NUDGE_NUM_BINS
)
schedule_start
=
datetime
.
datetime
(
2017
,
8
,
3
,
20
,
34
,
30
,
tzinfo
=
pytz
.
UTC
)
day_command_is_run
=
schedule_start
+
datetime
.
timedelta
(
days
=
3
)
schedule
=
ScheduleFactory
.
create
(
start
=
schedule_start
,
enrollment__user
=
user1
,
)
schedule
.
enrollment
.
course
.
start
=
schedule_start
-
datetime
.
timedelta
(
days
=
30
)
schedule
.
enrollment
.
course
.
end
=
day_command_is_run
-
datetime
.
timedelta
(
days
=
1
)
schedule
.
enrollment
.
course
.
save
()
test_datetime
=
datetime
.
datetime
(
2017
,
8
,
3
,
20
,
tzinfo
=
pytz
.
UTC
)
test_datetime_str
=
serialize
(
test_datetime
)
tasks
.
ScheduleRecurringNudge
.
apply
(
kwargs
=
dict
(
site_id
=
self
.
site_config
.
site
.
id
,
target_day_str
=
test_datetime_str
,
day_offset
=-
3
,
bin_num
=
0
,
))
self
.
assertFalse
(
mock_schedule_send
.
apply_async
.
called
)
@patch.object
(
tasks
,
'ace'
)
def
test_delivery_disabled
(
self
,
mock_ace
):
ScheduleConfigFactory
.
create
(
site
=
self
.
site_config
.
site
,
deliver_recurring_nudge
=
False
)
mock_msg
=
Mock
()
tasks
.
_recurring_nudge_schedule_send
(
self
.
site_config
.
site
.
id
,
mock_msg
)
self
.
assertFalse
(
mock_ace
.
send
.
called
)
@patch.object
(
tasks
,
'ace'
)
@patch.object
(
tasks
.
ScheduleUpgradeReminder
,
'apply_async'
)
def
test_enqueue_disabled
(
self
,
mock_ace
,
mock_apply_async
):
ScheduleConfigFactory
.
create
(
site
=
self
.
site_config
.
site
,
enqueue_recurring_nudge
=
False
)
current_datetime
=
datetime
.
datetime
(
2017
,
8
,
1
,
tzinfo
=
pytz
.
UTC
)
tasks
.
ScheduleRecurringNudge
.
enqueue
(
self
.
site_config
.
site
,
current_datetime
,
3
)
self
.
assertFalse
(
mock_apply_async
.
called
)
self
.
assertFalse
(
mock_ace
.
send
.
called
)
@patch.object
(
tasks
,
'ace'
)
@patch.object
(
tasks
.
ScheduleRecurringNudge
,
'async_send_task'
)
@ddt.data
(
(([
'filtered_org'
],
[],
1
)),
(([],
[
'filtered_org'
],
2
))
)
@ddt.unpack
def
test_site_config
(
self
,
this_org_list
,
other_org_list
,
expected_message_count
,
mock_schedule_send
,
mock_ace
):
filtered_org
=
'filtered_org'
unfiltered_org
=
'unfiltered_org'
this_config
=
SiteConfigurationFactory
.
create
(
values
=
{
'course_org_filter'
:
this_org_list
})
other_config
=
SiteConfigurationFactory
.
create
(
values
=
{
'course_org_filter'
:
other_org_list
})
for
config
in
(
this_config
,
other_config
):
ScheduleConfigFactory
.
create
(
site
=
config
.
site
)
user1
=
UserFactory
.
create
(
id
=
resolvers
.
RECURRING_NUDGE_NUM_BINS
)
user2
=
UserFactory
.
create
(
id
=
resolvers
.
RECURRING_NUDGE_NUM_BINS
*
2
)
ScheduleFactory
.
create
(
start
=
datetime
.
datetime
(
2017
,
8
,
3
,
17
,
44
,
30
,
tzinfo
=
pytz
.
UTC
),
enrollment__course__org
=
filtered_org
,
enrollment__user
=
user1
,
)
ScheduleFactory
.
create
(
start
=
datetime
.
datetime
(
2017
,
8
,
3
,
17
,
44
,
30
,
tzinfo
=
pytz
.
UTC
),
enrollment__course__org
=
unfiltered_org
,
enrollment__user
=
user1
,
)
ScheduleFactory
.
create
(
start
=
datetime
.
datetime
(
2017
,
8
,
3
,
17
,
44
,
30
,
tzinfo
=
pytz
.
UTC
),
enrollment__course__org
=
unfiltered_org
,
enrollment__user
=
user2
,
)
test_datetime
=
datetime
.
datetime
(
2017
,
8
,
3
,
17
,
tzinfo
=
pytz
.
UTC
)
test_datetime_str
=
serialize
(
test_datetime
)
expected_queries
=
NUM_QUERIES_WITH_MATCHES
if
not
this_org_list
:
expected_queries
+=
NUM_QUERIES_NO_ORG_LIST
with
self
.
assertNumQueries
(
expected_queries
,
table_blacklist
=
WAFFLE_TABLES
):
tasks
.
ScheduleRecurringNudge
.
apply
(
kwargs
=
dict
(
site_id
=
this_config
.
site
.
id
,
target_day_str
=
test_datetime_str
,
day_offset
=-
3
,
bin_num
=
0
))
self
.
assertEqual
(
mock_schedule_send
.
apply_async
.
call_count
,
expected_message_count
)
self
.
assertFalse
(
mock_ace
.
send
.
called
)
@patch.object
(
tasks
,
'ace'
)
@patch.object
(
tasks
.
ScheduleRecurringNudge
,
'async_send_task'
)
def
test_multiple_enrollments
(
self
,
mock_schedule_send
,
mock_ace
):
user
=
UserFactory
.
create
()
schedules
=
[
ScheduleFactory
.
create
(
start
=
datetime
.
datetime
(
2017
,
8
,
3
,
19
,
44
,
30
,
tzinfo
=
pytz
.
UTC
),
enrollment__user
=
user
,
enrollment__course__id
=
CourseLocator
(
'edX'
,
'toy'
,
'Course{}'
.
format
(
course_num
))
)
for
course_num
in
(
1
,
2
,
3
)
]
test_datetime
=
datetime
.
datetime
(
2017
,
8
,
3
,
19
,
44
,
30
,
tzinfo
=
pytz
.
UTC
)
test_datetime_str
=
serialize
(
test_datetime
)
with
self
.
assertNumQueries
(
NUM_QUERIES_WITH_MATCHES
+
NUM_QUERIES_NO_ORG_LIST
,
table_blacklist
=
WAFFLE_TABLES
):
tasks
.
ScheduleRecurringNudge
.
apply
(
kwargs
=
dict
(
site_id
=
self
.
site_config
.
site
.
id
,
target_day_str
=
test_datetime_str
,
day_offset
=-
3
,
bin_num
=
user
.
id
%
resolvers
.
RECURRING_NUDGE_NUM_BINS
,
))
self
.
assertEqual
(
mock_schedule_send
.
apply_async
.
call_count
,
1
)
self
.
assertFalse
(
mock_ace
.
send
.
called
)
@ddt.data
(
*
itertools
.
product
((
1
,
10
,
100
),
(
-
3
,
-
10
)))
@ddt.unpack
def
test_templates
(
self
,
message_count
,
day
):
user
=
UserFactory
.
create
()
schedules
=
[
ScheduleFactory
.
create
(
start
=
datetime
.
datetime
(
2017
,
8
,
3
,
19
,
44
,
30
,
tzinfo
=
pytz
.
UTC
),
enrollment__user
=
user
,
enrollment__course__id
=
CourseLocator
(
'edX'
,
'toy'
,
'Course{}'
.
format
(
course_num
))
)
for
course_num
in
range
(
message_count
)
]
test_datetime
=
datetime
.
datetime
(
2017
,
8
,
3
,
19
,
tzinfo
=
pytz
.
UTC
)
test_datetime_str
=
serialize
(
test_datetime
)
class
TestSendRecurringNudge
(
ScheduleSendEmailTestBase
):
__test__
=
True
patch_policies
(
self
,
[
StubPolicy
([
ChannelType
.
PUSH
])])
mock_channel
=
Mock
(
name
=
'test_channel'
,
channel_type
=
ChannelType
.
EMAIL
)
patch_channels
(
self
,
[
mock_channel
])
sent_messages
=
[]
with
self
.
settings
(
TEMPLATES
=
self
.
_get_template_overrides
()):
with
patch
.
object
(
tasks
.
ScheduleRecurringNudge
,
'async_send_task'
)
as
mock_schedule_send
:
mock_schedule_send
.
apply_async
=
lambda
args
,
*
_a
,
**
_kw
:
sent_messages
.
append
(
args
)
with
self
.
assertNumQueries
(
NUM_QUERIES_WITH_MATCHES
+
NUM_QUERIES_NO_ORG_LIST
,
table_blacklist
=
WAFFLE_TABLES
):
tasks
.
ScheduleRecurringNudge
.
apply
(
kwargs
=
dict
(
site_id
=
self
.
site_config
.
site
.
id
,
target_day_str
=
test_datetime_str
,
day_offset
=
day
,
bin_num
=
self
.
_calculate_bin_for_user
(
user
),
))
self
.
assertEqual
(
len
(
sent_messages
),
1
)
# Load the site
# Check the schedule config
with
self
.
assertNumQueries
(
2
):
for
args
in
sent_messages
:
tasks
.
_recurring_nudge_schedule_send
(
*
args
)
self
.
assertEqual
(
mock_channel
.
deliver
.
call_count
,
1
)
for
(
_name
,
(
_msg
,
email
),
_kwargs
)
in
mock_channel
.
deliver
.
mock_calls
:
for
template
in
attr
.
astuple
(
email
):
self
.
assertNotIn
(
"TEMPLATE WARNING"
,
template
)
self
.
assertNotIn
(
"{{"
,
template
)
self
.
assertNotIn
(
"}}"
,
template
)
# pylint: disable=protected-access
tested_task
=
tasks
.
ScheduleRecurringNudge
deliver_task
=
tasks
.
_recurring_nudge_schedule_send
tested_command
=
nudge
.
Command
deliver_config
=
'deliver_recurring_nudge'
enqueue_config
=
'enqueue_recurring_nudge'
expected_offsets
=
(
-
3
,
-
10
)
def
test_user_in_course_with_verified_coursemode_receives_upsell
(
self
):
user
=
UserFactory
.
create
()
...
...
@@ -344,8 +64,8 @@ class TestSendRecurringNudge(FilteredQueryCountMixin, CacheIsolationTestCase):
user
,
schedule
.
enrollment
.
course
.
org
]
sent_messages
=
self
.
_stub_sender_and_collect_sent_messages
(
bin_task
=
tasks
.
ScheduleRecurringNudge
,
stubbed_send_task
=
patch
.
object
(
tasks
.
ScheduleRecurringNudge
,
'async_send_task'
),
sent_messages
=
self
.
_stub_sender_and_collect_sent_messages
(
bin_task
=
self
.
tested_task
,
stubbed_send_task
=
patch
.
object
(
self
.
tested_task
,
'async_send_task'
),
bin_task_params
=
bin_task_parameters
)
self
.
assertEqual
(
len
(
sent_messages
),
1
)
...
...
@@ -376,8 +96,8 @@ class TestSendRecurringNudge(FilteredQueryCountMixin, CacheIsolationTestCase):
user
,
schedule
.
enrollment
.
course
.
org
]
sent_messages
=
self
.
_stub_sender_and_collect_sent_messages
(
bin_task
=
tasks
.
ScheduleRecurringNudge
,
stubbed_send_task
=
patch
.
object
(
tasks
.
ScheduleRecurringNudge
,
'async_send_task'
),
sent_messages
=
self
.
_stub_sender_and_collect_sent_messages
(
bin_task
=
self
.
tested_task
,
stubbed_send_task
=
patch
.
object
(
self
.
tested_task
,
'async_send_task'
),
bin_task_params
=
bin_task_parameters
)
self
.
assertEqual
(
len
(
sent_messages
),
1
)
...
...
@@ -415,8 +135,8 @@ class TestSendRecurringNudge(FilteredQueryCountMixin, CacheIsolationTestCase):
user
,
schedule
.
enrollment
.
course
.
org
]
sent_messages
=
self
.
_stub_sender_and_collect_sent_messages
(
bin_task
=
tasks
.
ScheduleRecurringNudge
,
stubbed_send_task
=
patch
.
object
(
tasks
.
ScheduleRecurringNudge
,
'async_send_task'
),
sent_messages
=
self
.
_stub_sender_and_collect_sent_messages
(
bin_task
=
self
.
tested_task
,
stubbed_send_task
=
patch
.
object
(
self
.
tested_task
,
'async_send_task'
),
bin_task_params
=
bin_task_parameters
)
self
.
assertEqual
(
len
(
sent_messages
),
1
)
...
...
@@ -440,15 +160,6 @@ class TestSendRecurringNudge(FilteredQueryCountMixin, CacheIsolationTestCase):
return
sent_messages
def
_get_template_overrides
(
self
):
templates_override
=
deepcopy
(
settings
.
TEMPLATES
)
templates_override
[
0
][
'OPTIONS'
][
'string_if_invalid'
]
=
"TEMPLATE WARNING - MISSING VARIABLE [
%
s]"
return
templates_override
def
_calculate_bin_for_user
(
self
,
user
):
return
user
.
id
%
resolvers
.
RECURRING_NUDGE_NUM_BINS
def
_contains_upsell_attribute
(
self
,
msg_attr
):
msg
=
Message
.
from_string
(
msg_attr
)
tmp
=
msg
.
context
[
"show_upsell"
]
return
msg
.
context
[
"show_upsell"
]
openedx/core/djangoapps/schedules/management/commands/tests/test_send_upgrade_reminder.py
View file @
2c80b1b4
import
datetime
from
copy
import
deepcopy
import
logging
from
unittest
import
skipUnless
import
attr
import
ddt
import
pytz
from
django.conf
import
settings
from
edx_ace
import
Message
from
freezegun
import
freeze_time
from
edx_ace.channel
import
ChannelType
from
edx_ace.test_utils
import
StubPolicy
,
patch_channels
,
patch_policies
from
edx_ace.utils.date
import
serialize
from
mock
import
Mock
,
patch
from
opaque_keys.edx.keys
import
CourseKey
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.content.course_overviews.models
import
CourseOverview
from
openedx.core.djangoapps.schedules
import
resolvers
,
tasks
from
openedx.core.djangoapps.schedules
import
tasks
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.site_configuration.tests.factories
import
SiteConfigurationFactory
,
SiteFactory
from
openedx.core.djangoapps.waffle_utils.testutils
import
WAFFLE_TABLES
from
openedx.core.djangoapps.schedules.management.commands.tests.send_email_base
import
ScheduleSendEmailTestBase
from
openedx.core.djangoapps.schedules.tests.factories
import
ScheduleFactory
from
openedx.core.djangolib.testing.utils
import
skip_unless_lms
from
student.tests.factories
import
UserFactory
from
xmodule.modulestore.tests.django_utils
import
SharedModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
SITE_QUERY
=
1
SCHEDULES_QUERY
=
1
COURSE_MODES_QUERY
=
1
GLOBAL_DEADLINE_SWITCH_QUERY
=
1
COMMERCE_CONFIG_QUERY
=
1
NUM_QUERIES_NO_ORG_LIST
=
1
NUM_QUERIES_NO_MATCHING_SCHEDULES
=
SITE_QUERY
+
SCHEDULES_QUERY
NUM_QUERIES_WITH_MATCHES
=
(
NUM_QUERIES_NO_MATCHING_SCHEDULES
+
COURSE_MODES_QUERY
)
NUM_QUERIES_FIRST_MATCH
=
(
NUM_QUERIES_WITH_MATCHES
+
GLOBAL_DEADLINE_SWITCH_QUERY
+
COMMERCE_CONFIG_QUERY
)
LOG
=
logging
.
getLogger
(
__name__
)
...
...
@@ -58,378 +24,58 @@ LOG = logging.getLogger(__name__)
@skip_unless_lms
@skipUnless
(
'openedx.core.djangoapps.schedules.apps.SchedulesConfig'
in
settings
.
INSTALLED_APPS
,
"Can't test schedules if the app isn't installed"
)
@freeze_time
(
'2017-08-01 00:00:00'
,
tz_offset
=
0
,
tick
=
True
)
class
TestUpgradeReminder
(
SharedModuleStoreTestCase
):
ENABLED_CACHES
=
[
'default'
]
@classmethod
def
setUpClass
(
cls
):
super
(
TestUpgradeReminder
,
cls
)
.
setUpClass
()
cls
.
course
=
CourseFactory
.
create
(
org
=
'edX'
,
number
=
'test'
,
display_name
=
'Test Course'
,
self_paced
=
True
,
start
=
datetime
.
datetime
.
now
(
pytz
.
UTC
)
-
datetime
.
timedelta
(
days
=
30
),
)
cls
.
course_overview
=
CourseOverview
.
get_from_id
(
cls
.
course
.
id
)
def
setUp
(
self
):
super
(
TestUpgradeReminder
,
self
)
.
setUp
()
CourseModeFactory
(
course_id
=
self
.
course
.
id
,
mode_slug
=
CourseMode
.
VERIFIED
,
expiration_datetime
=
datetime
.
datetime
.
now
(
pytz
.
UTC
)
+
datetime
.
timedelta
(
days
=
30
),
)
ScheduleFactory
.
create
(
upgrade_deadline
=
datetime
.
datetime
(
2017
,
8
,
1
,
15
,
44
,
30
,
tzinfo
=
pytz
.
UTC
))
ScheduleFactory
.
create
(
upgrade_deadline
=
datetime
.
datetime
(
2017
,
8
,
1
,
17
,
34
,
30
,
tzinfo
=
pytz
.
UTC
))
ScheduleFactory
.
create
(
upgrade_deadline
=
datetime
.
datetime
(
2017
,
8
,
2
,
15
,
34
,
30
,
tzinfo
=
pytz
.
UTC
))
site
=
SiteFactory
.
create
()
self
.
site_config
=
SiteConfigurationFactory
.
create
(
site
=
site
)
ScheduleConfigFactory
.
create
(
site
=
self
.
site_config
.
site
)
DynamicUpgradeDeadlineConfiguration
.
objects
.
create
(
enabled
=
True
)
@patch.object
(
reminder
.
Command
,
'async_send_task'
)
def
test_handle
(
self
,
mock_send
):
test_day
=
datetime
.
datetime
(
2017
,
8
,
1
,
tzinfo
=
pytz
.
UTC
)
reminder
.
Command
()
.
handle
(
date
=
'2017-08-01'
,
site_domain_name
=
self
.
site_config
.
site
.
domain
)
mock_send
.
enqueue
.
assert_called_with
(
self
.
site_config
.
site
,
test_day
,
2
,
None
)
@patch.object
(
tasks
,
'ace'
)
def
test_resolver_send
(
self
,
mock_ace
):
current_day
=
datetime
.
datetime
(
2017
,
8
,
1
,
tzinfo
=
pytz
.
UTC
)
test_day
=
current_day
+
datetime
.
timedelta
(
days
=
2
)
ScheduleFactory
.
create
(
upgrade_deadline
=
datetime
.
datetime
(
2017
,
8
,
3
,
15
,
34
,
30
,
tzinfo
=
pytz
.
UTC
))
with
patch
.
object
(
tasks
.
ScheduleUpgradeReminder
,
'apply_async'
)
as
mock_apply_async
:
tasks
.
ScheduleUpgradeReminder
.
enqueue
(
self
.
site_config
.
site
,
current_day
,
2
)
mock_apply_async
.
assert_any_call
(
(
self
.
site_config
.
site
.
id
,
serialize
(
test_day
),
2
,
0
,
None
),
retry
=
False
,
)
mock_apply_async
.
assert_any_call
(
(
self
.
site_config
.
site
.
id
,
serialize
(
test_day
),
2
,
resolvers
.
UPGRADE_REMINDER_NUM_BINS
-
1
,
None
),
retry
=
False
,
)
self
.
assertFalse
(
mock_ace
.
send
.
called
)
@ddt.data
(
1
,
10
,
100
)
@patch.object
(
tasks
,
'ace'
)
@patch.object
(
tasks
.
ScheduleUpgradeReminder
,
'async_send_task'
)
def
test_schedule_bin
(
self
,
schedule_count
,
mock_schedule_send
,
mock_ace
):
upgrade_deadline
=
datetime
.
datetime
.
now
(
pytz
.
UTC
)
+
datetime
.
timedelta
(
days
=
2
)
schedules
=
[
ScheduleFactory
.
create
(
upgrade_deadline
=
upgrade_deadline
,
enrollment__course
=
self
.
course_overview
,
)
for
i
in
range
(
schedule_count
)
]
bins_in_use
=
frozenset
((
self
.
_calculate_bin_for_user
(
s
.
enrollment
.
user
))
for
s
in
schedules
)
is_first_match
=
True
course_switch_queries
=
len
(
set
(
s
.
enrollment
.
course
.
id
for
s
in
schedules
))
org_switch_queries
=
len
(
set
(
s
.
enrollment
.
course
.
id
.
org
for
s
in
schedules
))
test_datetime
=
upgrade_deadline
test_datetime_str
=
serialize
(
test_datetime
)
for
b
in
range
(
resolvers
.
UPGRADE_REMINDER_NUM_BINS
):
LOG
.
debug
(
'Running bin
%
d'
,
b
)
expected_queries
=
NUM_QUERIES_NO_MATCHING_SCHEDULES
if
b
in
bins_in_use
:
if
is_first_match
:
expected_queries
=
(
# Since this is the first match, we need to cache all of the config models, so we run a query
# for each of those...
NUM_QUERIES_FIRST_MATCH
+
course_switch_queries
+
org_switch_queries
)
is_first_match
=
False
else
:
expected_queries
=
NUM_QUERIES_WITH_MATCHES
expected_queries
+=
NUM_QUERIES_NO_ORG_LIST
with
self
.
assertNumQueries
(
expected_queries
,
table_blacklist
=
WAFFLE_TABLES
):
tasks
.
ScheduleUpgradeReminder
.
apply
(
kwargs
=
dict
(
site_id
=
self
.
site_config
.
site
.
id
,
target_day_str
=
test_datetime_str
,
day_offset
=
2
,
bin_num
=
b
,
))
self
.
assertEqual
(
mock_schedule_send
.
apply_async
.
call_count
,
schedule_count
)
self
.
assertFalse
(
mock_ace
.
send
.
called
)
@patch.object
(
tasks
.
ScheduleUpgradeReminder
,
'async_send_task'
)
def
test_no_course_overview
(
self
,
mock_schedule_send
):
schedule
=
ScheduleFactory
.
create
(
upgrade_deadline
=
datetime
.
datetime
(
2017
,
8
,
3
,
20
,
34
,
30
,
tzinfo
=
pytz
.
UTC
),
)
schedule
.
enrollment
.
course_id
=
CourseKey
.
from_string
(
'edX/toy/Not_2012_Fall'
)
schedule
.
enrollment
.
save
()
test_datetime
=
datetime
.
datetime
(
2017
,
8
,
3
,
20
,
tzinfo
=
pytz
.
UTC
)
test_datetime_str
=
serialize
(
test_datetime
)
for
b
in
range
(
resolvers
.
UPGRADE_REMINDER_NUM_BINS
):
with
self
.
assertNumQueries
(
NUM_QUERIES_NO_MATCHING_SCHEDULES
+
NUM_QUERIES_NO_ORG_LIST
,
table_blacklist
=
WAFFLE_TABLES
):
tasks
.
ScheduleUpgradeReminder
.
apply
(
kwargs
=
dict
(
site_id
=
self
.
site_config
.
site
.
id
,
target_day_str
=
test_datetime_str
,
day_offset
=
2
,
bin_num
=
b
,
))
# There is no database constraint that enforces that enrollment.course_id points
# to a valid CourseOverview object. However, in that case, schedules isn't going
# to attempt to address it, and will instead simply skip those users.
# This happens 'transparently' because django generates an inner-join between
# enrollment and course_overview, and thus will skip any rows where course_overview
# is null.
self
.
assertEqual
(
mock_schedule_send
.
apply_async
.
call_count
,
0
)
@patch.object
(
tasks
,
'ace'
)
def
test_delivery_disabled
(
self
,
mock_ace
):
ScheduleConfigFactory
.
create
(
site
=
self
.
site_config
.
site
,
deliver_upgrade_reminder
=
False
)
mock_msg
=
Mock
()
tasks
.
_upgrade_reminder_schedule_send
(
self
.
site_config
.
site
.
id
,
mock_msg
)
self
.
assertFalse
(
mock_ace
.
send
.
called
)
@patch.object
(
tasks
,
'ace'
)
@patch.object
(
tasks
.
ScheduleUpgradeReminder
,
'apply_async'
)
def
test_enqueue_disabled
(
self
,
mock_ace
,
mock_apply_async
):
ScheduleConfigFactory
.
create
(
site
=
self
.
site_config
.
site
,
enqueue_upgrade_reminder
=
False
)
current_day
=
datetime
.
datetime
(
2017
,
8
,
1
,
tzinfo
=
pytz
.
UTC
)
tasks
.
ScheduleUpgradeReminder
.
enqueue
(
self
.
site_config
.
site
,
current_day
,
day_offset
=
3
,
)
self
.
assertFalse
(
mock_apply_async
.
called
)
self
.
assertFalse
(
mock_ace
.
send
.
called
)
@patch.object
(
tasks
,
'ace'
)
@patch.object
(
tasks
.
ScheduleUpgradeReminder
,
'async_send_task'
)
@ddt.data
(
(([
'filtered_org'
],
[],
1
)),
(([],
[
'filtered_org'
],
2
))
)
@ddt.unpack
def
test_site_config
(
self
,
this_org_list
,
other_org_list
,
expected_message_count
,
mock_schedule_send
,
mock_ace
):
filtered_org
=
'filtered_org'
unfiltered_org
=
'unfiltered_org'
this_config
=
SiteConfigurationFactory
.
create
(
values
=
{
'course_org_filter'
:
this_org_list
})
other_config
=
SiteConfigurationFactory
.
create
(
values
=
{
'course_org_filter'
:
other_org_list
})
for
config
in
(
this_config
,
other_config
):
ScheduleConfigFactory
.
create
(
site
=
config
.
site
)
user1
=
UserFactory
.
create
(
id
=
resolvers
.
UPGRADE_REMINDER_NUM_BINS
)
user2
=
UserFactory
.
create
(
id
=
resolvers
.
UPGRADE_REMINDER_NUM_BINS
*
2
)
ScheduleFactory
.
create
(
upgrade_deadline
=
datetime
.
datetime
(
2017
,
8
,
3
,
17
,
44
,
30
,
tzinfo
=
pytz
.
UTC
),
enrollment__course__org
=
filtered_org
,
enrollment__course__self_paced
=
True
,
enrollment__user
=
user1
,
)
ScheduleFactory
.
create
(
upgrade_deadline
=
datetime
.
datetime
(
2017
,
8
,
3
,
17
,
44
,
30
,
tzinfo
=
pytz
.
UTC
),
enrollment__course__org
=
unfiltered_org
,
enrollment__course__self_paced
=
True
,
enrollment__user
=
user1
,
)
ScheduleFactory
.
create
(
upgrade_deadline
=
datetime
.
datetime
(
2017
,
8
,
3
,
17
,
44
,
30
,
tzinfo
=
pytz
.
UTC
),
enrollment__course__org
=
unfiltered_org
,
enrollment__course__self_paced
=
True
,
enrollment__user
=
user2
,
)
class
TestUpgradeReminder
(
ScheduleSendEmailTestBase
):
__test__
=
True
test_datetime
=
datetime
.
datetime
(
2017
,
8
,
3
,
17
,
tzinfo
=
pytz
.
UTC
)
test_datetime_str
=
serialize
(
test_datetime
)
tested_task
=
tasks
.
ScheduleUpgradeReminder
deliver_task
=
tasks
.
_upgrade_reminder_schedule_send
tested_command
=
reminder
.
Command
deliver_config
=
'deliver_upgrade_reminder'
enqueue_config
=
'enqueue_upgrade_reminder'
expected_offsets
=
(
2
,)
course_switch_queries
=
1
org_switch_queries
=
1
expected_queries
=
NUM_QUERIES_FIRST_MATCH
+
course_switch_queries
+
org_switch_queries
if
not
this_org_list
:
expected_queries
+=
NUM_QUERIES_NO_ORG_LIST
with
self
.
assertNumQueries
(
expected_queries
,
table_blacklist
=
WAFFLE_TABLES
):
tasks
.
ScheduleUpgradeReminder
.
apply
(
kwargs
=
dict
(
site_id
=
this_config
.
site
.
id
,
target_day_str
=
test_datetime_str
,
day_offset
=-
3
,
bin_num
=
0
))
self
.
assertEqual
(
mock_schedule_send
.
apply_async
.
call_count
,
expected_message_count
)
self
.
assertFalse
(
mock_ace
.
send
.
called
)
has_course_queries
=
True
@ddt.data
(
True
,
False
)
@patch.object
(
tasks
,
'ace'
)
@patch.object
(
tasks
.
ScheduleUpgradeReminder
,
'async_send_task'
)
def
test_multiple_enrollments
(
self
,
mock_schedule_send
,
mock_ace
):
user
=
UserFactory
.
create
()
schedules
=
[
def
test_verified_learner
(
self
,
is_verified
,
mock_ace
):
user
=
UserFactory
.
create
(
id
=
self
.
tested_task
.
num_bins
)
current_day
,
offset
,
target_day
=
self
.
_get_dates
()
ScheduleFactory
.
create
(
upgrade_deadline
=
datetime
.
datetime
(
2017
,
8
,
3
,
19
,
44
,
30
,
tzinfo
=
pytz
.
UTC
),
enrollment__user
=
user
,
upgrade_deadline
=
target_day
,
enrollment__course__self_paced
=
True
,
enrollment__course__id
=
CourseLocator
(
'edX'
,
'toy'
,
'Course{}'
.
format
(
course_num
))
)
for
course_num
in
(
1
,
2
,
3
)
]
course_switch_queries
=
len
(
set
(
s
.
enrollment
.
course
.
id
for
s
in
schedules
))
org_switch_queries
=
len
(
set
(
s
.
enrollment
.
course
.
id
.
org
for
s
in
schedules
))
test_datetime
=
datetime
.
datetime
(
2017
,
8
,
3
,
19
,
44
,
30
,
tzinfo
=
pytz
.
UTC
)
test_datetime_str
=
serialize
(
test_datetime
)
expected_query_count
=
(
NUM_QUERIES_FIRST_MATCH
+
course_switch_queries
+
org_switch_queries
+
NUM_QUERIES_NO_ORG_LIST
)
with
self
.
assertNumQueries
(
expected_query_count
,
table_blacklist
=
WAFFLE_TABLES
):
tasks
.
ScheduleUpgradeReminder
.
apply
(
kwargs
=
dict
(
site_id
=
self
.
site_config
.
site
.
id
,
target_day_str
=
test_datetime_str
,
day_offset
=
2
,
bin_num
=
self
.
_calculate_bin_for_user
(
user
),
))
self
.
assertEqual
(
mock_schedule_send
.
apply_async
.
call_count
,
1
)
self
.
assertFalse
(
mock_ace
.
send
.
called
)
@ddt.data
(
1
,
10
,
100
)
def
test_templates
(
self
,
message_count
):
now
=
datetime
.
datetime
.
now
(
pytz
.
UTC
)
future_datetime
=
now
+
datetime
.
timedelta
(
days
=
21
)
user
=
UserFactory
.
create
()
schedules
=
[
ScheduleFactory
.
create
(
upgrade_deadline
=
future_datetime
,
enrollment__user
=
user
,
enrollment__course__self_paced
=
True
,
enrollment__course__end
=
future_datetime
+
datetime
.
timedelta
(
days
=
30
),
enrollment__course__id
=
CourseLocator
(
'edX'
,
'toy'
,
'Course{}'
.
format
(
course_num
))
)
for
course_num
in
range
(
message_count
)
]
for
schedule
in
schedules
:
CourseModeFactory
(
course_id
=
schedule
.
enrollment
.
course
.
id
,
mode_slug
=
CourseMode
.
VERIFIED
,
expiration_datetime
=
future_datetime
)
course_switch_queries
=
len
(
set
(
s
.
enrollment
.
course
.
id
for
s
in
schedules
))
org_switch_queries
=
len
(
set
(
s
.
enrollment
.
course
.
id
.
org
for
s
in
schedules
))
test_datetime
=
future_datetime
test_datetime_str
=
serialize
(
test_datetime
)
patch_policies
(
self
,
[
StubPolicy
([
ChannelType
.
PUSH
])])
mock_channel
=
Mock
(
name
=
'test_channel'
,
channel_type
=
ChannelType
.
EMAIL
)
patch_channels
(
self
,
[
mock_channel
])
sent_messages
=
[]
with
self
.
settings
(
TEMPLATES
=
self
.
_get_template_overrides
()):
with
patch
.
object
(
tasks
.
ScheduleUpgradeReminder
,
'async_send_task'
)
as
mock_schedule_send
:
mock_schedule_send
.
apply_async
=
lambda
args
,
*
_a
,
**
_kw
:
sent_messages
.
append
(
args
)
# we execute one query per course to see if it's opted out of dynamic upgrade deadlines
num_expected_queries
=
(
NUM_QUERIES_FIRST_MATCH
+
NUM_QUERIES_NO_ORG_LIST
+
course_switch_queries
+
org_switch_queries
enrollment__mode
=
CourseMode
.
VERIFIED
if
is_verified
else
CourseMode
.
AUDIT
,
)
with
self
.
assertNumQueries
(
num_expected_queries
,
table_blacklist
=
WAFFLE_TABLES
):
tasks
.
ScheduleUpgradeReminder
.
apply
(
kwargs
=
dict
(
site_id
=
self
.
site_config
.
site
.
id
,
target_day_str
=
test_datetime_str
,
day_offset
=
2
,
self
.
tested_task
.
apply
(
kwargs
=
dict
(
site_id
=
self
.
site_config
.
site
.
id
,
target_day_str
=
serialize
(
target_day
),
day_offset
=
offset
,
bin_num
=
self
.
_calculate_bin_for_user
(
user
),
))
self
.
assertEqual
(
len
(
sent_messages
),
1
)
# Load the site (which we query per message sent)
# Check the schedule config
with
self
.
assertNumQueries
(
2
):
for
args
in
sent_messages
:
tasks
.
_upgrade_reminder_schedule_send
(
*
args
)
self
.
assertEqual
(
mock_channel
.
deliver
.
call_count
,
1
)
for
(
_name
,
(
_msg
,
email
),
_kwargs
)
in
mock_channel
.
deliver
.
mock_calls
:
for
template
in
attr
.
astuple
(
email
):
self
.
assertNotIn
(
"TEMPLATE WARNING"
,
template
)
self
.
assertNotIn
(
"{{"
,
template
)
self
.
assertNotIn
(
"}}"
,
template
)
def
_get_template_overrides
(
self
):
templates_override
=
deepcopy
(
settings
.
TEMPLATES
)
templates_override
[
0
][
'OPTIONS'
][
'string_if_invalid'
]
=
"TEMPLATE WARNING - MISSING VARIABLE [
%
s]"
return
templates_override
def
_calculate_bin_for_user
(
self
,
user
):
return
user
.
id
%
resolvers
.
UPGRADE_REMINDER_NUM_BINS
@patch.object
(
tasks
,
'_upgrade_reminder_schedule_send'
)
def
test_dont_send_to_verified_learner
(
self
,
mock_schedule_send
):
upgrade_deadline
=
datetime
.
datetime
.
now
(
pytz
.
UTC
)
+
datetime
.
timedelta
(
days
=
2
)
ScheduleFactory
.
create
(
upgrade_deadline
=
upgrade_deadline
,
enrollment__user
=
UserFactory
.
create
(
id
=
resolvers
.
UPGRADE_REMINDER_NUM_BINS
),
enrollment__course
=
self
.
course_overview
,
enrollment__mode
=
CourseMode
.
VERIFIED
,
)
test_datetime_str
=
serialize
(
datetime
.
datetime
.
now
(
pytz
.
UTC
))
tasks
.
ScheduleUpgradeReminder
.
delay
(
self
.
site_config
.
site
.
id
,
target_day_str
=
test_datetime_str
,
day_offset
=
2
,
bin_num
=
0
,
org_list
=
[
self
.
course
.
org
],
)
self
.
assertFalse
(
mock_schedule_send
.
called
)
self
.
assertFalse
(
mock_schedule_send
.
apply_async
.
called
)
self
.
assertEqual
(
mock_ace
.
send
.
called
,
not
is_verified
)
def
test_filter_out_verified_schedules
(
self
):
now
=
datetime
.
datetime
.
now
(
pytz
.
UTC
)
future_datetime
=
now
+
datetime
.
timedelta
(
days
=
21
)
current_day
,
offset
,
target_day
=
self
.
_get_dates
()
user
=
UserFactory
.
create
()
schedules
=
[
ScheduleFactory
.
create
(
upgrade_deadline
=
future_datetime
,
upgrade_deadline
=
target_day
,
enrollment__user
=
user
,
enrollment__course__self_paced
=
True
,
enrollment__course__end
=
future_datetime
+
datetime
.
timedelta
(
days
=
30
),
enrollment__course__id
=
CourseLocator
(
'edX'
,
'toy'
,
'Course{}'
.
format
(
i
)),
enrollment__mode
=
CourseMode
.
VERIFIED
if
i
in
(
0
,
3
)
else
CourseMode
.
AUDIT
,
)
for
i
in
range
(
5
)
]
for
schedule
in
schedules
:
CourseModeFactory
(
course_id
=
schedule
.
enrollment
.
course
.
id
,
mode_slug
=
CourseMode
.
VERIFIED
,
expiration_datetime
=
future_datetime
)
test_datetime
=
future_datetime
test_datetime_str
=
serialize
(
test_datetime
)
sent_messages
=
[]
with
patch
.
object
(
tasks
.
ScheduleUpgradeReminder
,
'async_send_task'
)
as
mock_schedule_send
:
with
patch
.
object
(
self
.
tested_task
,
'async_send_task'
)
as
mock_schedule_send
:
mock_schedule_send
.
apply_async
=
lambda
args
,
*
_a
,
**
_kw
:
sent_messages
.
append
(
args
[
1
])
tasks
.
ScheduleUpgradeReminder
.
apply
(
kwargs
=
dict
(
site_id
=
self
.
site_config
.
site
.
id
,
target_day_str
=
test_datetime_str
,
day_offset
=
2
,
self
.
tested_task
.
apply
(
kwargs
=
dict
(
site_id
=
self
.
site_config
.
site
.
id
,
target_day_str
=
serialize
(
target_day
),
day_offset
=
offset
,
bin_num
=
self
.
_calculate_bin_for_user
(
user
),
))
...
...
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