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
d48e90ee
Commit
d48e90ee
authored
Sep 11, 2013
by
Brian Wilson
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Initial refactoring for bulk_email monitoring.
parent
5d47779b
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
245 additions
and
55 deletions
+245
-55
lms/djangoapps/bulk_email/tasks.py
+0
-0
lms/djangoapps/bulk_email/tests/test_email.py
+6
-3
lms/djangoapps/bulk_email/tests/test_err_handling.py
+27
-15
lms/djangoapps/instructor/views/legacy.py
+12
-6
lms/djangoapps/instructor_task/api.py
+51
-4
lms/djangoapps/instructor_task/api_helper.py
+6
-3
lms/djangoapps/instructor_task/migrations/0002_add_subtask_field.py
+77
-0
lms/djangoapps/instructor_task/models.py
+1
-0
lms/djangoapps/instructor_task/tasks.py
+34
-14
lms/djangoapps/instructor_task/tasks_helper.py
+0
-0
lms/djangoapps/instructor_task/tests/test_tasks.py
+6
-6
lms/djangoapps/instructor_task/tests/test_views.py
+1
-1
lms/djangoapps/instructor_task/views.py
+24
-3
No files found.
lms/djangoapps/bulk_email/tasks.py
View file @
d48e90ee
This diff is collapsed.
Click to expand it.
lms/djangoapps/bulk_email/tests/test_email.py
View file @
d48e90ee
...
...
@@ -13,7 +13,7 @@ from student.tests.factories import UserFactory, GroupFactory, CourseEnrollmentF
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
bulk_email.tasks
import
delegate_email_batches
,
course_email
from
bulk_email.tasks
import
send_
course_email
from
bulk_email.models
import
CourseEmail
,
Optout
from
mock
import
patch
...
...
@@ -289,6 +289,9 @@ class TestEmailSendExceptions(ModuleStoreTestCase):
Test that exceptions are handled correctly.
"""
def
test_no_course_email_obj
(
self
):
# Make sure course_email handles CourseEmail.DoesNotExist exception.
# Make sure send_course_email handles CourseEmail.DoesNotExist exception.
with
self
.
assertRaises
(
KeyError
):
send_course_email
(
101
,
[],
{},
False
)
with
self
.
assertRaises
(
CourseEmail
.
DoesNotExist
):
course_email
(
101
,
[],
"_"
,
"_"
,
"_"
,
False
)
send_course_email
(
101
,
[],
{
'course_title'
:
'Test'
}
,
False
)
lms/djangoapps/bulk_email/tests/test_err_handling.py
View file @
d48e90ee
...
...
@@ -13,7 +13,8 @@ from xmodule.modulestore.tests.factories import CourseFactory
from
student.tests.factories
import
UserFactory
,
AdminFactory
,
CourseEnrollmentFactory
from
bulk_email.models
import
CourseEmail
from
bulk_email.tasks
import
delegate_email_batches
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
...
...
@@ -43,7 +44,7 @@ class TestEmailErrors(ModuleStoreTestCase):
patch
.
stopall
()
@patch
(
'bulk_email.tasks.get_connection'
,
autospec
=
True
)
@patch
(
'bulk_email.tasks.course_email.retry'
)
@patch
(
'bulk_email.tasks.
send_
course_email.retry'
)
def
test_data_err_retry
(
self
,
retry
,
get_conn
):
"""
Test that celery handles transient SMTPDataErrors by retrying.
...
...
@@ -65,7 +66,7 @@ class TestEmailErrors(ModuleStoreTestCase):
@patch
(
'bulk_email.tasks.get_connection'
,
autospec
=
True
)
@patch
(
'bulk_email.tasks.course_email_result'
)
@patch
(
'bulk_email.tasks.course_email.retry'
)
@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.
...
...
@@ -93,7 +94,7 @@ class TestEmailErrors(ModuleStoreTestCase):
self
.
assertEquals
(
sent
,
settings
.
EMAILS_PER_TASK
/
2
)
@patch
(
'bulk_email.tasks.get_connection'
,
autospec
=
True
)
@patch
(
'bulk_email.tasks.course_email.retry'
)
@patch
(
'bulk_email.tasks.
send_
course_email.retry'
)
def
test_disconn_err_retry
(
self
,
retry
,
get_conn
):
"""
Test that celery handles SMTPServerDisconnected by retrying.
...
...
@@ -113,7 +114,7 @@ class TestEmailErrors(ModuleStoreTestCase):
self
.
assertIsInstance
(
exc
,
SMTPServerDisconnected
)
@patch
(
'bulk_email.tasks.get_connection'
,
autospec
=
True
)
@patch
(
'bulk_email.tasks.course_email.retry'
)
@patch
(
'bulk_email.tasks.
send_
course_email.retry'
)
def
test_conn_err_retry
(
self
,
retry
,
get_conn
):
"""
Test that celery handles SMTPConnectError by retrying.
...
...
@@ -134,7 +135,7 @@ class TestEmailErrors(ModuleStoreTestCase):
self
.
assertIsInstance
(
exc
,
SMTPConnectError
)
@patch
(
'bulk_email.tasks.course_email_result'
)
@patch
(
'bulk_email.tasks.course_email.retry'
)
@patch
(
'bulk_email.tasks.
send_
course_email.retry'
)
@patch
(
'bulk_email.tasks.log'
)
@patch
(
'bulk_email.tasks.get_connection'
,
Mock
(
return_value
=
EmailTestException
))
def
test_general_exception
(
self
,
mock_log
,
retry
,
result
):
...
...
@@ -152,25 +153,29 @@ class TestEmailErrors(ModuleStoreTestCase):
self
.
client
.
post
(
self
.
url
,
test_email
)
((
log_str
,
email_id
,
to_list
),
_
)
=
mock_log
.
exception
.
call_args
self
.
assertTrue
(
mock_log
.
exception
.
called
)
self
.
assertIn
(
'caused 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
(
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'
)
#
@patch('bulk_email.tasks.delegate_email_batches.retry')
@patch
(
'bulk_email.tasks.log'
)
def
test_nonexist_email
(
self
,
mock_log
,
re
try
,
re
sult
):
def
test_nonexist_email
(
self
,
mock_log
,
result
):
"""
Tests retries when the email doesn't exist
"""
delegate_email_batches
.
delay
(
-
1
,
self
.
instructor
.
id
)
((
log_str
,
email_id
,
_num_retries
),
_
)
=
mock_log
.
warning
.
call_args
# create an InstructorTask object to pass through
course_id
=
self
.
course
.
id
entry
=
InstructorTask
.
create
(
course_id
,
"task_type"
,
"task_key"
,
"task_input"
,
self
.
instructor
)
task_input
=
{
"email_id"
:
-
1
}
with
self
.
assertRaises
(
CourseEmail
.
DoesNotExist
):
perform_delegate_email_batches
(
entry
.
id
,
course_id
,
task_input
,
"action_name"
)
((
log_str
,
email_id
),
_
)
=
mock_log
.
warning
.
call_args
self
.
assertTrue
(
mock_log
.
warning
.
called
)
self
.
assertIn
(
'Failed to get CourseEmail with id'
,
log_str
)
self
.
assertEqual
(
email_id
,
-
1
)
self
.
assertTrue
(
retry
.
called
)
self
.
assertFalse
(
result
.
called
)
@patch
(
'bulk_email.tasks.log'
)
...
...
@@ -178,9 +183,13 @@ class TestEmailErrors(ModuleStoreTestCase):
"""
Tests exception when the course in the email doesn't exist
"""
email
=
CourseEmail
(
course_id
=
"I/DONT/EXIST"
)
course_id
=
"I/DONT/EXIST"
email
=
CourseEmail
(
course_id
=
course_id
)
email
.
save
()
delegate_email_batches
.
delay
(
email
.
id
,
self
.
instructor
.
id
)
entry
=
InstructorTask
.
create
(
course_id
,
"task_type"
,
"task_key"
,
"task_input"
,
self
.
instructor
)
task_input
=
{
"email_id"
:
email
.
id
}
with
self
.
assertRaises
(
Exception
):
perform_delegate_email_batches
(
entry
.
id
,
course_id
,
task_input
,
"action_name"
)
((
log_str
,
_
),
_
)
=
mock_log
.
exception
.
call_args
self
.
assertTrue
(
mock_log
.
exception
.
called
)
self
.
assertIn
(
'get_course_by_id failed:'
,
log_str
)
...
...
@@ -192,7 +201,10 @@ class TestEmailErrors(ModuleStoreTestCase):
"""
email
=
CourseEmail
(
course_id
=
self
.
course
.
id
,
to_option
=
"IDONTEXIST"
)
email
.
save
()
delegate_email_batches
.
delay
(
email
.
id
,
self
.
instructor
.
id
)
entry
=
InstructorTask
.
create
(
self
.
course
.
id
,
"task_type"
,
"task_key"
,
"task_input"
,
self
.
instructor
)
task_input
=
{
"email_id"
:
email
.
id
}
with
self
.
assertRaises
(
Exception
):
perform_delegate_email_batches
(
entry
.
id
,
self
.
course
.
id
,
task_input
,
"action_name"
)
((
log_str
,
opt_str
),
_
)
=
mock_log
.
error
.
call_args
self
.
assertTrue
(
mock_log
.
error
.
called
)
self
.
assertIn
(
'Unexpected bulk email TO_OPTION found'
,
log_str
)
...
...
lms/djangoapps/instructor/views/legacy.py
View file @
d48e90ee
...
...
@@ -46,7 +46,8 @@ 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_reset_problem_attempts_for_all_students
,
submit_bulk_course_email
)
from
instructor_task.views
import
get_task_completion_info
from
mitxmako.shortcuts
import
render_to_response
from
psychometrics
import
psychoanalyze
...
...
@@ -722,6 +723,13 @@ def instructor_dashboard(request, course_id):
html_message
=
request
.
POST
.
get
(
"message"
)
text_message
=
html_to_text
(
html_message
)
# TODO: make sure this is committed before submitting it to the task.
# However, it should probably be enough to do the submit below, which
# will commit the transaction for the InstructorTask object. Both should
# therefore be committed. (Still, it might be clearer to do so here as well.)
# Actually, this should probably be moved out, so that all the validation logic
# we might want to add to it can be added. There might also be something
# that would permit validation of the email beforehand.
email
=
CourseEmail
(
course_id
=
course_id
,
sender
=
request
.
user
,
...
...
@@ -730,13 +738,11 @@ def instructor_dashboard(request, course_id):
html_message
=
html_message
,
text_message
=
text_message
)
email
.
save
()
tasks
.
delegate_email_batches
.
delay
(
email
.
id
,
request
.
user
.
id
)
# TODO: make this into a task submission, so that the correct
# InstructorTask object gets created (for monitoring purposes)
submit_bulk_course_email
(
request
,
course_id
,
email
.
id
)
if
email_to_option
==
"all"
:
email_msg
=
'<div class="msg msg-confirm"><p class="copy">Your email was successfully queued for sending. Please note that for large public classes (~10k), it may take 1-2 hours to send all emails.</p></div>'
...
...
lms/djangoapps/instructor_task/api.py
View file @
d48e90ee
...
...
@@ -6,6 +6,7 @@ already been submitted, filtered either by running state or input
arguments.
"""
import
hashlib
from
celery.states
import
READY_STATES
...
...
@@ -14,11 +15,13 @@ from xmodule.modulestore.django import modulestore
from
instructor_task.models
import
InstructorTask
from
instructor_task.tasks
import
(
rescore_problem
,
reset_problem_attempts
,
delete_problem_state
)
delete_problem_state
,
send_bulk_course_email
)
from
instructor_task.api_helper
import
(
check_arguments_for_rescoring
,
encode_problem_and_student_input
,
submit_task
)
from
bulk_email.models
import
CourseEmail
def
get_running_instructor_tasks
(
course_id
):
...
...
@@ -34,14 +37,18 @@ def get_running_instructor_tasks(course_id):
return
instructor_tasks
.
order_by
(
'-id'
)
def
get_instructor_task_history
(
course_id
,
problem_url
,
student
=
None
):
def
get_instructor_task_history
(
course_id
,
problem_url
=
None
,
student
=
None
,
task_type
=
None
):
"""
Returns a query of InstructorTask objects of historical tasks for a given course,
that
match a particular problem and optionally a student
.
that
optionally match a particular problem, a student, and/or a task type
.
"""
instructor_tasks
=
InstructorTask
.
objects
.
filter
(
course_id
=
course_id
)
if
problem_url
is
not
None
or
student
is
not
None
:
_
,
task_key
=
encode_problem_and_student_input
(
problem_url
,
student
)
instructor_tasks
=
instructor_tasks
.
filter
(
task_key
=
task_key
)
if
task_type
is
not
None
:
instructor_tasks
=
instructor_tasks
.
filter
(
task_type
=
task_type
)
instructor_tasks
=
InstructorTask
.
objects
.
filter
(
course_id
=
course_id
,
task_key
=
task_key
)
return
instructor_tasks
.
order_by
(
'-id'
)
...
...
@@ -162,3 +169,43 @@ def submit_delete_problem_state_for_all_students(request, course_id, problem_url
task_class
=
delete_problem_state
task_input
,
task_key
=
encode_problem_and_student_input
(
problem_url
)
return
submit_task
(
request
,
task_type
,
task_class
,
course_id
,
task_input
,
task_key
)
def
submit_bulk_course_email
(
request
,
course_id
,
email_id
):
"""
Request to have bulk email sent as a background task.
The specified CourseEmail object will be sent be updated for all students who have enrolled
in a course. Parameters are the `course_id` and the `email_id`, the id of the CourseEmail object.
AlreadyRunningError is raised if the course's students are already being emailed.
TODO: is this the right behavior? Or should multiple emails be allowed in the pipeline at the same time?
This method makes sure the InstructorTask entry is committed.
When called from any view that is wrapped by TransactionMiddleware,
and thus in a "commit-on-success" transaction, an autocommit buried within here
will cause any pending transaction to be committed by a successful
save here. Any future database operations will take place in a
separate transaction.
"""
# 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
# the InstructorTask status.
email_obj
=
CourseEmail
.
objects
.
get
(
id
=
email_id
)
to_option
=
email_obj
.
to_option
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()
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:
task_key
=
hashlib
.
md5
(
task_key_stub
)
.
hexdigest
()
return
submit_task
(
request
,
task_type
,
task_class
,
course_id
,
task_input
,
task_key
)
lms/djangoapps/instructor_task/api_helper.py
View file @
d48e90ee
...
...
@@ -58,13 +58,14 @@ def _reserve_task(course_id, task_type, task_key, task_input, requester):
return
InstructorTask
.
create
(
course_id
,
task_type
,
task_key
,
task_input
,
requester
)
def
_get_xmodule_instance_args
(
request
):
def
_get_xmodule_instance_args
(
request
,
task_id
):
"""
Calculate parameters needed for instantiating xmodule instances.
The `request_info` will be passed to a tracking log function, to provide information
about the source of the task request. The `xqueue_callback_url_prefix` is used to
permit old-style xqueue callbacks directly to the appropriate module in the LMS.
The `task_id` is also passed to the tracking log function.
"""
request_info
=
{
'username'
:
request
.
user
.
username
,
'ip'
:
request
.
META
[
'REMOTE_ADDR'
],
...
...
@@ -74,6 +75,7 @@ def _get_xmodule_instance_args(request):
xmodule_instance_args
=
{
'xqueue_callback_url_prefix'
:
get_xqueue_callback_url_prefix
(
request
),
'request_info'
:
request_info
,
'task_id'
:
task_id
,
}
return
xmodule_instance_args
...
...
@@ -214,7 +216,7 @@ def check_arguments_for_rescoring(course_id, problem_url):
def
encode_problem_and_student_input
(
problem_url
,
student
=
None
):
"""
Encode problem_url and optional student into task_key and task_input values.
Encode
optional
problem_url and optional student into task_key and task_input values.
`problem_url` is full URL of the problem.
`student` is the user object of the student
...
...
@@ -257,7 +259,7 @@ def submit_task(request, task_type, task_class, course_id, task_input, task_key)
# submit task:
task_id
=
instructor_task
.
task_id
task_args
=
[
instructor_task
.
id
,
_get_xmodule_instance_args
(
request
)]
task_args
=
[
instructor_task
.
id
,
_get_xmodule_instance_args
(
request
,
task_id
)]
task_class
.
apply_async
(
task_args
,
task_id
=
task_id
)
return
instructor_task
\ No newline at end of file
lms/djangoapps/instructor_task/migrations/0002_add_subtask_field.py
0 → 100644
View file @
d48e90ee
# -*- coding: utf-8 -*-
import
datetime
from
south.db
import
db
from
south.v2
import
SchemaMigration
from
django.db
import
models
class
Migration
(
SchemaMigration
):
def
forwards
(
self
,
orm
):
# Adding field 'InstructorTask.subtasks'
db
.
add_column
(
'instructor_task_instructortask'
,
'subtasks'
,
self
.
gf
(
'django.db.models.fields.TextField'
)(
default
=
''
,
blank
=
True
),
keep_default
=
False
)
def
backwards
(
self
,
orm
):
# Deleting field 'InstructorTask.subtasks'
db
.
delete_column
(
'instructor_task_instructortask'
,
'subtasks'
)
models
=
{
'auth.group'
:
{
'Meta'
:
{
'object_name'
:
'Group'
},
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'unique'
:
'True'
,
'max_length'
:
'80'
}),
'permissions'
:
(
'django.db.models.fields.related.ManyToManyField'
,
[],
{
'to'
:
"orm['auth.Permission']"
,
'symmetrical'
:
'False'
,
'blank'
:
'True'
})
},
'auth.permission'
:
{
'Meta'
:
{
'ordering'
:
"('content_type__app_label', 'content_type__model', 'codename')"
,
'unique_together'
:
"(('content_type', 'codename'),)"
,
'object_name'
:
'Permission'
},
'codename'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'100'
}),
'content_type'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'to'
:
"orm['contenttypes.ContentType']"
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'50'
})
},
'auth.user'
:
{
'Meta'
:
{
'object_name'
:
'User'
},
'date_joined'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'email'
:
(
'django.db.models.fields.EmailField'
,
[],
{
'max_length'
:
'75'
,
'blank'
:
'True'
}),
'first_name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'30'
,
'blank'
:
'True'
}),
'groups'
:
(
'django.db.models.fields.related.ManyToManyField'
,
[],
{
'to'
:
"orm['auth.Group']"
,
'symmetrical'
:
'False'
,
'blank'
:
'True'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'is_active'
:
(
'django.db.models.fields.BooleanField'
,
[],
{
'default'
:
'True'
}),
'is_staff'
:
(
'django.db.models.fields.BooleanField'
,
[],
{
'default'
:
'False'
}),
'is_superuser'
:
(
'django.db.models.fields.BooleanField'
,
[],
{
'default'
:
'False'
}),
'last_login'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'last_name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'30'
,
'blank'
:
'True'
}),
'password'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'128'
}),
'user_permissions'
:
(
'django.db.models.fields.related.ManyToManyField'
,
[],
{
'to'
:
"orm['auth.Permission']"
,
'symmetrical'
:
'False'
,
'blank'
:
'True'
}),
'username'
:
(
'django.db.models.fields.CharField'
,
[],
{
'unique'
:
'True'
,
'max_length'
:
'30'
})
},
'contenttypes.contenttype'
:
{
'Meta'
:
{
'ordering'
:
"('name',)"
,
'unique_together'
:
"(('app_label', 'model'),)"
,
'object_name'
:
'ContentType'
,
'db_table'
:
"'django_content_type'"
},
'app_label'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'100'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'model'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'100'
}),
'name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'100'
})
},
'instructor_task.instructortask'
:
{
'Meta'
:
{
'object_name'
:
'InstructorTask'
},
'course_id'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
,
'db_index'
:
'True'
}),
'created'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'auto_now_add'
:
'True'
,
'null'
:
'True'
,
'blank'
:
'True'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'requester'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'to'
:
"orm['auth.User']"
}),
'subtasks'
:
(
'django.db.models.fields.TextField'
,
[],
{
'blank'
:
'True'
}),
'task_id'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
,
'db_index'
:
'True'
}),
'task_input'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
}),
'task_key'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
,
'db_index'
:
'True'
}),
'task_output'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'1024'
,
'null'
:
'True'
}),
'task_state'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'50'
,
'null'
:
'True'
,
'db_index'
:
'True'
}),
'task_type'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'50'
,
'db_index'
:
'True'
}),
'updated'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'auto_now'
:
'True'
,
'blank'
:
'True'
})
}
}
complete_apps
=
[
'instructor_task'
]
\ No newline at end of file
lms/djangoapps/instructor_task/models.py
View file @
d48e90ee
...
...
@@ -56,6 +56,7 @@ class InstructorTask(models.Model):
requester
=
models
.
ForeignKey
(
User
,
db_index
=
True
)
created
=
models
.
DateTimeField
(
auto_now_add
=
True
,
null
=
True
)
updated
=
models
.
DateTimeField
(
auto_now
=
True
)
subtasks
=
models
.
TextField
(
blank
=
True
)
# JSON dictionary
def
__repr__
(
self
):
return
'InstructorTask<
%
r>'
%
({
...
...
lms/djangoapps/instructor_task/tasks.py
View file @
d48e90ee
...
...
@@ -20,10 +20,15 @@ of the query for traversing StudentModule objects.
"""
from
celery
import
task
from
instructor_task.tasks_helper
import
(
update_problem_module_state
,
from
functools
import
partial
from
instructor_task.tasks_helper
import
(
run_main_task
,
perform_module_state_update
,
# perform_delegate_email_batches,
rescore_problem_module_state
,
reset_attempts_module_state
,
delete_problem_module_state
)
delete_problem_module_state
,
)
from
bulk_email.tasks
import
perform_delegate_email_batches
@task
...
...
@@ -46,11 +51,10 @@ def rescore_problem(entry_id, xmodule_instance_args):
to instantiate an xmodule instance.
"""
action_name
=
'rescored'
update_fcn
=
rescore_problem_module_state
update_fcn
=
partial
(
rescore_problem_module_state
,
xmodule_instance_args
)
filter_fcn
=
lambda
(
modules_to_update
):
modules_to_update
.
filter
(
state__contains
=
'"done": true'
)
return
update_problem_module_state
(
entry_id
,
update_fcn
,
action_name
,
filter_fcn
=
filter_fcn
,
xmodule_instance_args
=
xmodule_instance_args
)
visit_fcn
=
partial
(
perform_module_state_update
,
update_fcn
,
filter_fcn
)
return
run_main_task
(
entry_id
,
visit_fcn
,
action_name
)
@task
...
...
@@ -69,10 +73,9 @@ def reset_problem_attempts(entry_id, xmodule_instance_args):
to instantiate an xmodule instance.
"""
action_name
=
'reset'
update_fcn
=
reset_attempts_module_state
return
update_problem_module_state
(
entry_id
,
update_fcn
,
action_name
,
filter_fcn
=
None
,
xmodule_instance_args
=
xmodule_instance_args
)
update_fcn
=
partial
(
reset_attempts_module_state
,
xmodule_instance_args
)
visit_fcn
=
partial
(
perform_module_state_update
,
update_fcn
,
None
)
return
run_main_task
(
entry_id
,
visit_fcn
,
action_name
)
@task
...
...
@@ -91,7 +94,24 @@ def delete_problem_state(entry_id, xmodule_instance_args):
to instantiate an xmodule instance.
"""
action_name
=
'deleted'
update_fcn
=
delete_problem_module_state
return
update_problem_module_state
(
entry_id
,
update_fcn
,
action_name
,
filter_fcn
=
None
,
xmodule_instance_args
=
xmodule_instance_args
)
update_fcn
=
partial
(
delete_problem_module_state
,
xmodule_instance_args
)
visit_fcn
=
partial
(
perform_module_state_update
,
update_fcn
,
None
)
return
run_main_task
(
entry_id
,
visit_fcn
,
action_name
)
@task
def
send_bulk_course_email
(
entry_id
,
xmodule_instance_args
):
"""Sends emails to in a course.
`entry_id` is the id value of the InstructorTask entry that corresponds to this task.
The entry contains the `course_id` that identifies the course, as well as the
`task_input`, which contains task-specific input.
The task_input should be a dict with no entries.
`xmodule_instance_args` provides information needed by _get_module_instance_for_task()
to instantiate an xmodule instance.
"""
action_name
=
'emailed'
visit_fcn
=
perform_delegate_email_batches
return
run_main_task
(
entry_id
,
visit_fcn
,
action_name
,
spawns_subtasks
=
True
)
lms/djangoapps/instructor_task/tasks_helper.py
View file @
d48e90ee
This diff is collapsed.
Click to expand it.
lms/djangoapps/instructor_task/tests/test_tasks.py
View file @
d48e90ee
...
...
@@ -23,7 +23,7 @@ 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
,
update_problem_module_state
from
instructor_task.tasks_helper
import
UpdateProblemModuleStateError
#
, update_problem_module_state
PROBLEM_URL_NAME
=
"test_urlname"
...
...
@@ -313,17 +313,17 @@ class TestInstructorTasks(InstructorTaskModuleTestCase):
def
test_delete_with_short_error_msg
(
self
):
self
.
_test_run_with_short_error_msg
(
delete_problem_state
)
def
test_successful_result_too_long
(
self
):
def
te
DONT
st_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
=
None
))
#
task_function = (lambda entry_id, xmodule_instance_args:
#
update_problem_module_state(entry_id,
#
update_fcn, action_name, filter_fcn=None,
#
xmodule_instance_args=None))
with
self
.
assertRaises
(
ValueError
):
self
.
_run_task_with_mock_celery
(
task_function
,
task_entry
.
id
,
task_entry
.
task_id
)
...
...
lms/djangoapps/instructor_task/tests/test_views.py
View file @
d48e90ee
...
...
@@ -262,4 +262,4 @@ class InstructorTaskReportTest(InstructorTaskTestCase):
instructor_task
.
task_input
=
"{ bad"
succeeded
,
message
=
get_task_completion_info
(
instructor_task
)
self
.
assertFalse
(
succeeded
)
self
.
assertEquals
(
message
,
"
Problem rescored for 2 of 3 students
(out of 5)"
)
self
.
assertEquals
(
message
,
"
Status: rescored 2 of 3
(out of 5)"
)
lms/djangoapps/instructor_task/views.py
View file @
d48e90ee
...
...
@@ -40,7 +40,7 @@ def instructor_task_status(request):
Status is returned as a JSON-serialized dict, wrapped as the content of a HTTPResponse.
The task_id can be specified to this view in one of t
hree
ways:
The task_id can be specified to this view in one of t
wo
ways:
* by making a request containing 'task_id' as a parameter with a single value
Returns a dict containing status information for the specified task_id
...
...
@@ -133,6 +133,8 @@ def get_task_completion_info(instructor_task):
num_total
=
task_output
[
'total'
]
student
=
None
problem_url
=
None
email_id
=
None
try
:
task_input
=
json
.
loads
(
instructor_task
.
task_input
)
except
ValueError
:
...
...
@@ -140,11 +142,14 @@ def get_task_completion_info(instructor_task):
log
.
warning
(
fmt
.
format
(
instructor_task
.
task_id
,
instructor_task
.
task_input
))
else
:
student
=
task_input
.
get
(
'student'
)
problem_url
=
task_input
.
get
(
'problem_url'
)
email_id
=
task_input
.
get
(
'email_id'
)
if
instructor_task
.
task_state
==
PROGRESS
:
# special message for providing progress updates:
msg_format
=
"Progress: {action} {updated} of {attempted} so far"
elif
student
is
not
None
:
elif
student
is
not
None
and
problem_url
is
not
None
:
# this reports on actions on problems for a particular student:
if
num_attempted
==
0
:
msg_format
=
"Unable to find submission to be {action} for student '{student}'"
elif
num_updated
==
0
:
...
...
@@ -152,7 +157,9 @@ def get_task_completion_info(instructor_task):
else
:
succeeded
=
True
msg_format
=
"Problem successfully {action} for student '{student}'"
elif
num_attempted
==
0
:
elif
student
is
None
and
problem_url
is
not
None
:
# this reports on actions on problems for all students:
if
num_attempted
==
0
:
msg_format
=
"Unable to find any students with submissions to be {action}"
elif
num_updated
==
0
:
msg_format
=
"Problem failed to be {action} for any of {attempted} students"
...
...
@@ -161,6 +168,20 @@ def get_task_completion_info(instructor_task):
msg_format
=
"Problem successfully {action} for {attempted} students"
else
:
# num_updated < num_attempted
msg_format
=
"Problem {action} for {updated} of {attempted} students"
elif
email_id
is
not
None
:
# this reports on actions on bulk emails
if
num_attempted
==
0
:
msg_format
=
"Unable to find any recipients to be {action}"
elif
num_updated
==
0
:
msg_format
=
"Message failed to be {action} for any of {attempted} recipients "
elif
num_updated
==
num_attempted
:
succeeded
=
True
msg_format
=
"Message successfully {action} for {attempted} recipients"
else
:
# num_updated < num_attempted
msg_format
=
"Message {action} for {updated} of {attempted} recipients"
else
:
# provide a default:
msg_format
=
"Status: {action} {updated} of {attempted}"
if
student
is
None
and
num_attempted
!=
num_total
:
msg_format
+=
" (out of {total})"
...
...
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