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
d171dc3e
Commit
d171dc3e
authored
Sep 19, 2013
by
Brian Wilson
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Refactor instructor_task tests, and add handling for general errors in bulk_email subtasks.
parent
2337b6d8
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
172 additions
and
119 deletions
+172
-119
lms/djangoapps/bulk_email/tasks.py
+64
-36
lms/djangoapps/bulk_email/tests/test_email.py
+8
-33
lms/djangoapps/bulk_email/tests/test_err_handling.py
+31
-17
lms/djangoapps/instructor_task/api.py
+4
-5
lms/djangoapps/instructor_task/tests/test_api.py
+38
-8
lms/djangoapps/instructor_task/tests/test_base.py
+27
-20
lms/djangoapps/instructor_task/tests/test_tasks.py
+0
-0
No files found.
lms/djangoapps/bulk_email/tasks.py
View file @
d171dc3e
...
@@ -166,7 +166,7 @@ def perform_delegate_email_batches(entry_id, course_id, task_input, action_name)
...
@@ -166,7 +166,7 @@ def perform_delegate_email_batches(entry_id, course_id, task_input, action_name)
to_list
=
recipient_sublist
[
i
*
chunk
:
i
*
chunk
+
chunk
]
to_list
=
recipient_sublist
[
i
*
chunk
:
i
*
chunk
+
chunk
]
subtask_id
=
str
(
uuid4
())
subtask_id
=
str
(
uuid4
())
subtask_id_list
.
append
(
subtask_id
)
subtask_id_list
.
append
(
subtask_id
)
subtask_progress
=
_course_email
_result
(
None
,
0
,
0
,
0
)
subtask_progress
=
update_subtask
_result
(
None
,
0
,
0
,
0
)
task_list
.
append
(
send_course_email
.
subtask
((
task_list
.
append
(
send_course_email
.
subtask
((
entry_id
,
entry_id
,
email_id
,
email_id
,
...
@@ -259,14 +259,14 @@ def _update_subtask_status(entry_id, current_task_id, status, subtask_result):
...
@@ -259,14 +259,14 @@ def _update_subtask_status(entry_id, current_task_id, status, subtask_result):
log
.
info
(
"Task output updated to
%
s for email subtask
%
s of instructor task
%
d"
,
log
.
info
(
"Task output updated to
%
s for email subtask
%
s of instructor task
%
d"
,
entry
.
task_output
,
current_task_id
,
entry_id
)
entry
.
task_output
,
current_task_id
,
entry_id
)
# TODO: temporary -- switch to debug
# TODO: temporary -- switch to debug
once working
log
.
info
(
"about to save...."
)
log
.
info
(
"about to save...."
)
entry
.
save
()
entry
.
save
()
except
:
except
:
log
.
exception
(
"Unexpected error while updating InstructorTask."
)
log
.
exception
(
"Unexpected error while updating InstructorTask."
)
transaction
.
rollback
()
transaction
.
rollback
()
else
:
else
:
# TODO: temporary -- switch to debug
# TODO: temporary -- switch to debug
once working
log
.
info
(
"about to commit...."
)
log
.
info
(
"about to commit...."
)
transaction
.
commit
()
transaction
.
commit
()
...
@@ -289,40 +289,69 @@ def send_course_email(entry_id, email_id, to_list, global_email_context, subtask
...
@@ -289,40 +289,69 @@ def send_course_email(entry_id, email_id, to_list, global_email_context, subtask
current_task_id
=
_get_current_task
()
.
request
.
id
current_task_id
=
_get_current_task
()
.
request
.
id
retry_index
=
_get_current_task
()
.
request
.
retries
retry_index
=
_get_current_task
()
.
request
.
retries
log
.
info
(
"Preparing to send email as subtask
%
s for instructor task
%
d"
,
log
.
info
(
"Preparing to send email as subtask
%
s for instructor task
%
d
, retry
%
d
"
,
current_task_id
,
entry_id
)
current_task_id
,
entry_id
,
retry_index
)
try
:
try
:
course_title
=
global_email_context
[
'course_title'
]
course_title
=
global_email_context
[
'course_title'
]
course_email_result_value
=
None
course_email_result_value
=
None
send_exception
=
None
with
dog_stats_api
.
timer
(
'course_email.single_task.time.overall'
,
tags
=
[
_statsd_tag
(
course_title
)]):
with
dog_stats_api
.
timer
(
'course_email.single_task.time.overall'
,
tags
=
[
_statsd_tag
(
course_title
)]):
course_email_result_value
=
_send_course_email
(
email_id
,
to_list
,
global_email_context
,
subtask_progress
,
retry_index
)
course_email_result_value
,
send_exception
=
_send_course_email
(
# Assume that if we get here without a raise, the task was successful.
current_task_id
,
# Update the InstructorTask object that is storing its progress.
email_id
,
_update_subtask_status
(
entry_id
,
current_task_id
,
SUCCESS
,
course_email_result_value
)
to_list
,
global_email_context
,
subtask_progress
,
retry_index
,
)
if
send_exception
is
None
:
# Update the InstructorTask object that is storing its progress.
_update_subtask_status
(
entry_id
,
current_task_id
,
SUCCESS
,
course_email_result_value
)
else
:
log
.
error
(
"background task (
%
s) failed:
%
s"
,
current_task_id
,
send_exception
)
_update_subtask_status
(
entry_id
,
current_task_id
,
FAILURE
,
course_email_result_value
)
raise
send_exception
except
Exception
:
except
Exception
:
# try to write out the failure to the entry before failing
# try to write out the failure to the entry before failing
_
,
exception
,
traceback
=
exc_info
()
_
,
exception
,
traceback
=
exc_info
()
traceback_string
=
format_exc
(
traceback
)
if
traceback
is
not
None
else
''
traceback_string
=
format_exc
(
traceback
)
if
traceback
is
not
None
else
''
log
.
warning
(
"background task (
%
s) failed:
%
s
%
s"
,
current_task_id
,
exception
,
traceback_string
)
log
.
error
(
"background task (
%
s) failed:
%
s
%
s"
,
current_task_id
,
exception
,
traceback_string
)
_update_subtask_status
(
entry_id
,
current_task_id
,
FAILURE
,
subtask_progress
)
_update_subtask_status
(
entry_id
,
current_task_id
,
FAILURE
,
subtask_progress
)
raise
raise
return
course_email_result_value
return
course_email_result_value
def
_send_course_email
(
email_id
,
to_list
,
global_email_context
,
subtask_progress
,
retry_index
):
def
_send_course_email
(
task_id
,
email_id
,
to_list
,
global_email_context
,
subtask_progress
,
retry_index
):
"""
"""
Performs the email sending task.
Performs the email sending task.
Returns a tuple of two values:
* First value is a dict which represents current progress. Keys are:
'attempted': number of emails attempted
'succeeded': number of emails succeeded
'skipped': number of emails skipped (due to optout)
'failed': number of emails not sent because of some failure
* Second value is an exception returned by the innards of the method, indicating a fatal error.
In this case, the number of recipients that were not sent have already been added to the
'failed' count above.
"""
"""
throttle
=
retry_index
>
0
throttle
=
retry_index
>
0
num_optout
=
0
num_sent
=
0
num_error
=
0
try
:
try
:
course_email
=
CourseEmail
.
objects
.
get
(
id
=
email_id
)
course_email
=
CourseEmail
.
objects
.
get
(
id
=
email_id
)
except
CourseEmail
.
DoesNotExist
:
except
CourseEmail
.
DoesNotExist
as
exc
:
log
.
exception
(
"Could not find email id:{} to send."
.
format
(
email_id
))
log
.
exception
(
"Task
%
s: could not find email id:
%
s to send."
,
task_id
,
email_id
)
raise
num_error
+=
len
(
to_list
)
return
update_subtask_result
(
subtask_progress
,
num_sent
,
num_error
,
num_optout
),
exc
# exclude optouts (if not a retry):
# exclude optouts (if not a retry):
# Note that we don't have to do the optout logic at all if this is a retry,
# Note that we don't have to do the optout logic at all if this is a retry,
...
@@ -330,7 +359,6 @@ def _send_course_email(email_id, to_list, global_email_context, subtask_progress
...
@@ -330,7 +359,6 @@ def _send_course_email(email_id, to_list, global_email_context, subtask_progress
# attempt. Anyone on the to_list on a retry has already passed the filter
# attempt. Anyone on the to_list on a retry has already passed the filter
# that existed at that time, and we don't need to keep checking for changes
# that existed at that time, and we don't need to keep checking for changes
# in the Optout list.
# in the Optout list.
num_optout
=
0
if
retry_index
==
0
:
if
retry_index
==
0
:
optouts
=
(
Optout
.
objects
.
filter
(
course_id
=
course_email
.
course_id
,
optouts
=
(
Optout
.
objects
.
filter
(
course_id
=
course_email
.
course_id
,
user__in
=
[
i
[
'pk'
]
for
i
in
to_list
])
user__in
=
[
i
[
'pk'
]
for
i
in
to_list
])
...
@@ -359,8 +387,6 @@ def _send_course_email(email_id, to_list, global_email_context, subtask_progress
...
@@ -359,8 +387,6 @@ def _send_course_email(email_id, to_list, global_email_context, subtask_progress
course_email_template
=
CourseEmailTemplate
.
get_template
()
course_email_template
=
CourseEmailTemplate
.
get_template
()
num_sent
=
0
num_error
=
0
try
:
try
:
connection
=
get_connection
()
connection
=
get_connection
()
connection
.
open
()
connection
.
open
()
...
@@ -413,45 +439,47 @@ def _send_course_email(email_id, to_list, global_email_context, subtask_progress
...
@@ -413,45 +439,47 @@ def _send_course_email(email_id, to_list, global_email_context, subtask_progress
raise
exc
raise
exc
else
:
else
:
# This will fall through and not retry the message, since it will be popped
# This will fall through and not retry the message, since it will be popped
log
.
warning
(
'Email with id
%
s not delivered to
%
s due to error
%
s'
,
email_id
,
email
,
exc
.
smtp_error
)
log
.
warning
(
'Task
%
s: email with id
%
s not delivered to
%
s due to error
%
s'
,
task_id
,
email_id
,
email
,
exc
.
smtp_error
)
dog_stats_api
.
increment
(
'course_email.error'
,
tags
=
[
_statsd_tag
(
course_title
)])
dog_stats_api
.
increment
(
'course_email.error'
,
tags
=
[
_statsd_tag
(
course_title
)])
num_error
+=
1
num_error
+=
1
to_list
.
pop
()
to_list
.
pop
()
except
(
SMTPDataError
,
SMTPConnectError
,
SMTPServerDisconnected
)
as
exc
:
except
(
SMTPDataError
,
SMTPConnectError
,
SMTPServerDisconnected
)
as
exc
:
# Error
caught here cause the email to be retried. The entire task is actually retried without popping the list
# Error
s caught here cause the email to be retried. The entire task is actually retried
#
Reasoning is that all of these errors may be temporary condition
.
#
without popping the current recipient off of the existing list
.
#
TODO: figure out what this means. Presumably we have popped the list with those that have succeeded
#
Errors caught are those that indicate a temporary condition that might succeed on retry.
# and failed, rather than those needing a later retry.
connection
.
close
()
log
.
warning
(
'
E
mail with id
%
d not delivered due to temporary error
%
s, retrying send to
%
d recipients'
,
log
.
warning
(
'
Task
%
s: e
mail with id
%
d not delivered due to temporary error
%
s, retrying send to
%
d recipients'
,
email_id
,
exc
,
len
(
to_list
))
task_id
,
email_id
,
exc
,
len
(
to_list
))
raise
send_course_email
.
retry
(
raise
send_course_email
.
retry
(
arg
=
[
arg
=
[
email_id
,
email_id
,
to_list
,
to_list
,
global_email_context
,
global_email_context
,
_course_email
_result
(
subtask_progress
,
num_sent
,
num_error
,
num_optout
),
update_subtask
_result
(
subtask_progress
,
num_sent
,
num_error
,
num_optout
),
],
],
exc
=
exc
,
exc
=
exc
,
countdown
=
(
2
**
retry_index
)
*
15
countdown
=
(
2
**
retry_index
)
*
15
)
)
except
:
except
Exception
as
exc
:
log
.
exception
(
'Email with id
%
d caused send_course_email task to fail with uncaught exception. To list:
%
s'
,
email_id
,
# If we have a general exception for this request, we need to figure out what to do with it.
[
i
[
'email'
]
for
i
in
to_list
])
# If we're going to just mark it as failed
# Close the connection before we exit
# And the log message below should indicate which task_id is failing, so we have a chance to
# reconstruct the problems.
connection
.
close
()
connection
.
close
()
raise
log
.
exception
(
'Task
%
s: email with id
%
d caused send_course_email task to fail with uncaught exception. To list:
%
s'
,
task_id
,
email_id
,
[
i
[
'email'
]
for
i
in
to_list
])
num_error
+=
len
(
to_list
)
return
update_subtask_result
(
subtask_progress
,
num_sent
,
num_error
,
num_optout
),
exc
else
:
else
:
connection
.
close
()
# Add current progress to any progress stemming from previous retries:
# Add current progress to any progress stemming from previous retries:
return
_course_email_result
(
subtask_progress
,
num_sent
,
num_error
,
num_optout
)
connection
.
close
()
return
update_subtask_result
(
subtask_progress
,
num_sent
,
num_error
,
num_optout
),
None
def
_course_email
_result
(
previous_result
,
new_num_sent
,
new_num_error
,
new_num_optout
):
def
update_subtask
_result
(
previous_result
,
new_num_sent
,
new_num_error
,
new_num_optout
):
"""Return the result of course_email sending as a dict (not a string)."""
"""Return the result of course_email sending as a dict (not a string)."""
attempted
=
new_num_sent
+
new_num_error
attempted
=
new_num_sent
+
new_num_error
current_result
=
{
'attempted'
:
attempted
,
'succeeded'
:
new_num_sent
,
'skipped'
:
new_num_optout
,
'failed'
:
new_num_error
}
current_result
=
{
'attempted'
:
attempted
,
'succeeded'
:
new_num_sent
,
'skipped'
:
new_num_optout
,
'failed'
:
new_num_error
}
...
...
lms/djangoapps/bulk_email/tests/test_email.py
View file @
d171dc3e
...
@@ -2,6 +2,8 @@
...
@@ -2,6 +2,8 @@
"""
"""
Unit tests for sending course email
Unit tests for sending course email
"""
"""
from
mock
import
patch
from
django.conf
import
settings
from
django.conf
import
settings
from
django.core
import
mail
from
django.core
import
mail
from
django.core.urlresolvers
import
reverse
from
django.core.urlresolvers
import
reverse
...
@@ -12,13 +14,7 @@ from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
...
@@ -12,13 +14,7 @@ from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from
student.tests.factories
import
UserFactory
,
GroupFactory
,
CourseEnrollmentFactory
from
student.tests.factories
import
UserFactory
,
GroupFactory
,
CourseEnrollmentFactory
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
instructor_task.models
import
InstructorTask
from
bulk_email.models
import
Optout
from
instructor_task.tests.factories
import
InstructorTaskFactory
from
bulk_email.tasks
import
send_course_email
from
bulk_email.models
import
CourseEmail
,
Optout
from
mock
import
patch
STAFF_COUNT
=
3
STAFF_COUNT
=
3
STUDENT_COUNT
=
10
STUDENT_COUNT
=
10
...
@@ -32,13 +28,13 @@ class MockCourseEmailResult(object):
...
@@ -32,13 +28,13 @@ class MockCourseEmailResult(object):
"""
"""
emails_sent
=
0
emails_sent
=
0
def
get_mock_
course_email
_result
(
self
):
def
get_mock_
update_subtask
_result
(
self
):
"""Wrapper for mock email function."""
"""Wrapper for mock email function."""
def
mock_
course_email
_result
(
prev_results
,
sent
,
failed
,
output
,
**
kwargs
):
# pylint: disable=W0613
def
mock_
update_subtask
_result
(
prev_results
,
sent
,
failed
,
output
,
**
kwargs
):
# pylint: disable=W0613
"""Increments count of number of emails sent."""
"""Increments count of number of emails sent."""
self
.
emails_sent
+=
sent
self
.
emails_sent
+=
sent
return
True
return
True
return
mock_
course_email
_result
return
mock_
update_subtask
_result
@override_settings
(
MODULESTORE
=
TEST_DATA_MONGO_MODULESTORE
)
@override_settings
(
MODULESTORE
=
TEST_DATA_MONGO_MODULESTORE
)
...
@@ -247,13 +243,13 @@ class TestEmailSendFromDashboard(ModuleStoreTestCase):
...
@@ -247,13 +243,13 @@ class TestEmailSendFromDashboard(ModuleStoreTestCase):
)
)
@override_settings
(
EMAILS_PER_TASK
=
3
,
EMAILS_PER_QUERY
=
7
)
@override_settings
(
EMAILS_PER_TASK
=
3
,
EMAILS_PER_QUERY
=
7
)
@patch
(
'bulk_email.tasks.
_course_email
_result'
)
@patch
(
'bulk_email.tasks.
update_subtask
_result'
)
def
test_chunked_queries_send_numerous_emails
(
self
,
email_mock
):
def
test_chunked_queries_send_numerous_emails
(
self
,
email_mock
):
"""
"""
Test sending a large number of emails, to test the chunked querying
Test sending a large number of emails, to test the chunked querying
"""
"""
mock_factory
=
MockCourseEmailResult
()
mock_factory
=
MockCourseEmailResult
()
email_mock
.
side_effect
=
mock_factory
.
get_mock_
course_email
_result
()
email_mock
.
side_effect
=
mock_factory
.
get_mock_
update_subtask
_result
()
added_users
=
[]
added_users
=
[]
for
_
in
xrange
(
LARGE_NUM_EMAILS
):
for
_
in
xrange
(
LARGE_NUM_EMAILS
):
user
=
UserFactory
()
user
=
UserFactory
()
...
@@ -283,24 +279,3 @@ class TestEmailSendFromDashboard(ModuleStoreTestCase):
...
@@ -283,24 +279,3 @@ class TestEmailSendFromDashboard(ModuleStoreTestCase):
[
s
.
email
for
s
in
self
.
students
]
+
[
s
.
email
for
s
in
self
.
students
]
+
[
s
.
email
for
s
in
added_users
if
s
not
in
optouts
])
[
s
.
email
for
s
in
added_users
if
s
not
in
optouts
])
self
.
assertItemsEqual
(
outbox_contents
,
should_send_contents
)
self
.
assertItemsEqual
(
outbox_contents
,
should_send_contents
)
@override_settings
(
MODULESTORE
=
TEST_DATA_MONGO_MODULESTORE
)
class
TestEmailSendExceptions
(
ModuleStoreTestCase
):
"""
Test that exceptions are handled correctly.
"""
def
test_no_instructor_task
(
self
):
with
self
.
assertRaises
(
InstructorTask
.
DoesNotExist
):
send_course_email
(
100
,
101
,
[],
{},
False
)
def
test_no_course_title
(
self
):
entry
=
InstructorTaskFactory
.
create
(
task_key
=
''
,
task_id
=
'dummy'
)
with
self
.
assertRaises
(
KeyError
):
send_course_email
(
entry
.
id
,
101
,
[],
{},
False
)
def
test_no_course_email_obj
(
self
):
# Make sure send_course_email handles CourseEmail.DoesNotExist exception.
entry
=
InstructorTaskFactory
.
create
(
task_key
=
''
,
task_id
=
'dummy'
)
with
self
.
assertRaises
(
CourseEmail
.
DoesNotExist
):
send_course_email
(
entry
.
id
,
101
,
[],
{
'course_title'
:
'Test'
},
False
)
lms/djangoapps/bulk_email/tests/test_err_handling.py
View file @
d171dc3e
...
@@ -2,11 +2,16 @@
...
@@ -2,11 +2,16 @@
Unit tests for handling email sending errors
Unit tests for handling email sending errors
"""
"""
from
itertools
import
cycle
from
itertools
import
cycle
from
mock
import
patch
,
Mock
from
smtplib
import
SMTPDataError
,
SMTPServerDisconnected
,
SMTPConnectError
from
unittest
import
skip
from
django.test.utils
import
override_settings
from
django.test.utils
import
override_settings
from
django.conf
import
settings
from
django.conf
import
settings
from
django.core.management
import
call_command
from
django.core.management
import
call_command
from
django.core.urlresolvers
import
reverse
from
django.core.urlresolvers
import
reverse
from
courseware.tests.tests
import
TEST_DATA_MONGO_MODULESTORE
from
courseware.tests.tests
import
TEST_DATA_MONGO_MODULESTORE
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
xmodule.modulestore.tests.factories
import
CourseFactory
...
@@ -16,9 +21,6 @@ from bulk_email.models import CourseEmail
...
@@ -16,9 +21,6 @@ from bulk_email.models import CourseEmail
from
bulk_email.tasks
import
perform_delegate_email_batches
from
bulk_email.tasks
import
perform_delegate_email_batches
from
instructor_task.models
import
InstructorTask
from
instructor_task.models
import
InstructorTask
from
mock
import
patch
,
Mock
from
smtplib
import
SMTPDataError
,
SMTPServerDisconnected
,
SMTPConnectError
class
EmailTestException
(
Exception
):
class
EmailTestException
(
Exception
):
"""Mock exception for email testing."""
"""Mock exception for email testing."""
...
@@ -65,14 +67,15 @@ class TestEmailErrors(ModuleStoreTestCase):
...
@@ -65,14 +67,15 @@ class TestEmailErrors(ModuleStoreTestCase):
self
.
assertIsInstance
(
exc
,
SMTPDataError
)
self
.
assertIsInstance
(
exc
,
SMTPDataError
)
@patch
(
'bulk_email.tasks.get_connection'
,
autospec
=
True
)
@patch
(
'bulk_email.tasks.get_connection'
,
autospec
=
True
)
@patch
(
'bulk_email.tasks.
course_email
_result'
)
@patch
(
'bulk_email.tasks.
update_subtask
_result'
)
@patch
(
'bulk_email.tasks.send_course_email.retry'
)
@patch
(
'bulk_email.tasks.send_course_email.retry'
)
def
test_data_err_fail
(
self
,
retry
,
result
,
get_conn
):
def
test_data_err_fail
(
self
,
retry
,
result
,
get_conn
):
"""
"""
Test that celery handles permanent SMTPDataErrors by failing and not retrying.
Test that celery handles permanent SMTPDataErrors by failing and not retrying.
"""
"""
# have every fourth email fail due to blacklisting:
get_conn
.
return_value
.
send_messages
.
side_effect
=
cycle
([
SMTPDataError
(
554
,
"Email address is blacklisted"
),
get_conn
.
return_value
.
send_messages
.
side_effect
=
cycle
([
SMTPDataError
(
554
,
"Email address is blacklisted"
),
None
])
None
,
None
,
None
])
students
=
[
UserFactory
()
for
_
in
xrange
(
settings
.
EMAILS_PER_TASK
)]
students
=
[
UserFactory
()
for
_
in
xrange
(
settings
.
EMAILS_PER_TASK
)]
for
student
in
students
:
for
student
in
students
:
CourseEnrollmentFactory
.
create
(
user
=
student
,
course_id
=
self
.
course
.
id
)
CourseEnrollmentFactory
.
create
(
user
=
student
,
course_id
=
self
.
course
.
id
)
...
@@ -88,10 +91,10 @@ class TestEmailErrors(ModuleStoreTestCase):
...
@@ -88,10 +91,10 @@ class TestEmailErrors(ModuleStoreTestCase):
# We shouldn't retry when hitting a 5xx error
# We shouldn't retry when hitting a 5xx error
self
.
assertFalse
(
retry
.
called
)
self
.
assertFalse
(
retry
.
called
)
# Test that after the rejected email, the rest still successfully send
# Test that after the rejected email, the rest still successfully send
((
sent
,
fail
,
optouts
),
_
)
=
result
.
call_args
((
_
,
sent
,
fail
,
optouts
),
_
)
=
result
.
call_args
self
.
assertEquals
(
optouts
,
0
)
self
.
assertEquals
(
optouts
,
0
)
self
.
assertEquals
(
fail
,
settings
.
EMAILS_PER_TASK
/
2
)
self
.
assertEquals
(
fail
,
settings
.
EMAILS_PER_TASK
/
4
)
self
.
assertEquals
(
sent
,
settings
.
EMAILS_PER_TASK
/
2
)
self
.
assertEquals
(
sent
,
3
*
settings
.
EMAILS_PER_TASK
/
4
)
@patch
(
'bulk_email.tasks.get_connection'
,
autospec
=
True
)
@patch
(
'bulk_email.tasks.get_connection'
,
autospec
=
True
)
@patch
(
'bulk_email.tasks.send_course_email.retry'
)
@patch
(
'bulk_email.tasks.send_course_email.retry'
)
...
@@ -134,10 +137,11 @@ class TestEmailErrors(ModuleStoreTestCase):
...
@@ -134,10 +137,11 @@ class TestEmailErrors(ModuleStoreTestCase):
exc
=
kwargs
[
'exc'
]
exc
=
kwargs
[
'exc'
]
self
.
assertIsInstance
(
exc
,
SMTPConnectError
)
self
.
assertIsInstance
(
exc
,
SMTPConnectError
)
@patch
(
'bulk_email.tasks.
course_email
_result'
)
@patch
(
'bulk_email.tasks.
update_subtask
_result'
)
@patch
(
'bulk_email.tasks.send_course_email.retry'
)
@patch
(
'bulk_email.tasks.send_course_email.retry'
)
@patch
(
'bulk_email.tasks.log'
)
@patch
(
'bulk_email.tasks.log'
)
@patch
(
'bulk_email.tasks.get_connection'
,
Mock
(
return_value
=
EmailTestException
))
@patch
(
'bulk_email.tasks.get_connection'
,
Mock
(
return_value
=
EmailTestException
))
@skip
def
test_general_exception
(
self
,
mock_log
,
retry
,
result
):
def
test_general_exception
(
self
,
mock_log
,
retry
,
result
):
"""
"""
Tests the if the error is not SMTP-related, we log and reraise
Tests the if the error is not SMTP-related, we log and reraise
...
@@ -148,19 +152,29 @@ class TestEmailErrors(ModuleStoreTestCase):
...
@@ -148,19 +152,29 @@ class TestEmailErrors(ModuleStoreTestCase):
'subject'
:
'test subject for myself'
,
'subject'
:
'test subject for myself'
,
'message'
:
'test message for myself'
'message'
:
'test message for myself'
}
}
# TODO: This whole test is flawed. Figure out how to make it work correctly,
# possibly moving it elsewhere. It's hitting the wrong exception.
# For some reason (probably the weirdness of testing with celery tasks) assertRaises doesn't work here
# For some reason (probably the weirdness of testing with celery tasks) assertRaises doesn't work here
# so we assert on the arguments of log.exception
# so we assert on the arguments of log.exception
# TODO: This is way too fragile, because if any additional log statement is added anywhere in the flow,
# this test will break.
self
.
client
.
post
(
self
.
url
,
test_email
)
self
.
client
.
post
(
self
.
url
,
test_email
)
((
log_str
,
email_id
,
to_list
),
_
)
=
mock_log
.
exception
.
call_args
# ((log_str, email_id, to_list), _) = mock_log.exception.call_args
# instead, use call_args_list[-1] to get the last call?
self
.
assertTrue
(
mock_log
.
exception
.
called
)
self
.
assertTrue
(
mock_log
.
exception
.
called
)
self
.
assertIn
(
'caused send_course_email task to fail with uncaught exception.'
,
log_str
)
#
self.assertIn('caused send_course_email task to fail with uncaught exception.', log_str)
self
.
assertEqual
(
email_id
,
1
)
#
self.assertEqual(email_id, 1)
self
.
assertEqual
(
to_list
,
[
self
.
instructor
.
email
])
#
self.assertEqual(to_list, [self.instructor.email])
self
.
assertFalse
(
retry
.
called
)
self
.
assertFalse
(
retry
.
called
)
self
.
assertFalse
(
result
.
called
)
# TODO: cannot use the result method to determine if a result was generated,
# because we now call the particular method as part of all subtask calls.
@patch
(
'bulk_email.tasks.course_email_result'
)
# So use result.called_count to track this...
# @patch('bulk_email.tasks.delegate_email_batches.retry')
# self.assertFalse(result.called)
# call_args_list = result.call_args_list
num_calls
=
result
.
called_count
self
.
assertTrue
(
num_calls
==
2
)
@patch
(
'bulk_email.tasks.update_subtask_result'
)
@patch
(
'bulk_email.tasks.log'
)
@patch
(
'bulk_email.tasks.log'
)
def
test_nonexist_email
(
self
,
mock_log
,
result
):
def
test_nonexist_email
(
self
,
mock_log
,
result
):
"""
"""
...
...
lms/djangoapps/instructor_task/api.py
View file @
d171dc3e
...
@@ -190,7 +190,6 @@ def submit_bulk_course_email(request, course_id, email_id):
...
@@ -190,7 +190,6 @@ def submit_bulk_course_email(request, course_id, email_id):
"""
"""
# check arguments: make sure that the course is defined?
# check arguments: make sure that the course is defined?
# TODO: what is the right test here?
# TODO: what is the right test here?
# modulestore().get_instance(course_id, problem_url)
# This should also make sure that the email exists.
# This should also make sure that the email exists.
# We can also pull out the To argument here, so that is displayed in
# We can also pull out the To argument here, so that is displayed in
...
@@ -200,10 +199,10 @@ def submit_bulk_course_email(request, course_id, email_id):
...
@@ -200,10 +199,10 @@ def submit_bulk_course_email(request, course_id, email_id):
task_type
=
'bulk_course_email'
task_type
=
'bulk_course_email'
task_class
=
send_bulk_course_email
task_class
=
send_bulk_course_email
#
TODO: figure out if we need to encode in a standard way, or if we can get away
#
Pass in the to_option as a separate argument, even though it's (currently)
#
with doing this manually. Shouldn't be hard to make the encode call explicitly,
#
in the CourseEmail. That way it's visible in the progress status.
#
and allow no problem_url or student to be defined. Like this:
#
(At some point in the future, we might take the recipient out of the CourseEmail,
#
task_input, task_key = encode_problem_and_student_input(
)
#
so that the same saved email can be sent to different recipients, as it is tested.
)
task_input
=
{
'email_id'
:
email_id
,
'to_option'
:
to_option
}
task_input
=
{
'email_id'
:
email_id
,
'to_option'
:
to_option
}
task_key_stub
=
"{email_id}_{to_option}"
.
format
(
email_id
=
email_id
,
to_option
=
to_option
)
task_key_stub
=
"{email_id}_{to_option}"
.
format
(
email_id
=
email_id
,
to_option
=
to_option
)
# create the key value by using MD5 hash:
# create the key value by using MD5 hash:
...
...
lms/djangoapps/instructor_task/tests/test_api.py
View file @
d171dc3e
...
@@ -6,16 +6,21 @@ from xmodule.modulestore.exceptions import ItemNotFoundError
...
@@ -6,16 +6,21 @@ from xmodule.modulestore.exceptions import ItemNotFoundError
from
courseware.tests.factories
import
UserFactory
from
courseware.tests.factories
import
UserFactory
from
instructor_task.api
import
(
get_running_instructor_tasks
,
from
bulk_email.models
import
CourseEmail
,
SEND_TO_ALL
get_instructor_task_history
,
from
instructor_task.api
import
(
submit_rescore_problem_for_all_students
,
get_running_instructor_tasks
,
submit_rescore_problem_for_student
,
get_instructor_task_history
,
submit_reset_problem_attempts_for_all_students
,
submit_rescore_problem_for_all_students
,
submit_delete_problem_state_for_all_students
)
submit_rescore_problem_for_student
,
submit_reset_problem_attempts_for_all_students
,
submit_delete_problem_state_for_all_students
,
submit_bulk_course_email
,
)
from
instructor_task.api_helper
import
AlreadyRunningError
from
instructor_task.api_helper
import
AlreadyRunningError
from
instructor_task.models
import
InstructorTask
,
PROGRESS
from
instructor_task.models
import
InstructorTask
,
PROGRESS
from
instructor_task.tests.test_base
import
(
InstructorTaskTestCase
,
from
instructor_task.tests.test_base
import
(
InstructorTaskTestCase
,
InstructorTaskCourseTestCase
,
InstructorTaskModuleTestCase
,
InstructorTaskModuleTestCase
,
TEST_COURSE_ID
)
TEST_COURSE_ID
)
...
@@ -46,8 +51,8 @@ class InstructorTaskReportTest(InstructorTaskTestCase):
...
@@ -46,8 +51,8 @@ class InstructorTaskReportTest(InstructorTaskTestCase):
self
.
assertEquals
(
set
(
task_ids
),
set
(
expected_ids
))
self
.
assertEquals
(
set
(
task_ids
),
set
(
expected_ids
))
class
InstructorTaskSubmitTest
(
InstructorTaskModuleTestCase
):
class
InstructorTask
Module
SubmitTest
(
InstructorTaskModuleTestCase
):
"""Tests API methods that involve the submission of background tasks."""
"""Tests API methods that involve the submission of
module-based
background tasks."""
def
setUp
(
self
):
def
setUp
(
self
):
self
.
initialize_course
()
self
.
initialize_course
()
...
@@ -136,3 +141,28 @@ class InstructorTaskSubmitTest(InstructorTaskModuleTestCase):
...
@@ -136,3 +141,28 @@ class InstructorTaskSubmitTest(InstructorTaskModuleTestCase):
def
test_submit_delete_all
(
self
):
def
test_submit_delete_all
(
self
):
self
.
_test_submit_task
(
submit_delete_problem_state_for_all_students
)
self
.
_test_submit_task
(
submit_delete_problem_state_for_all_students
)
class
InstructorTaskCourseSubmitTest
(
InstructorTaskCourseTestCase
):
"""Tests API methods that involve the submission of course-based background tasks."""
def
setUp
(
self
):
self
.
initialize_course
()
self
.
student
=
UserFactory
.
create
(
username
=
"student"
,
email
=
"student@edx.org"
)
self
.
instructor
=
UserFactory
.
create
(
username
=
"instructor"
,
email
=
"instructor@edx.org"
)
def
_define_course_email
(
self
):
course_email
=
CourseEmail
.
create
(
self
.
course
.
id
,
self
.
instructor
,
SEND_TO_ALL
,
"Test Subject"
,
"<p>This is a test message</p>"
)
return
course_email
.
id
def
test_submit_bulk_email_all
(
self
):
email_id
=
self
.
_define_course_email
()
instructor_task
=
submit_bulk_course_email
(
self
.
create_task_request
(
self
.
instructor
),
self
.
course
.
id
,
email_id
)
# test resubmitting, by updating the existing record:
instructor_task
=
InstructorTask
.
objects
.
get
(
id
=
instructor_task
.
id
)
instructor_task
.
task_state
=
PROGRESS
instructor_task
.
save
()
with
self
.
assertRaises
(
AlreadyRunningError
):
instructor_task
=
submit_bulk_course_email
(
self
.
create_task_request
(
self
.
instructor
),
self
.
course
.
id
,
email_id
)
lms/djangoapps/instructor_task/tests/test_base.py
View file @
d171dc3e
...
@@ -96,10 +96,10 @@ class InstructorTaskTestCase(TestCase):
...
@@ -96,10 +96,10 @@ class InstructorTaskTestCase(TestCase):
@override_settings
(
MODULESTORE
=
TEST_DATA_MIXED_MODULESTORE
)
@override_settings
(
MODULESTORE
=
TEST_DATA_MIXED_MODULESTORE
)
class
InstructorTask
Modul
eTestCase
(
LoginEnrollmentTestCase
,
ModuleStoreTestCase
):
class
InstructorTask
Cours
eTestCase
(
LoginEnrollmentTestCase
,
ModuleStoreTestCase
):
"""
"""
Base test class for InstructorTask-related tests that require
Base test class for InstructorTask-related tests that require
the setup of a course
and problem in order to access StudentModule state
.
the setup of a course.
"""
"""
course
=
None
course
=
None
current_user
=
None
current_user
=
None
...
@@ -150,6 +150,31 @@ class InstructorTaskModuleTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase)
...
@@ -150,6 +150,31 @@ class InstructorTaskModuleTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase)
return
self
.
_create_user
(
username
,
is_staff
=
False
)
return
self
.
_create_user
(
username
,
is_staff
=
False
)
@staticmethod
@staticmethod
def
get_task_status
(
task_id
):
"""Use api method to fetch task status, using mock request."""
mock_request
=
Mock
()
mock_request
.
REQUEST
=
{
'task_id'
:
task_id
}
response
=
instructor_task_status
(
mock_request
)
status
=
json
.
loads
(
response
.
content
)
return
status
def
create_task_request
(
self
,
requester_username
):
"""Generate request that can be used for submitting tasks"""
request
=
Mock
()
request
.
user
=
User
.
objects
.
get
(
username
=
requester_username
)
request
.
get_host
=
Mock
(
return_value
=
"testhost"
)
request
.
META
=
{
'REMOTE_ADDR'
:
'0:0:0:0'
,
'SERVER_NAME'
:
'testhost'
}
request
.
is_secure
=
Mock
(
return_value
=
False
)
return
request
@override_settings
(
MODULESTORE
=
TEST_DATA_MIXED_MODULESTORE
)
class
InstructorTaskModuleTestCase
(
InstructorTaskCourseTestCase
):
"""
Base test class for InstructorTask-related tests that require
the setup of a course and problem in order to access StudentModule state.
"""
@staticmethod
def
problem_location
(
problem_url_name
):
def
problem_location
(
problem_url_name
):
"""
"""
Create an internal location for a test problem.
Create an internal location for a test problem.
...
@@ -192,21 +217,3 @@ class InstructorTaskModuleTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase)
...
@@ -192,21 +217,3 @@ class InstructorTaskModuleTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase)
module_type
=
descriptor
.
location
.
category
,
module_type
=
descriptor
.
location
.
category
,
module_state_key
=
descriptor
.
location
.
url
(),
module_state_key
=
descriptor
.
location
.
url
(),
)
)
@staticmethod
def
get_task_status
(
task_id
):
"""Use api method to fetch task status, using mock request."""
mock_request
=
Mock
()
mock_request
.
REQUEST
=
{
'task_id'
:
task_id
}
response
=
instructor_task_status
(
mock_request
)
status
=
json
.
loads
(
response
.
content
)
return
status
def
create_task_request
(
self
,
requester_username
):
"""Generate request that can be used for submitting tasks"""
request
=
Mock
()
request
.
user
=
User
.
objects
.
get
(
username
=
requester_username
)
request
.
get_host
=
Mock
(
return_value
=
"testhost"
)
request
.
META
=
{
'REMOTE_ADDR'
:
'0:0:0:0'
,
'SERVER_NAME'
:
'testhost'
}
request
.
is_secure
=
Mock
(
return_value
=
False
)
return
request
lms/djangoapps/instructor_task/tests/test_tasks.py
View file @
d171dc3e
This diff is collapsed.
Click to expand it.
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