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
01611c33
Commit
01611c33
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
2f4774f4
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
367 additions
and
273 deletions
+367
-273
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
+195
-154
No files found.
lms/djangoapps/bulk_email/tasks.py
View file @
01611c33
...
...
@@ -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
]
subtask_id
=
str
(
uuid4
())
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
((
entry_id
,
email_id
,
...
...
@@ -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"
,
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...."
)
entry
.
save
()
except
:
log
.
exception
(
"Unexpected error while updating InstructorTask."
)
transaction
.
rollback
()
else
:
# TODO: temporary -- switch to debug
# TODO: temporary -- switch to debug
once working
log
.
info
(
"about to commit...."
)
transaction
.
commit
()
...
...
@@ -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
retry_index
=
_get_current_task
()
.
request
.
retries
log
.
info
(
"Preparing to send email as subtask
%
s for instructor task
%
d"
,
current_task_id
,
entry_id
)
log
.
info
(
"Preparing to send email as subtask
%
s for instructor task
%
d
, retry
%
d
"
,
current_task_id
,
entry_id
,
retry_index
)
try
:
course_title
=
global_email_context
[
'course_title'
]
course_email_result_value
=
None
send_exception
=
None
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
)
# Assume that if we get here without a raise, the task was successful.
# Update the InstructorTask object that is storing its progress.
_update_subtask_status
(
entry_id
,
current_task_id
,
SUCCESS
,
course_email_result_value
)
course_email_result_value
,
send_exception
=
_send_course_email
(
current_task_id
,
email_id
,
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
:
# try to write out the failure to the entry before failing
_
,
exception
,
traceback
=
exc_info
()
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
)
raise
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.
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
num_optout
=
0
num_sent
=
0
num_error
=
0
try
:
course_email
=
CourseEmail
.
objects
.
get
(
id
=
email_id
)
except
CourseEmail
.
DoesNotExist
:
log
.
exception
(
"Could not find email id:{} to send."
.
format
(
email_id
))
raise
except
CourseEmail
.
DoesNotExist
as
exc
:
log
.
exception
(
"Task
%
s: could not find email id:
%
s to send."
,
task_id
,
email_id
)
num_error
+=
len
(
to_list
)
return
update_subtask_result
(
subtask_progress
,
num_sent
,
num_error
,
num_optout
),
exc
# exclude optouts (if not 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
# 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
# in the Optout list.
num_optout
=
0
if
retry_index
==
0
:
optouts
=
(
Optout
.
objects
.
filter
(
course_id
=
course_email
.
course_id
,
user__in
=
[
i
[
'pk'
]
for
i
in
to_list
])
...
...
@@ -350,8 +378,6 @@ def _send_course_email(email_id, to_list, global_email_context, subtask_progress
course_email_template
=
CourseEmailTemplate
.
get_template
()
num_sent
=
0
num_error
=
0
try
:
connection
=
get_connection
()
connection
.
open
()
...
...
@@ -404,45 +430,47 @@ def _send_course_email(email_id, to_list, global_email_context, subtask_progress
raise
exc
else
:
# 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
)])
num_error
+=
1
to_list
.
pop
()
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
#
Reasoning is that all of these errors may be temporary condition
.
#
TODO: figure out what this means. Presumably we have popped the list with those that have succeeded
# and failed, rather than those needing a later retry.
log
.
warning
(
'
E
mail with id
%
d not delivered due to temporary error
%
s, retrying send to
%
d recipients'
,
email_id
,
exc
,
len
(
to_list
))
# Error
s caught here cause the email to be retried. The entire task is actually retried
#
without popping the current recipient off of the existing list
.
#
Errors caught are those that indicate a temporary condition that might succeed on retry.
connection
.
close
()
log
.
warning
(
'
Task
%
s: e
mail with id
%
d not delivered due to temporary error
%
s, retrying send to
%
d recipients'
,
task_id
,
email_id
,
exc
,
len
(
to_list
))
raise
send_course_email
.
retry
(
arg
=
[
email_id
,
to_list
,
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
,
countdown
=
(
2
**
retry_index
)
*
15
)
except
:
log
.
exception
(
'Email with id
%
d caused send_course_email task to fail with uncaught exception. To list:
%
s'
,
email_id
,
[
i
[
'email'
]
for
i
in
to_list
])
# Close the connection before we exit
except
Exception
as
exc
:
# If we have a general exception for this request, we need to figure out what to do with it.
# If we're going to just mark it as failed
# And the log message below should indicate which task_id is failing, so we have a chance to
# reconstruct the problems.
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
:
connection
.
close
()
# 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)."""
attempted
=
new_num_sent
+
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 @
01611c33
...
...
@@ -2,6 +2,8 @@
"""
Unit tests for sending course email
"""
from
mock
import
patch
from
django.conf
import
settings
from
django.core
import
mail
from
django.core.urlresolvers
import
reverse
...
...
@@ -12,13 +14,7 @@ from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from
student.tests.factories
import
UserFactory
,
GroupFactory
,
CourseEnrollmentFactory
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
instructor_task.models
import
InstructorTask
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
from
bulk_email.models
import
Optout
STAFF_COUNT
=
3
STUDENT_COUNT
=
10
...
...
@@ -32,13 +28,13 @@ class MockCourseEmailResult(object):
"""
emails_sent
=
0
def
get_mock_
course_email
_result
(
self
):
def
get_mock_
update_subtask
_result
(
self
):
"""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."""
self
.
emails_sent
+=
sent
return
True
return
mock_
course_email
_result
return
mock_
update_subtask
_result
@override_settings
(
MODULESTORE
=
TEST_DATA_MONGO_MODULESTORE
)
...
...
@@ -247,13 +243,13 @@ class TestEmailSendFromDashboard(ModuleStoreTestCase):
)
@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
):
"""
Test sending a large number of emails, to test the chunked querying
"""
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
=
[]
for
_
in
xrange
(
LARGE_NUM_EMAILS
):
user
=
UserFactory
()
...
...
@@ -283,24 +279,3 @@ class TestEmailSendFromDashboard(ModuleStoreTestCase):
[
s
.
email
for
s
in
self
.
students
]
+
[
s
.
email
for
s
in
added_users
if
s
not
in
optouts
])
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 @
01611c33
...
...
@@ -2,11 +2,16 @@
Unit tests for handling email sending errors
"""
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.conf
import
settings
from
django.core.management
import
call_command
from
django.core.urlresolvers
import
reverse
from
courseware.tests.tests
import
TEST_DATA_MONGO_MODULESTORE
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
...
...
@@ -16,9 +21,6 @@ from bulk_email.models import CourseEmail
from
bulk_email.tasks
import
perform_delegate_email_batches
from
instructor_task.models
import
InstructorTask
from
mock
import
patch
,
Mock
from
smtplib
import
SMTPDataError
,
SMTPServerDisconnected
,
SMTPConnectError
class
EmailTestException
(
Exception
):
"""Mock exception for email testing."""
...
...
@@ -65,14 +67,15 @@ class TestEmailErrors(ModuleStoreTestCase):
self
.
assertTrue
(
type
(
exc
)
==
SMTPDataError
)
@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'
)
def
test_data_err_fail
(
self
,
retry
,
result
,
get_conn
):
"""
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"
),
None
])
None
,
None
,
None
])
students
=
[
UserFactory
()
for
_
in
xrange
(
settings
.
EMAILS_PER_TASK
)]
for
student
in
students
:
CourseEnrollmentFactory
.
create
(
user
=
student
,
course_id
=
self
.
course
.
id
)
...
...
@@ -88,10 +91,10 @@ class TestEmailErrors(ModuleStoreTestCase):
# We shouldn't retry when hitting a 5xx error
self
.
assertFalse
(
retry
.
called
)
# 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
(
fail
,
settings
.
EMAILS_PER_TASK
/
2
)
self
.
assertEquals
(
sent
,
settings
.
EMAILS_PER_TASK
/
2
)
self
.
assertEquals
(
fail
,
settings
.
EMAILS_PER_TASK
/
4
)
self
.
assertEquals
(
sent
,
3
*
settings
.
EMAILS_PER_TASK
/
4
)
@patch
(
'bulk_email.tasks.get_connection'
,
autospec
=
True
)
@patch
(
'bulk_email.tasks.send_course_email.retry'
)
...
...
@@ -134,10 +137,11 @@ class TestEmailErrors(ModuleStoreTestCase):
exc
=
kwargs
[
'exc'
]
self
.
assertTrue
(
type
(
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.log'
)
@patch
(
'bulk_email.tasks.get_connection'
,
Mock
(
return_value
=
EmailTestException
))
@skip
def
test_general_exception
(
self
,
mock_log
,
retry
,
result
):
"""
Tests the if the error is not SMTP-related, we log and reraise
...
...
@@ -148,19 +152,29 @@ class TestEmailErrors(ModuleStoreTestCase):
'subject'
:
'test subject 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
# 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
)
((
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
.
assertIn
(
'caused send_course_email task to fail with uncaught exception.'
,
log_str
)
self
.
assertEqual
(
email_id
,
1
)
self
.
assertEqual
(
to_list
,
[
self
.
instructor
.
email
])
#
self.assertIn('caused send_course_email task to fail with uncaught exception.', log_str)
#
self.assertEqual(email_id, 1)
#
self.assertEqual(to_list, [self.instructor.email])
self
.
assertFalse
(
retry
.
called
)
self
.
assertFalse
(
result
.
called
)
@patch
(
'bulk_email.tasks.course_email_result'
)
# @patch('bulk_email.tasks.delegate_email_batches.retry')
# 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.
# So use result.called_count to track this...
# 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'
)
def
test_nonexist_email
(
self
,
mock_log
,
result
):
"""
...
...
lms/djangoapps/instructor_task/api.py
View file @
01611c33
...
...
@@ -190,7 +190,6 @@ def submit_bulk_course_email(request, course_id, email_id):
"""
# check arguments: make sure that the course is defined?
# TODO: what is the right test here?
# modulestore().get_instance(course_id, problem_url)
# This should also make sure that the email exists.
# 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):
task_type
=
'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
#
with doing this manually. Shouldn't be hard to make the encode call explicitly,
#
and allow no problem_url or student to be defined. Like this:
#
task_input, task_key = encode_problem_and_student_input(
)
#
Pass in the to_option as a separate argument, even though it's (currently)
#
in the CourseEmail. That way it's visible in the progress status.
#
(At some point in the future, we might take the recipient out of the CourseEmail,
#
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_key_stub
=
"{email_id}_{to_option}"
.
format
(
email_id
=
email_id
,
to_option
=
to_option
)
# create the key value by using MD5 hash:
...
...
lms/djangoapps/instructor_task/tests/test_api.py
View file @
01611c33
...
...
@@ -6,16 +6,21 @@ from xmodule.modulestore.exceptions import ItemNotFoundError
from
courseware.tests.factories
import
UserFactory
from
instructor_task.api
import
(
get_running_instructor_tasks
,
get_instructor_task_history
,
submit_rescore_problem_for_all_students
,
submit_rescore_problem_for_student
,
submit_reset_problem_attempts_for_all_students
,
submit_delete_problem_state_for_all_students
)
from
bulk_email.models
import
CourseEmail
,
SEND_TO_ALL
from
instructor_task.api
import
(
get_running_instructor_tasks
,
get_instructor_task_history
,
submit_rescore_problem_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.models
import
InstructorTask
,
PROGRESS
from
instructor_task.tests.test_base
import
(
InstructorTaskTestCase
,
InstructorTaskCourseTestCase
,
InstructorTaskModuleTestCase
,
TEST_COURSE_ID
)
...
...
@@ -46,8 +51,8 @@ class InstructorTaskReportTest(InstructorTaskTestCase):
self
.
assertEquals
(
set
(
task_ids
),
set
(
expected_ids
))
class
InstructorTaskSubmitTest
(
InstructorTaskModuleTestCase
):
"""Tests API methods that involve the submission of background tasks."""
class
InstructorTask
Module
SubmitTest
(
InstructorTaskModuleTestCase
):
"""Tests API methods that involve the submission of
module-based
background tasks."""
def
setUp
(
self
):
self
.
initialize_course
()
...
...
@@ -136,3 +141,28 @@ class InstructorTaskSubmitTest(InstructorTaskModuleTestCase):
def
test_submit_delete_all
(
self
):
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 @
01611c33
...
...
@@ -96,10 +96,10 @@ class InstructorTaskTestCase(TestCase):
@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
the setup of a course
and problem in order to access StudentModule state
.
the setup of a course.
"""
course
=
None
current_user
=
None
...
...
@@ -150,6 +150,31 @@ class InstructorTaskModuleTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase)
return
self
.
_create_user
(
username
,
is_staff
=
False
)
@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
):
"""
Create an internal location for a test problem.
...
...
@@ -192,21 +217,3 @@ class InstructorTaskModuleTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase)
module_type
=
descriptor
.
location
.
category
,
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 @
01611c33
...
...
@@ -8,23 +8,23 @@ paths actually work.
import
json
from
uuid
import
uuid4
from
unittest
import
skip
from
functools
import
partial
from
mock
import
Mock
,
patch
from
mock
import
Mock
,
MagicMock
,
patch
from
celery.states
import
SUCCESS
,
FAILURE
from
xmodule.modulestore.exceptions
import
ItemNotFoundError
from
courseware.model
_data
import
StudentModule
from
courseware.model
s
import
StudentModule
from
courseware.tests.factories
import
StudentModuleFactory
from
student.tests.factories
import
UserFactory
from
student.tests.factories
import
UserFactory
,
CourseEnrollmentFactory
from
instructor_task.models
import
InstructorTask
from
instructor_task.tests.test_base
import
InstructorTaskModuleTestCase
from
instructor_task.tests.factories
import
InstructorTaskFactory
from
instructor_task.tasks
import
rescore_problem
,
reset_problem_attempts
,
delete_problem_state
from
instructor_task.tasks_helper
import
UpdateProblemModuleStateError
from
instructor_task.tasks_helper
import
UpdateProblemModuleStateError
,
run_main_task
,
perform_module_state_update
,
UPDATE_STATUS_SUCCEEDED
PROBLEM_URL_NAME
=
"test_urlname"
...
...
@@ -34,20 +34,27 @@ class TestTaskFailure(Exception):
class
TestInstructorTasks
(
InstructorTaskModuleTestCase
):
def
setUp
(
self
):
super
(
InstructorTaskModuleTestCase
,
self
)
.
setUp
()
self
.
initialize_course
()
self
.
instructor
=
self
.
create_instructor
(
'instructor'
)
self
.
problem_url
=
InstructorTaskModuleTestCase
.
problem_location
(
PROBLEM_URL_NAME
)
def
_create_input_entry
(
self
,
student_ident
=
None
):
def
_create_input_entry
(
self
,
student_ident
=
None
,
use_problem_url
=
True
,
course_id
=
None
,
task_input
=
None
):
"""Creates a InstructorTask entry for testing."""
task_id
=
str
(
uuid4
())
task_input
=
{
'problem_url'
:
self
.
problem_url
}
if
task_input
is
None
:
task_input
=
{}
else
:
task_input
=
dict
(
task_input
)
if
use_problem_url
:
task_input
[
'problem_url'
]
=
self
.
problem_url
if
student_ident
is
not
None
:
task_input
[
'student'
]
=
student_ident
instructor_task
=
InstructorTaskFactory
.
create
(
course_id
=
self
.
course
.
id
,
course_id
=
course_id
or
self
.
course
.
id
instructor_task
=
InstructorTaskFactory
.
create
(
course_id
=
course_id
,
requester
=
self
.
instructor
,
task_input
=
json
.
dumps
(
task_input
),
task_key
=
'dummy value'
,
...
...
@@ -80,14 +87,11 @@ class TestInstructorTasks(InstructorTaskModuleTestCase):
with
self
.
assertRaises
(
UpdateProblemModuleStateError
):
task_function
(
task_entry
.
id
,
self
.
_get_xmodule_instance_args
())
def
test_rescore_missing_current_task
(
self
):
self
.
_test_missing_current_task
(
rescore_problem
)
def
test_reset_missing_current_task
(
self
):
self
.
_test_missing_current_task
(
reset_problem_attempts
)
def
test_delete_missing_current_task
(
self
):
self
.
_test_missing_current_task
(
delete_problem_state
)
def
_test_undefined_course
(
self
,
task_function
):
# run with celery, but no course defined
task_entry
=
self
.
_create_input_entry
(
course_id
=
"bogus/course/id"
)
with
self
.
assertRaises
(
ItemNotFoundError
):
self
.
_run_task_with_mock_celery
(
task_function
,
task_entry
.
id
,
task_entry
.
task_id
)
def
_test_undefined_problem
(
self
,
task_function
):
"""Run with celery, but no problem defined."""
...
...
@@ -95,15 +99,6 @@ class TestInstructorTasks(InstructorTaskModuleTestCase):
with
self
.
assertRaises
(
ItemNotFoundError
):
self
.
_run_task_with_mock_celery
(
task_function
,
task_entry
.
id
,
task_entry
.
task_id
)
def
test_rescore_undefined_problem
(
self
):
self
.
_test_undefined_problem
(
rescore_problem
)
def
test_reset_undefined_problem
(
self
):
self
.
_test_undefined_problem
(
reset_problem_attempts
)
def
test_delete_undefined_problem
(
self
):
self
.
_test_undefined_problem
(
delete_problem_state
)
def
_test_run_with_task
(
self
,
task_function
,
action_name
,
expected_num_succeeded
):
"""Run a task and check the number of StudentModules processed."""
task_entry
=
self
.
_create_input_entry
()
...
...
@@ -124,16 +119,7 @@ class TestInstructorTasks(InstructorTaskModuleTestCase):
self
.
define_option_problem
(
PROBLEM_URL_NAME
)
self
.
_test_run_with_task
(
task_function
,
action_name
,
0
)
def
test_rescore_with_no_state
(
self
):
self
.
_test_run_with_no_state
(
rescore_problem
,
'rescored'
)
def
test_reset_with_no_state
(
self
):
self
.
_test_run_with_no_state
(
reset_problem_attempts
,
'reset'
)
def
test_delete_with_no_state
(
self
):
self
.
_test_run_with_no_state
(
delete_problem_state
,
'deleted'
)
def
_create_students_with_state
(
self
,
num_students
,
state
=
None
):
def
_create_students_with_state
(
self
,
num_students
,
state
=
None
,
grade
=
0
,
max_grade
=
1
):
"""Create students, a problem, and StudentModule objects for testing"""
self
.
define_option_problem
(
PROBLEM_URL_NAME
)
students
=
[
...
...
@@ -141,9 +127,12 @@ class TestInstructorTasks(InstructorTaskModuleTestCase):
for
i
in
xrange
(
num_students
)
]
for
student
in
students
:
CourseEnrollmentFactory
.
create
(
course_id
=
self
.
course
.
id
,
user
=
student
)
StudentModuleFactory
.
create
(
course_id
=
self
.
course
.
id
,
module_state_key
=
self
.
problem_url
,
student
=
student
,
grade
=
grade
,
max_grade
=
max_grade
,
state
=
state
)
return
students
...
...
@@ -156,86 +145,8 @@ class TestInstructorTasks(InstructorTaskModuleTestCase):
state
=
json
.
loads
(
module
.
state
)
self
.
assertEquals
(
state
[
'attempts'
],
num_attempts
)
def
test_reset_with_some_state
(
self
):
initial_attempts
=
3
input_state
=
json
.
dumps
({
'attempts'
:
initial_attempts
})
num_students
=
10
students
=
self
.
_create_students_with_state
(
num_students
,
input_state
)
# check that entries were set correctly
self
.
_assert_num_attempts
(
students
,
initial_attempts
)
# run the task
self
.
_test_run_with_task
(
reset_problem_attempts
,
'reset'
,
num_students
)
# check that entries were reset
self
.
_assert_num_attempts
(
students
,
0
)
def
test_delete_with_some_state
(
self
):
# This will create StudentModule entries -- we don't have to worry about
# the state inside them.
num_students
=
10
students
=
self
.
_create_students_with_state
(
num_students
)
# check that entries were created correctly
for
student
in
students
:
StudentModule
.
objects
.
get
(
course_id
=
self
.
course
.
id
,
student
=
student
,
module_state_key
=
self
.
problem_url
)
self
.
_test_run_with_task
(
delete_problem_state
,
'deleted'
,
num_students
)
# confirm that no state can be found anymore:
for
student
in
students
:
with
self
.
assertRaises
(
StudentModule
.
DoesNotExist
):
StudentModule
.
objects
.
get
(
course_id
=
self
.
course
.
id
,
student
=
student
,
module_state_key
=
self
.
problem_url
)
def
_test_reset_with_student
(
self
,
use_email
):
"""Run a reset task for one student, with several StudentModules for the problem defined."""
num_students
=
10
initial_attempts
=
3
input_state
=
json
.
dumps
({
'attempts'
:
initial_attempts
})
students
=
self
.
_create_students_with_state
(
num_students
,
input_state
)
# check that entries were set correctly
for
student
in
students
:
module
=
StudentModule
.
objects
.
get
(
course_id
=
self
.
course
.
id
,
student
=
student
,
module_state_key
=
self
.
problem_url
)
state
=
json
.
loads
(
module
.
state
)
self
.
assertEquals
(
state
[
'attempts'
],
initial_attempts
)
if
use_email
:
student_ident
=
students
[
3
]
.
email
else
:
student_ident
=
students
[
3
]
.
username
task_entry
=
self
.
_create_input_entry
(
student_ident
)
status
=
self
.
_run_task_with_mock_celery
(
reset_problem_attempts
,
task_entry
.
id
,
task_entry
.
task_id
)
# check return value
self
.
assertEquals
(
status
.
get
(
'attempted'
),
1
)
self
.
assertEquals
(
status
.
get
(
'succeeded'
),
1
)
self
.
assertEquals
(
status
.
get
(
'total'
),
1
)
self
.
assertEquals
(
status
.
get
(
'action_name'
),
'reset'
)
self
.
assertGreater
(
'duration_ms'
,
0
)
# compare with entry in table:
entry
=
InstructorTask
.
objects
.
get
(
id
=
task_entry
.
id
)
self
.
assertEquals
(
json
.
loads
(
entry
.
task_output
),
status
)
self
.
assertEquals
(
entry
.
task_state
,
SUCCESS
)
# check that the correct entry was reset
for
index
,
student
in
enumerate
(
students
):
module
=
StudentModule
.
objects
.
get
(
course_id
=
self
.
course
.
id
,
student
=
student
,
module_state_key
=
self
.
problem_url
)
state
=
json
.
loads
(
module
.
state
)
if
index
==
3
:
self
.
assertEquals
(
state
[
'attempts'
],
0
)
else
:
self
.
assertEquals
(
state
[
'attempts'
],
initial_attempts
)
def
test_reset_with_student_username
(
self
):
self
.
_test_reset_with_student
(
False
)
def
test_reset_with_student_email
(
self
):
self
.
_test_reset_with_student
(
True
)
def
_test_run_with_failure
(
self
,
task_function
,
expected_message
):
"""Run a task and trigger an artificial failure with
give
message."""
"""Run a task and trigger an artificial failure with
the given
message."""
task_entry
=
self
.
_create_input_entry
()
self
.
define_option_problem
(
PROBLEM_URL_NAME
)
with
self
.
assertRaises
(
TestTaskFailure
):
...
...
@@ -247,15 +158,6 @@ class TestInstructorTasks(InstructorTaskModuleTestCase):
self
.
assertEquals
(
output
[
'exception'
],
'TestTaskFailure'
)
self
.
assertEquals
(
output
[
'message'
],
expected_message
)
def
test_rescore_with_failure
(
self
):
self
.
_test_run_with_failure
(
rescore_problem
,
'We expected this to fail'
)
def
test_reset_with_failure
(
self
):
self
.
_test_run_with_failure
(
reset_problem_attempts
,
'We expected this to fail'
)
def
test_delete_with_failure
(
self
):
self
.
_test_run_with_failure
(
delete_problem_state
,
'We expected this to fail'
)
def
_test_run_with_long_error_msg
(
self
,
task_function
):
"""
Run with an error message that is so long it will require
...
...
@@ -275,15 +177,6 @@ class TestInstructorTasks(InstructorTaskModuleTestCase):
self
.
assertEquals
(
output
[
'message'
],
expected_message
[:
len
(
output
[
'message'
])
-
3
]
+
"..."
)
self
.
assertTrue
(
'traceback'
not
in
output
)
def
test_rescore_with_long_error_msg
(
self
):
self
.
_test_run_with_long_error_msg
(
rescore_problem
)
def
test_reset_with_long_error_msg
(
self
):
self
.
_test_run_with_long_error_msg
(
reset_problem_attempts
)
def
test_delete_with_long_error_msg
(
self
):
self
.
_test_run_with_long_error_msg
(
delete_problem_state
)
def
_test_run_with_short_error_msg
(
self
,
task_function
):
"""
Run with an error message that is short enough to fit
...
...
@@ -304,27 +197,22 @@ class TestInstructorTasks(InstructorTaskModuleTestCase):
self
.
assertEquals
(
output
[
'message'
],
expected_message
)
self
.
assertEquals
(
output
[
'traceback'
][
-
3
:],
"..."
)
def
test_rescore_with_short_error_msg
(
self
):
self
.
_test_run_with_short_error_msg
(
rescore_problem
)
def
test_reset_with_short_error_msg
(
self
):
self
.
_test_run_with_short_error_msg
(
reset_problem_attempts
)
class
TestGeneralInstructorTask
(
TestInstructorTasks
):
"""Tests instructor task mechanism using custom tasks"""
def
test_delete_with_short_error_msg
(
self
):
self
.
_test_run_with_short_error_msg
(
delete_problem_state
)
def
teDONTst_successful_result_too_long
(
self
):
def
test_successful_result_too_long
(
self
):
# while we don't expect the existing tasks to generate output that is too
# long, we can test the framework will handle such an occurrence.
task_entry
=
self
.
_create_input_entry
()
self
.
define_option_problem
(
PROBLEM_URL_NAME
)
action_name
=
'x'
*
1000
update_fcn
=
lambda
(
_module_descriptor
,
_student_module
,
_xmodule_instance_args
):
True
# task_function = (lambda entry_id, xmodule_instance_args:
# update_problem_module_state(entry_id,
# update_fcn, action_name, filter_fcn=None,
# xmodule_instance_args=Non
e))
# define a custom task that does nothing:
update_fcn
=
lambda
(
_module_descriptor
,
_student_module
):
UPDATE_STATUS_SUCCEEDED
visit_fcn
=
partial
(
perform_module_state_update
,
update_fcn
,
None
)
task_function
=
(
lambda
entry_id
,
xmodule_instance_args
:
run_main_task
(
entry_id
,
visit_fcn
,
action_nam
e
))
# run the task:
with
self
.
assertRaises
(
ValueError
):
self
.
_run_task_with_mock_celery
(
task_function
,
task_entry
.
id
,
task_entry
.
task_id
)
# compare with entry in table:
...
...
@@ -336,16 +224,45 @@ class TestInstructorTasks(InstructorTaskModuleTestCase):
self
.
assertTrue
(
"Length of task output is too long"
in
output
[
'message'
])
self
.
assertTrue
(
'traceback'
not
in
output
)
class
TestRescoreInstructorTask
(
TestInstructorTasks
):
"""Tests problem-rescoring instructor task."""
def
test_rescore_missing_current_task
(
self
):
self
.
_test_missing_current_task
(
rescore_problem
)
def
test_rescore_undefined_course
(
self
):
self
.
_test_undefined_course
(
rescore_problem
)
def
test_rescore_undefined_problem
(
self
):
self
.
_test_undefined_problem
(
rescore_problem
)
def
test_rescore_with_no_state
(
self
):
self
.
_test_run_with_no_state
(
rescore_problem
,
'rescored'
)
def
test_rescore_with_failure
(
self
):
self
.
_test_run_with_failure
(
rescore_problem
,
'We expected this to fail'
)
def
test_rescore_with_long_error_msg
(
self
):
self
.
_test_run_with_long_error_msg
(
rescore_problem
)
def
test_rescore_with_short_error_msg
(
self
):
self
.
_test_run_with_short_error_msg
(
rescore_problem
)
@skip
def
test_rescoring_unrescorable
(
self
):
# TODO: this test needs to have Mako templates initialized
# to make sure that the creation of an XModule works.
input_state
=
json
.
dumps
({
'done'
:
True
})
num_students
=
1
self
.
_create_students_with_state
(
num_students
,
input_state
)
task_entry
=
self
.
_create_input_entry
()
with
self
.
assertRaises
(
UpdateProblemModuleStateError
):
self
.
_run_task_with_mock_celery
(
rescore_problem
,
task_entry
.
id
,
task_entry
.
task_id
)
mock_instance
=
MagicMock
()
del
mock_instance
.
rescore_problem
# TODO: figure out why this patch isn't working
# with patch('courseware.module_render.get_module_for_descriptor_internal') as mock_get_module:
with
patch
(
'courseware.module_render.get_module_for_descriptor_internal'
)
as
mock_get_module
:
mock_get_module
.
return_value
=
mock_instance
with
self
.
assertRaises
(
UpdateProblemModuleStateError
):
self
.
_run_task_with_mock_celery
(
rescore_problem
,
task_entry
.
id
,
task_entry
.
task_id
)
# check values stored in table:
entry
=
InstructorTask
.
objects
.
get
(
id
=
task_entry
.
id
)
output
=
json
.
loads
(
entry
.
task_output
)
...
...
@@ -353,17 +270,13 @@ class TestInstructorTasks(InstructorTaskModuleTestCase):
self
.
assertEquals
(
output
[
'message'
],
"Specified problem does not support rescoring."
)
self
.
assertGreater
(
len
(
output
[
'traceback'
]),
0
)
@skip
def
test_rescoring_success
(
self
):
# TODO: this test needs to have Mako templates initialized
# to make sure that the creation of an XModule works.
input_state
=
json
.
dumps
({
'done'
:
True
})
num_students
=
10
self
.
_create_students_with_state
(
num_students
,
input_state
)
task_entry
=
self
.
_create_input_entry
()
mock_instance
=
Mock
()
mock_instance
.
rescore_problem
=
Mock
({
'success'
:
'correct'
})
# TODO: figure out why this mock is not working....
with
patch
(
'courseware.module_render.get_module_for_descriptor_internal'
)
as
mock_get_module
:
mock_get_module
.
return_value
=
mock_instance
self
.
_run_task_with_mock_celery
(
rescore_problem
,
task_entry
.
id
,
task_entry
.
task_id
)
...
...
@@ -375,3 +288,131 @@ class TestInstructorTasks(InstructorTaskModuleTestCase):
self
.
assertEquals
(
output
.
get
(
'total'
),
num_students
)
self
.
assertEquals
(
output
.
get
(
'action_name'
),
'rescored'
)
self
.
assertGreater
(
'duration_ms'
,
0
)
class
TestResetAttemptsInstructorTask
(
TestInstructorTasks
):
"""Tests instructor task that resets problem attempts."""
def
test_reset_missing_current_task
(
self
):
self
.
_test_missing_current_task
(
reset_problem_attempts
)
def
test_reset_undefined_course
(
self
):
self
.
_test_undefined_course
(
reset_problem_attempts
)
def
test_reset_undefined_problem
(
self
):
self
.
_test_undefined_problem
(
reset_problem_attempts
)
def
test_reset_with_no_state
(
self
):
self
.
_test_run_with_no_state
(
reset_problem_attempts
,
'reset'
)
def
test_reset_with_failure
(
self
):
self
.
_test_run_with_failure
(
reset_problem_attempts
,
'We expected this to fail'
)
def
test_reset_with_long_error_msg
(
self
):
self
.
_test_run_with_long_error_msg
(
reset_problem_attempts
)
def
test_reset_with_short_error_msg
(
self
):
self
.
_test_run_with_short_error_msg
(
reset_problem_attempts
)
def
test_reset_with_some_state
(
self
):
initial_attempts
=
3
input_state
=
json
.
dumps
({
'attempts'
:
initial_attempts
})
num_students
=
10
students
=
self
.
_create_students_with_state
(
num_students
,
input_state
)
# check that entries were set correctly
self
.
_assert_num_attempts
(
students
,
initial_attempts
)
# run the task
self
.
_test_run_with_task
(
reset_problem_attempts
,
'reset'
,
num_students
)
# check that entries were reset
self
.
_assert_num_attempts
(
students
,
0
)
def
_test_reset_with_student
(
self
,
use_email
):
"""Run a reset task for one student, with several StudentModules for the problem defined."""
num_students
=
10
initial_attempts
=
3
input_state
=
json
.
dumps
({
'attempts'
:
initial_attempts
})
students
=
self
.
_create_students_with_state
(
num_students
,
input_state
)
# check that entries were set correctly
for
student
in
students
:
module
=
StudentModule
.
objects
.
get
(
course_id
=
self
.
course
.
id
,
student
=
student
,
module_state_key
=
self
.
problem_url
)
state
=
json
.
loads
(
module
.
state
)
self
.
assertEquals
(
state
[
'attempts'
],
initial_attempts
)
if
use_email
:
student_ident
=
students
[
3
]
.
email
else
:
student_ident
=
students
[
3
]
.
username
task_entry
=
self
.
_create_input_entry
(
student_ident
)
status
=
self
.
_run_task_with_mock_celery
(
reset_problem_attempts
,
task_entry
.
id
,
task_entry
.
task_id
)
# check return value
self
.
assertEquals
(
status
.
get
(
'attempted'
),
1
)
self
.
assertEquals
(
status
.
get
(
'succeeded'
),
1
)
self
.
assertEquals
(
status
.
get
(
'total'
),
1
)
self
.
assertEquals
(
status
.
get
(
'action_name'
),
'reset'
)
self
.
assertGreater
(
'duration_ms'
,
0
)
# compare with entry in table:
entry
=
InstructorTask
.
objects
.
get
(
id
=
task_entry
.
id
)
self
.
assertEquals
(
json
.
loads
(
entry
.
task_output
),
status
)
self
.
assertEquals
(
entry
.
task_state
,
SUCCESS
)
# check that the correct entry was reset
for
index
,
student
in
enumerate
(
students
):
module
=
StudentModule
.
objects
.
get
(
course_id
=
self
.
course
.
id
,
student
=
student
,
module_state_key
=
self
.
problem_url
)
state
=
json
.
loads
(
module
.
state
)
if
index
==
3
:
self
.
assertEquals
(
state
[
'attempts'
],
0
)
else
:
self
.
assertEquals
(
state
[
'attempts'
],
initial_attempts
)
def
test_reset_with_student_username
(
self
):
self
.
_test_reset_with_student
(
False
)
def
test_reset_with_student_email
(
self
):
self
.
_test_reset_with_student
(
True
)
class
TestDeleteStateInstructorTask
(
TestInstructorTasks
):
"""Tests instructor task that deletes problem state."""
def
test_delete_missing_current_task
(
self
):
self
.
_test_missing_current_task
(
delete_problem_state
)
def
test_delete_undefined_course
(
self
):
self
.
_test_undefined_course
(
delete_problem_state
)
def
test_delete_undefined_problem
(
self
):
self
.
_test_undefined_problem
(
delete_problem_state
)
def
test_delete_with_no_state
(
self
):
self
.
_test_run_with_no_state
(
delete_problem_state
,
'deleted'
)
def
test_delete_with_failure
(
self
):
self
.
_test_run_with_failure
(
delete_problem_state
,
'We expected this to fail'
)
def
test_delete_with_long_error_msg
(
self
):
self
.
_test_run_with_long_error_msg
(
delete_problem_state
)
def
test_delete_with_short_error_msg
(
self
):
self
.
_test_run_with_short_error_msg
(
delete_problem_state
)
def
test_delete_with_some_state
(
self
):
# This will create StudentModule entries -- we don't have to worry about
# the state inside them.
num_students
=
10
students
=
self
.
_create_students_with_state
(
num_students
)
# check that entries were created correctly
for
student
in
students
:
StudentModule
.
objects
.
get
(
course_id
=
self
.
course
.
id
,
student
=
student
,
module_state_key
=
self
.
problem_url
)
self
.
_test_run_with_task
(
delete_problem_state
,
'deleted'
,
num_students
)
# confirm that no state can be found anymore:
for
student
in
students
:
with
self
.
assertRaises
(
StudentModule
.
DoesNotExist
):
StudentModule
.
objects
.
get
(
course_id
=
self
.
course
.
id
,
student
=
student
,
module_state_key
=
self
.
problem_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