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
a67674fe
Commit
a67674fe
authored
Jun 12, 2013
by
Brian Wilson
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
set task_id on LMS side.
parent
b224f4e8
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
102 additions
and
87 deletions
+102
-87
lms/djangoapps/courseware/models.py
+0
-47
lms/djangoapps/instructor_task/api.py
+29
-0
lms/djangoapps/instructor_task/api_helper.py
+0
-0
lms/djangoapps/instructor_task/tests/test_api.py
+30
-30
lms/djangoapps/instructor_task/tests/test_integration.py
+13
-6
lms/djangoapps/instructor_task/views.py
+30
-4
No files found.
lms/djangoapps/courseware/models.py
View file @
a67674fe
...
...
@@ -263,50 +263,3 @@ class OfflineComputedGradeLog(models.Model):
def
__unicode__
(
self
):
return
"[OCGLog]
%
s:
%
s"
%
(
self
.
course_id
,
self
.
created
)
class
CourseTask
(
models
.
Model
):
"""
Stores information about background tasks that have been submitted to
perform course-specific work.
Examples include grading and rescoring.
`task_type` identifies the kind of task being performed, e.g. rescoring.
`course_id` uses the course run's unique id to identify the course.
`task_input` stores input arguments as JSON-serialized dict, for reporting purposes.
Examples include url of problem being rescored, id of student if only one student being rescored.
`task_key` stores relevant input arguments encoded into key value for testing to see
if the task is already running (together with task_type and course_id).
`task_id` stores the id used by celery for the background task.
`task_state` stores the last known state of the celery task
`task_output` stores the output of the celery task.
Format is a JSON-serialized dict. Content varies by task_type and task_state.
`requester` stores id of user who submitted the task
`created` stores date that entry was first created
`updated` stores date that entry was last modified
"""
task_type
=
models
.
CharField
(
max_length
=
50
,
db_index
=
True
)
course_id
=
models
.
CharField
(
max_length
=
255
,
db_index
=
True
)
task_key
=
models
.
CharField
(
max_length
=
255
,
db_index
=
True
)
task_input
=
models
.
CharField
(
max_length
=
255
)
task_id
=
models
.
CharField
(
max_length
=
255
,
db_index
=
True
)
# max_length from celery_taskmeta
task_state
=
models
.
CharField
(
max_length
=
50
,
null
=
True
,
db_index
=
True
)
# max_length from celery_taskmeta
task_output
=
models
.
CharField
(
max_length
=
1024
,
null
=
True
)
requester
=
models
.
ForeignKey
(
User
,
db_index
=
True
)
created
=
models
.
DateTimeField
(
auto_now_add
=
True
,
null
=
True
)
updated
=
models
.
DateTimeField
(
auto_now
=
True
)
def
__repr__
(
self
):
return
'CourseTask<
%
r>'
%
({
'task_type'
:
self
.
task_type
,
'course_id'
:
self
.
course_id
,
'task_input'
:
self
.
task_input
,
'task_id'
:
self
.
task_id
,
'task_state'
:
self
.
task_state
,
'task_output'
:
self
.
task_output
,
},)
def
__unicode__
(
self
):
return
unicode
(
repr
(
self
))
lms/djangoapps/instructor_task/api.py
View file @
a67674fe
...
...
@@ -54,6 +54,14 @@ def submit_rescore_problem_for_student(request, course_id, problem_url, student)
ItemNotFoundException is raised if the problem doesn't exist, or AlreadyRunningError
if the problem is already being rescored for this student, or NotImplementedError if
the problem doesn't support rescoring.
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: let exceptions return up to the caller.
check_arguments_for_rescoring
(
course_id
,
problem_url
)
...
...
@@ -76,6 +84,13 @@ def submit_rescore_problem_for_all_students(request, course_id, problem_url):
ItemNotFoundException is raised if the problem doesn't exist, or AlreadyRunningError
if the problem is already being rescored, or NotImplementedError if the problem doesn't
support rescoring.
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: let exceptions return up to the caller.
check_arguments_for_rescoring
(
course_id
,
problem_url
)
...
...
@@ -98,6 +113,13 @@ def submit_reset_problem_attempts_for_all_students(request, course_id, problem_u
ItemNotFoundException is raised if the problem doesn't exist, or AlreadyRunningError
if the problem is already being reset.
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 problem_url is defined
# (since that's currently typed in). If the corresponding module descriptor doesn't exist,
...
...
@@ -121,6 +143,13 @@ def submit_delete_problem_state_for_all_students(request, course_id, problem_url
ItemNotFoundException is raised if the problem doesn't exist, or AlreadyRunningError
if the particular problem is already being deleted.
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 problem_url is defined
# (since that's currently typed in). If the corresponding module descriptor doesn't exist,
...
...
lms/djangoapps/instructor_task/api_helper.py
View file @
a67674fe
This diff is collapsed.
Click to expand it.
lms/djangoapps/instructor_task/tests/test_api.py
View file @
a67674fe
...
...
@@ -60,14 +60,14 @@ class TaskSubmitTestCase(TestCase):
progress_json
=
json
.
dumps
(
task_output
)
task_input
,
task_key
=
encode_problem_and_student_input
(
self
.
problem_url
,
student
)
course
_task
=
InstructorTaskFactory
.
create
(
course_id
=
TEST_COURSE_ID
,
instructor
_task
=
InstructorTaskFactory
.
create
(
course_id
=
TEST_COURSE_ID
,
requester
=
self
.
instructor
,
task_input
=
json
.
dumps
(
task_input
),
task_key
=
task_key
,
task_id
=
task_id
,
task_state
=
task_state
,
task_output
=
progress_json
)
return
course
_task
return
instructor
_task
def
_create_failure_entry
(
self
):
"""Creates a InstructorTask entry representing a failed task."""
...
...
@@ -97,24 +97,24 @@ class TaskSubmitTestCase(TestCase):
self
.
_create_failure_entry
()
self
.
_create_success_entry
()
progress_task_ids
=
[
self
.
_create_progress_entry
()
.
task_id
for
_
in
range
(
1
,
5
)]
task_ids
=
[
course_task
.
task_id
for
course
_task
in
get_running_instructor_tasks
(
TEST_COURSE_ID
)]
task_ids
=
[
instructor_task
.
task_id
for
instructor
_task
in
get_running_instructor_tasks
(
TEST_COURSE_ID
)]
self
.
assertEquals
(
set
(
task_ids
),
set
(
progress_task_ids
))
def
_get_
course
_task_status
(
self
,
task_id
):
def
_get_
instructor
_task_status
(
self
,
task_id
):
request
=
Mock
()
request
.
REQUEST
=
{
'task_id'
:
task_id
}
return
instructor_task_status
(
request
)
def
test_
course
_task_status
(
self
):
course
_task
=
self
.
_create_failure_entry
()
task_id
=
course
_task
.
task_id
def
test_
instructor
_task_status
(
self
):
instructor
_task
=
self
.
_create_failure_entry
()
task_id
=
instructor
_task
.
task_id
request
=
Mock
()
request
.
REQUEST
=
{
'task_id'
:
task_id
}
response
=
instructor_task_status
(
request
)
output
=
json
.
loads
(
response
.
content
)
self
.
assertEquals
(
output
[
'task_id'
],
task_id
)
def
test_
course
_task_status_list
(
self
):
def
test_
instructor
_task_status_list
(
self
):
# Fetch status for existing tasks by arg list, as if called from ajax.
# Note that ajax does something funny with the marshalling of
# list data, so the key value has "[]" appended to it.
...
...
@@ -128,9 +128,9 @@ class TaskSubmitTestCase(TestCase):
self
.
assertEquals
(
output
[
task_id
][
'task_id'
],
task_id
)
def
test_get_status_from_failure
(
self
):
course
_task
=
self
.
_create_failure_entry
()
task_id
=
course
_task
.
task_id
response
=
self
.
_get_
course
_task_status
(
task_id
)
instructor
_task
=
self
.
_create_failure_entry
()
task_id
=
instructor
_task
.
task_id
response
=
self
.
_get_
instructor
_task_status
(
task_id
)
output
=
json
.
loads
(
response
.
content
)
self
.
assertEquals
(
output
[
'task_id'
],
task_id
)
self
.
assertEquals
(
output
[
'task_state'
],
FAILURE
)
...
...
@@ -138,9 +138,9 @@ class TaskSubmitTestCase(TestCase):
self
.
assertEquals
(
output
[
'message'
],
TEST_FAILURE_MESSAGE
)
def
test_get_status_from_success
(
self
):
course
_task
=
self
.
_create_success_entry
()
task_id
=
course
_task
.
task_id
response
=
self
.
_get_
course
_task_status
(
task_id
)
instructor
_task
=
self
.
_create_success_entry
()
task_id
=
instructor
_task
.
task_id
response
=
self
.
_get_
instructor
_task_status
(
task_id
)
output
=
json
.
loads
(
response
.
content
)
self
.
assertEquals
(
output
[
'task_id'
],
task_id
)
self
.
assertEquals
(
output
[
'task_state'
],
SUCCESS
)
...
...
@@ -148,8 +148,8 @@ class TaskSubmitTestCase(TestCase):
def
test_update_progress_to_progress
(
self
):
# view task entry for task in progress
course
_task
=
self
.
_create_progress_entry
()
task_id
=
course
_task
.
task_id
instructor
_task
=
self
.
_create_progress_entry
()
task_id
=
instructor
_task
.
task_id
mock_result
=
Mock
()
mock_result
.
task_id
=
task_id
mock_result
.
state
=
PROGRESS
...
...
@@ -159,7 +159,7 @@ class TaskSubmitTestCase(TestCase):
'action_name'
:
'rescored'
}
with
patch
(
'celery.result.AsyncResult.__new__'
)
as
mock_result_ctor
:
mock_result_ctor
.
return_value
=
mock_result
response
=
self
.
_get_
course
_task_status
(
task_id
)
response
=
self
.
_get_
instructor
_task_status
(
task_id
)
output
=
json
.
loads
(
response
.
content
)
self
.
assertEquals
(
output
[
'task_id'
],
task_id
)
self
.
assertEquals
(
output
[
'task_state'
],
PROGRESS
)
...
...
@@ -168,8 +168,8 @@ class TaskSubmitTestCase(TestCase):
def
test_update_progress_to_failure
(
self
):
# view task entry for task in progress that later fails
course
_task
=
self
.
_create_progress_entry
()
task_id
=
course
_task
.
task_id
instructor
_task
=
self
.
_create_progress_entry
()
task_id
=
instructor
_task
.
task_id
mock_result
=
Mock
()
mock_result
.
task_id
=
task_id
mock_result
.
state
=
FAILURE
...
...
@@ -177,7 +177,7 @@ class TaskSubmitTestCase(TestCase):
mock_result
.
traceback
=
"random traceback"
with
patch
(
'celery.result.AsyncResult.__new__'
)
as
mock_result_ctor
:
mock_result_ctor
.
return_value
=
mock_result
response
=
self
.
_get_
course
_task_status
(
task_id
)
response
=
self
.
_get_
instructor
_task_status
(
task_id
)
output
=
json
.
loads
(
response
.
content
)
self
.
assertEquals
(
output
[
'task_id'
],
task_id
)
self
.
assertEquals
(
output
[
'task_state'
],
FAILURE
)
...
...
@@ -186,14 +186,14 @@ class TaskSubmitTestCase(TestCase):
def
test_update_progress_to_revoked
(
self
):
# view task entry for task in progress that later fails
course
_task
=
self
.
_create_progress_entry
()
task_id
=
course
_task
.
task_id
instructor
_task
=
self
.
_create_progress_entry
()
task_id
=
instructor
_task
.
task_id
mock_result
=
Mock
()
mock_result
.
task_id
=
task_id
mock_result
.
state
=
REVOKED
with
patch
(
'celery.result.AsyncResult.__new__'
)
as
mock_result_ctor
:
mock_result_ctor
.
return_value
=
mock_result
response
=
self
.
_get_
course
_task_status
(
task_id
)
response
=
self
.
_get_
instructor
_task_status
(
task_id
)
output
=
json
.
loads
(
response
.
content
)
self
.
assertEquals
(
output
[
'task_id'
],
task_id
)
self
.
assertEquals
(
output
[
'task_state'
],
REVOKED
)
...
...
@@ -201,10 +201,10 @@ class TaskSubmitTestCase(TestCase):
self
.
assertEquals
(
output
[
'message'
],
"Task revoked before running"
)
def
_get_output_for_task_success
(
self
,
attempted
,
updated
,
total
,
student
=
None
):
"""returns the task_id and the result returned by
course
_task_status()."""
"""returns the task_id and the result returned by
instructor
_task_status()."""
# view task entry for task in progress
course
_task
=
self
.
_create_progress_entry
(
student
)
task_id
=
course
_task
.
task_id
instructor
_task
=
self
.
_create_progress_entry
(
student
)
task_id
=
instructor
_task
.
task_id
mock_result
=
Mock
()
mock_result
.
task_id
=
task_id
mock_result
.
state
=
SUCCESS
...
...
@@ -214,7 +214,7 @@ class TaskSubmitTestCase(TestCase):
'action_name'
:
'rescored'
}
with
patch
(
'celery.result.AsyncResult.__new__'
)
as
mock_result_ctor
:
mock_result_ctor
.
return_value
=
mock_result
response
=
self
.
_get_
course
_task_status
(
task_id
)
response
=
self
.
_get_
instructor
_task_status
(
task_id
)
output
=
json
.
loads
(
response
.
content
)
return
task_id
,
output
...
...
@@ -271,9 +271,9 @@ class TaskSubmitTestCase(TestCase):
# def test_submit_when_running(self):
# # get exception when trying to submit a task that is already running
#
course
_task = self._create_progress_entry()
# problem_url = json.loads(
course
_task.task_input).get('problem_url')
# course_id =
course
_task.course_id
#
instructor
_task = self._create_progress_entry()
# problem_url = json.loads(
instructor
_task.task_input).get('problem_url')
# course_id =
instructor
_task.course_id
# # requester doesn't have to be the same when determining if a task is already running
# request = Mock()
# request.user = self.instructor
...
...
lms/djangoapps/instructor_task/tests/test_integration.py
View file @
a67674fe
...
...
@@ -9,7 +9,6 @@ import logging
import
json
from
mock
import
Mock
,
patch
import
textwrap
from
uuid
import
uuid4
from
celery.states
import
SUCCESS
,
FAILURE
from
django.contrib.auth.models
import
User
...
...
@@ -23,17 +22,18 @@ from xmodule.modulestore.django import modulestore
from
xmodule.modulestore.tests.factories
import
CourseFactory
,
ItemFactory
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
xmodule.modulestore.exceptions
import
ItemNotFoundError
from
student.tests.factories
import
CourseEnrollmentFactory
,
UserFactory
,
AdminFactory
from
student.tests.factories
import
CourseEnrollmentFactory
,
UserFactory
,
AdminFactory
from
courseware.model_data
import
StudentModule
from
courseware.tests.tests
import
LoginEnrollmentTestCase
,
TEST_DATA_MONGO_MODULESTORE
from
instructor_task.api
import
(
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
instructor_task.views
import
instructor_task_status
from
courseware.tests.tests
import
LoginEnrollmentTestCase
,
TEST_DATA_MONGO_MODULESTORE
from
instructor_task.models
import
InstructorTask
from
instructor_task.tests.factories
import
InstructorTaskFactory
from
instructor_task.views
import
instructor_task_status
log
=
logging
.
getLogger
(
__name__
)
...
...
@@ -306,6 +306,7 @@ class TestRescoring(TestRescoringBase):
instructor_task
=
self
.
submit_rescore_all_student_answers
(
'instructor'
,
problem_url_name
)
# check instructor_task returned
instructor_task
=
InstructorTask
.
objects
.
get
(
id
=
instructor_task
.
id
)
self
.
assertEqual
(
instructor_task
.
task_state
,
'FAILURE'
)
self
.
assertEqual
(
instructor_task
.
requester
.
username
,
'instructor'
)
self
.
assertEqual
(
instructor_task
.
task_type
,
'rescore_problem'
)
...
...
@@ -359,6 +360,8 @@ class TestRescoring(TestRescoringBase):
self
.
submit_student_answer
(
'u1'
,
problem_url_name
,
[
"answer1"
,
"answer2"
])
instructor_task
=
self
.
submit_rescore_all_student_answers
(
'instructor'
,
problem_url_name
)
instructor_task
=
InstructorTask
.
objects
.
get
(
id
=
instructor_task
.
id
)
self
.
assertEqual
(
instructor_task
.
task_state
,
FAILURE
)
status
=
json
.
loads
(
instructor_task
.
task_output
)
self
.
assertEqual
(
status
[
'exception'
],
'NotImplementedError'
)
...
...
@@ -510,7 +513,8 @@ class TestResetAttempts(TestRescoringBase):
mock_save
.
side_effect
=
ZeroDivisionError
(
expected_message
)
instructor_task
=
self
.
reset_problem_attempts
(
'instructor'
,
problem_url_name
)
# check instructor_task returned
# check instructor_task
instructor_task
=
InstructorTask
.
objects
.
get
(
id
=
instructor_task
.
id
)
self
.
assertEqual
(
instructor_task
.
task_state
,
FAILURE
)
self
.
assertEqual
(
instructor_task
.
requester
.
username
,
'instructor'
)
self
.
assertEqual
(
instructor_task
.
task_type
,
'reset_problem_attempts'
)
...
...
@@ -529,6 +533,7 @@ class TestResetAttempts(TestRescoringBase):
"""confirm that a non-problem can still be successfully reset"""
problem_url_name
=
self
.
problem_section
.
location
.
url
()
instructor_task
=
self
.
reset_problem_attempts
(
'instructor'
,
problem_url_name
)
instructor_task
=
InstructorTask
.
objects
.
get
(
id
=
instructor_task
.
id
)
self
.
assertEqual
(
instructor_task
.
task_state
,
SUCCESS
)
def
test_reset_nonexistent_problem
(
self
):
...
...
@@ -586,6 +591,7 @@ class TestDeleteProblem(TestRescoringBase):
instructor_task
=
self
.
delete_problem_state
(
'instructor'
,
problem_url_name
)
# check instructor_task returned
instructor_task
=
InstructorTask
.
objects
.
get
(
id
=
instructor_task
.
id
)
self
.
assertEqual
(
instructor_task
.
task_state
,
FAILURE
)
self
.
assertEqual
(
instructor_task
.
requester
.
username
,
'instructor'
)
self
.
assertEqual
(
instructor_task
.
task_type
,
'delete_problem_state'
)
...
...
@@ -604,6 +610,7 @@ class TestDeleteProblem(TestRescoringBase):
"""confirm that a non-problem can still be successfully deleted"""
problem_url_name
=
self
.
problem_section
.
location
.
url
()
instructor_task
=
self
.
delete_problem_state
(
'instructor'
,
problem_url_name
)
instructor_task
=
InstructorTask
.
objects
.
get
(
id
=
instructor_task
.
id
)
self
.
assertEqual
(
instructor_task
.
task_state
,
SUCCESS
)
def
test_delete_nonexistent_module
(
self
):
...
...
lms/djangoapps/instructor_task/views.py
View file @
a67674fe
...
...
@@ -6,8 +6,8 @@ from django.http import HttpResponse
from
celery.states
import
FAILURE
,
REVOKED
,
READY_STATES
from
instructor_task.api_helper
import
(
_get_instructor_task_status
,
_
get_updated_instructor_task
)
from
instructor_task.api_helper
import
(
get_status_from_instructor_task
,
get_updated_instructor_task
)
log
=
logging
.
getLogger
(
__name__
)
...
...
@@ -31,10 +31,36 @@ def instructor_task_status(request):
Task_id values that are unrecognized are skipped.
The dict with status information for a task contains the following keys:
'message': status message reporting on progress, or providing exception message if failed.
'succeeded': on complete tasks, indicates if the task outcome was successful:
did it achieve what it set out to do.
This is in contrast with a successful task_state, which indicates that the
task merely completed.
'task_id': id assigned by LMS and used by celery.
'task_state': state of task as stored in celery's result store.
'in_progress': boolean indicating if task is still running.
'task_progress': dict containing progress information. This includes:
'attempted': number of attempts made
'updated': number of attempts that "succeeded"
'total': number of possible subtasks to attempt
'action_name': user-visible verb to use in status messages. Should be past-tense.
'duration_ms': how long the task has (or had) been running.
'exception': name of exception class raised in failed tasks.
'message': returned for failed and revoked tasks.
'traceback': optional, returned if task failed and produced a traceback.
"""
def
get_instructor_task_status
(
task_id
):
instructor_task
=
_get_updated_instructor_task
(
task_id
)
status
=
_get_instructor_task_status
(
instructor_task
)
"""
Returns status for a specific task.
Written as an internal method here (rather than as a helper)
so that get_task_completion_info() can be called without
causing a circular dependency (since it's also called directly).
"""
instructor_task
=
get_updated_instructor_task
(
task_id
)
status
=
get_status_from_instructor_task
(
instructor_task
)
if
instructor_task
.
task_state
in
READY_STATES
:
succeeded
,
message
=
get_task_completion_info
(
instructor_task
)
status
[
'message'
]
=
message
...
...
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