diff --git a/lms/djangoapps/courseware/task_submit.py b/lms/djangoapps/courseware/task_submit.py
deleted file mode 100644
index 4064b709d2..0000000000
--- a/lms/djangoapps/courseware/task_submit.py
+++ /dev/null
@@ -1,514 +0,0 @@
-import hashlib
-import json
-import logging
-from django.http import HttpResponse
-from django.db import transaction
-
-from celery.result import AsyncResult
-from celery.states import READY_STATES, SUCCESS, FAILURE, REVOKED
-
-from courseware.models import CourseTask
-from courseware.module_render import get_xqueue_callback_url_prefix
-from courseware.tasks import (PROGRESS, rescore_problem,
- reset_problem_attempts, delete_problem_state)
-from xmodule.modulestore.django import modulestore
-
-
-log = logging.getLogger(__name__)
-
-# define a "state" used in CourseTask
-QUEUING = 'QUEUING'
-
-class AlreadyRunningError(Exception):
- pass
-
-
-def get_running_course_tasks(course_id):
- """
- Returns a query of CourseTask objects of running tasks for a given course.
-
- Used to generate a list of tasks to display on the instructor dashboard.
- """
- course_tasks = CourseTask.objects.filter(course_id=course_id)
- # exclude states that are "ready" (i.e. not "running", e.g. failure, success, revoked):
- for state in READY_STATES:
- course_tasks = course_tasks.exclude(task_state=state)
- return course_tasks
-
-
-def get_course_task_history(course_id, problem_url, student=None):
- """
- Returns a query of CourseTask objects of historical tasks for a given course,
- that match a particular problem and optionally a student.
- """
- _, task_key = _encode_problem_and_student_input(problem_url, student)
-
- course_tasks = CourseTask.objects.filter(course_id=course_id, task_key=task_key)
- return course_tasks.order_by('-id')
-
-
-def course_task_status(request):
- """
- View method that returns the status of a course-related task or tasks.
-
- 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 three 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
-
- * by making a request containing 'task_ids' as a parameter,
- with a list of task_id values.
- Returns a dict of dicts, with the task_id as key, and the corresponding
- dict containing status information for the specified task_id
-
- Task_id values that are unrecognized are skipped.
-
- """
- output = {}
- if 'task_id' in request.REQUEST:
- task_id = request.REQUEST['task_id']
- output = _get_course_task_status(task_id)
- elif 'task_ids[]' in request.REQUEST:
- tasks = request.REQUEST.getlist('task_ids[]')
- for task_id in tasks:
- task_output = _get_course_task_status(task_id)
- if task_output is not None:
- output[task_id] = task_output
-
- return HttpResponse(json.dumps(output, indent=4))
-
-
-def _task_is_running(course_id, task_type, task_key):
- """Checks if a particular task is already running"""
- runningTasks = CourseTask.objects.filter(course_id=course_id, task_type=task_type, task_key=task_key)
- # exclude states that are "ready" (i.e. not "running", e.g. failure, success, revoked):
- for state in READY_STATES:
- runningTasks = runningTasks.exclude(task_state=state)
- return len(runningTasks) > 0
-
-
-@transaction.autocommit
-def _reserve_task(course_id, task_type, task_key, task_input, requester):
- """
- Creates a database entry to indicate that a task is in progress.
-
- Throws AlreadyRunningError if the task is already in progress.
-
- Autocommit annotation makes sure the database entry is committed.
- """
-
- if _task_is_running(course_id, task_type, task_key):
- raise AlreadyRunningError("requested task is already running")
-
- # Create log entry now, so that future requests won't: no task_id yet....
- tasklog_args = {'course_id': course_id,
- 'task_type': task_type,
- 'task_key': task_key,
- 'task_input': json.dumps(task_input),
- 'task_state': 'QUEUING',
- 'requester': requester}
-
- course_task = CourseTask.objects.create(**tasklog_args)
- return course_task
-
-
-@transaction.autocommit
-def _update_task(course_task, task_result):
- """
- Updates a database entry with information about the submitted task.
-
- Autocommit annotation makes sure the database entry is committed.
- """
- # we at least update the entry with the task_id, and for ALWAYS_EAGER mode,
- # we update other status as well. (For non-ALWAYS_EAGER modes, the entry
- # should not have changed except for setting PENDING state and the
- # addition of the task_id.)
- _update_course_task(course_task, task_result)
- course_task.save()
-
-
-def _get_xmodule_instance_args(request):
- """
- 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.
- """
- request_info = {'username': request.user.username,
- 'ip': request.META['REMOTE_ADDR'],
- 'agent': request.META.get('HTTP_USER_AGENT', ''),
- 'host': request.META['SERVER_NAME'],
- }
-
- xmodule_instance_args = {'xqueue_callback_url_prefix': get_xqueue_callback_url_prefix(request),
- 'request_info': request_info,
- }
- return xmodule_instance_args
-
-
-def _update_course_task(course_task, task_result):
- """
- Updates and possibly saves a CourseTask entry based on a task Result.
-
- Used when a task initially returns, as well as when updated status is
- requested.
-
- The `course_task` that is passed in is updated in-place, but
- is usually not saved. In general, tasks that have finished (either with
- success or failure) should have their entries updated by the task itself,
- so are not updated here. Tasks that are still running are not updated
- while they run. So the one exception to the no-save rule are tasks that
- are in a "revoked" state. This may mean that the task never had the
- opportunity to update the CourseTask entry.
-
- Calculates json to store in "task_output" field of the `course_task`,
- as well as updating the task_state and task_id (which may not yet be set
- if this is the first call after the task is submitted).
-
- Returns a dict, with the following keys:
- 'message': status message reporting on progress, or providing exception message if failed.
- '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.
- 'task_traceback': optional, returned if task failed and produced a traceback.
- '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.
-
- """
- # Pull values out of the result object as close to each other as possible.
- # If we wait and check the values later, the values for the state and result
- # are more likely to have changed. Pull the state out first, and
- # then code assuming that the result may not exactly match the state.
- task_id = task_result.task_id
- result_state = task_result.state
- returned_result = task_result.result
- result_traceback = task_result.traceback
-
- # Assume we don't always update the CourseTask entry if we don't have to:
- entry_needs_saving = False
- output = {}
-
- if result_state == PROGRESS:
- # construct a status message directly from the task result's result:
- # it needs to go back with the entry passed in.
- course_task.task_output = json.dumps(returned_result)
- output['task_progress'] = returned_result
- log.info("background task (%s), succeeded: %s", task_id, returned_result)
-
- elif result_state == FAILURE:
- # on failure, the result's result contains the exception that caused the failure
- exception = returned_result
- traceback = result_traceback if result_traceback is not None else ''
- task_progress = {'exception': type(exception).__name__, 'message': str(exception.message)}
- output['message'] = exception.message
- log.warning("background task (%s) failed: %s %s", task_id, returned_result, traceback)
- if result_traceback is not None:
- output['task_traceback'] = result_traceback
- # truncate any traceback that goes into the CourseTask model:
- task_progress['traceback'] = result_traceback[:700]
- # save progress into the entry, even if it's not being saved:
- # when celery is run in "ALWAYS_EAGER" mode, progress needs to go back
- # with the entry passed in.
- course_task.task_output = json.dumps(task_progress)
- output['task_progress'] = task_progress
-
- elif result_state == REVOKED:
- # on revocation, the result's result doesn't contain anything
- # but we cannot rely on the worker thread to set this status,
- # so we set it here.
- entry_needs_saving = True
- message = 'Task revoked before running'
- output['message'] = message
- log.warning("background task (%s) revoked.", task_id)
- task_progress = {'message': message}
- course_task.task_output = json.dumps(task_progress)
- output['task_progress'] = task_progress
-
- # Always update the local version of the entry if the state has changed.
- # This is important for getting the task_id into the initial version
- # of the course_task, and also for development environments
- # when this code is executed when celery is run in "ALWAYS_EAGER" mode.
- if result_state != course_task.task_state:
- course_task.task_state = result_state
- course_task.task_id = task_id
-
- if entry_needs_saving:
- course_task.save()
-
- return output
-
-
-def _get_course_task_status(task_id):
- """
- Get the status for a given task_id.
-
- Returns a dict, with the following keys:
- 'task_id'
- 'task_state'
- 'in_progress': boolean indicating if the task is still running.
- 'message': status message reporting on progress, or providing exception message if failed.
- '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.
- 'task_traceback': optional, returned if task failed and produced a traceback.
- '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.
-
- If task doesn't exist, returns None.
-
- If task has been REVOKED, the CourseTask entry will be updated.
- """
- # First check if the task_id is known
- try:
- course_task = CourseTask.objects.get(task_id=task_id)
- except CourseTask.DoesNotExist:
- log.warning("query for CourseTask status failed: task_id=(%s) not found", task_id)
- return None
-
- status = {}
-
- # if the task is not already known to be done, then we need to query
- # the underlying task's result object:
- if course_task.task_state not in READY_STATES:
- result = AsyncResult(task_id)
- status.update(_update_course_task(course_task, result))
- elif course_task.task_output is not None:
- # task is already known to have finished, but report on its status:
- status['task_progress'] = json.loads(course_task.task_output)
-
- # status basic information matching what's stored in CourseTask:
- status['task_id'] = course_task.task_id
- status['task_state'] = course_task.task_state
- status['in_progress'] = course_task.task_state not in READY_STATES
-
- if course_task.task_state in READY_STATES:
- succeeded, message = get_task_completion_info(course_task)
- status['message'] = message
- status['succeeded'] = succeeded
-
- return status
-
-
-def get_task_completion_info(course_task):
- """
- Construct progress message from progress information in CourseTask entry.
-
- Returns (boolean, message string) duple, where the boolean indicates
- whether the task completed without incident. (It is possible for a
- task to attempt many sub-tasks, such as rescoring many students' problem
- responses, and while the task runs to completion, some of the students'
- responses could not be rescored.)
-
- Used for providing messages to course_task_status(), as well as
- external calls for providing course task submission history information.
- """
- succeeded = False
-
- if course_task.task_output is None:
- log.warning("No task_output information found for course_task {0}".format(course_task.task_id))
- return (succeeded, "No status information available")
-
- task_output = json.loads(course_task.task_output)
- if course_task.task_state in [FAILURE, REVOKED]:
- return(succeeded, task_output['message'])
-
- action_name = task_output['action_name']
- num_attempted = task_output['attempted']
- num_updated = task_output['updated']
- num_total = task_output['total']
-
- if course_task.task_input is None:
- log.warning("No task_input information found for course_task {0}".format(course_task.task_id))
- return (succeeded, "No status information available")
- task_input = json.loads(course_task.task_input)
- problem_url = task_input.get('problem_url')
- student = task_input.get('student')
- if student is not None:
- if num_attempted == 0:
- msg_format = "Unable to find submission to be {action} for student '{student}'"
- elif num_updated == 0:
- msg_format = "Problem failed to be {action} for student '{student}'"
- else:
- succeeded = True
- msg_format = "Problem successfully {action} for student '{student}'"
- elif 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"
- elif num_updated == num_attempted:
- succeeded = True
- msg_format = "Problem successfully {action} for {attempted} students"
- else: # num_updated < num_attempted
- msg_format = "Problem {action} for {updated} of {attempted} students"
-
- if student is not None and num_attempted != num_total:
- msg_format += " (out of {total})"
-
- # Update status in task result object itself:
- message = msg_format.format(action=action_name, updated=num_updated, attempted=num_attempted, total=num_total,
- student=student, problem=problem_url)
- return (succeeded, message)
-
-
-def _check_arguments_for_rescoring(course_id, problem_url):
- """
- Do simple checks on the descriptor to confirm that it supports rescoring.
-
- Confirms first that the problem_url is defined (since that's currently typed
- in). An ItemNotFoundException is raised if the corresponding module
- descriptor doesn't exist. NotImplementedError is raised if the
- corresponding module doesn't support rescoring calls.
- """
- descriptor = modulestore().get_instance(course_id, problem_url)
- if not hasattr(descriptor, 'module_class') or not hasattr(descriptor.module_class, 'rescore_problem'):
- msg = "Specified module does not support rescoring."
- raise NotImplementedError(msg)
-
-
-def _encode_problem_and_student_input(problem_url, student=None):
- """
- Encode 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
- """
- if student is not None:
- task_input = {'problem_url': problem_url, 'student': student.username}
- task_key_stub = "{student}_{problem}".format(student=student.id, problem=problem_url)
- else:
- task_input = {'problem_url': problem_url}
- task_key_stub = "{student}_{problem}".format(student="", problem=problem_url)
-
- # create the key value by using MD5 hash:
- task_key = hashlib.md5(task_key_stub).hexdigest()
-
- return task_input, task_key
-
-
-def _submit_task(request, task_type, task_class, course_id, task_input, task_key):
- """
- Helper method to submit a task.
-
- Reserves the requested task, based on the `course_id`, `task_type`, and `task_key`,
- checking to see if the task is already running. The `task_input` is also passed so that
- it can be stored in the resulting CourseTask entry. Arguments are extracted from
- the `request` provided by the originating server request. Then the task is submitted to run
- asynchronously, using the specified `task_class`. Finally the CourseTask entry is
- updated in order to store the task_id.
-
- `AlreadyRunningError` is raised if the task is already running.
- """
- # check to see if task is already running, and reserve it otherwise:
- course_task = _reserve_task(course_id, task_type, task_key, task_input, request.user)
-
- # submit task:
- task_args = [course_task.id, course_id, task_input, _get_xmodule_instance_args(request)]
- task_result = task_class.apply_async(task_args)
-
- # Update info in table with the resulting task_id (and state).
- _update_task(course_task, task_result)
-
- return course_task
-
-
-def submit_rescore_problem_for_student(request, course_id, problem_url, student):
- """
- Request a problem to be rescored as a background task.
-
- The problem will be rescored for the specified student only. Parameters are the `course_id`,
- the `problem_url`, and the `student` as a User object.
- The url must specify the location of the problem, using i4x-type notation.
-
- 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.
- """
- # check arguments: let exceptions return up to the caller.
- _check_arguments_for_rescoring(course_id, problem_url)
-
- task_type = 'rescore_problem'
- task_class = rescore_problem
- task_input, task_key = _encode_problem_and_student_input(problem_url, student)
- return _submit_task(request, task_type, task_class, course_id, task_input, task_key)
-
-
-def submit_rescore_problem_for_all_students(request, course_id, problem_url):
- """
- Request a problem to be rescored as a background task.
-
- The problem will be rescored for all students who have accessed the
- particular problem in a course and have provided and checked an answer.
- Parameters are the `course_id` and the `problem_url`.
- The url must specify the location of the problem, using i4x-type notation.
-
- 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.
- """
- # check arguments: let exceptions return up to the caller.
- _check_arguments_for_rescoring(course_id, problem_url)
-
- # check to see if task is already running, and reserve it otherwise
- task_type = 'rescore_problem'
- task_class = rescore_problem
- 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_reset_problem_attempts_for_all_students(request, course_id, problem_url):
- """
- Request to have attempts reset for a problem as a background task.
-
- The problem's attempts will be reset for all students who have accessed the
- particular problem in a course. Parameters are the `course_id` and
- the `problem_url`. The url must specify the location of the problem,
- using i4x-type notation.
-
- ItemNotFoundException is raised if the problem doesn't exist, or AlreadyRunningError
- if the problem is already being reset.
- """
- # check arguments: make sure that the problem_url is defined
- # (since that's currently typed in). If the corresponding module descriptor doesn't exist,
- # an exception will be raised. Let it pass up to the caller.
- modulestore().get_instance(course_id, problem_url)
-
- task_type = 'reset_problem_attempts'
- task_class = reset_problem_attempts
- 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_delete_problem_state_for_all_students(request, course_id, problem_url):
- """
- Request to have state deleted for a problem as a background task.
-
- The problem's state will be deleted for all students who have accessed the
- particular problem in a course. Parameters are the `course_id` and
- the `problem_url`. The url must specify the location of the problem,
- using i4x-type notation.
-
- ItemNotFoundException is raised if the problem doesn't exist, or AlreadyRunningError
- if the particular problem is already being deleted.
- """
- # check arguments: make sure that the problem_url is defined
- # (since that's currently typed in). If the corresponding module descriptor doesn't exist,
- # an exception will be raised. Let it pass up to the caller.
- modulestore().get_instance(course_id, problem_url)
-
- task_type = 'delete_problem_state'
- 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)
diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py
index b0cad6ec68..d94d31700b 100644
--- a/lms/djangoapps/instructor/views.py
+++ b/lms/djangoapps/instructor/views.py
@@ -25,7 +25,7 @@ from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError
from courseware import grades
-from courseware import task_submit
+from instructor_task import api as task_api
from courseware.access import (has_access, get_access_group_name,
course_beta_test_group_name)
from courseware.courses import get_course_with_access
@@ -70,7 +70,7 @@ def instructor_dashboard(request, course_id):
problems = []
plots = []
datatable = None
-
+
# the instructor dashboard page is modal: grades, psychometrics, admin
# keep that state in request.session (defaults to grades mode)
idash_mode = request.POST.get('idash_mode', '')
@@ -250,7 +250,7 @@ def instructor_dashboard(request, course_id):
problem_urlname = request.POST.get('problem_for_all_students', '')
problem_url = get_module_url(problem_urlname)
try:
- course_task = task_submit.submit_rescore_problem_for_all_students(request, course_id, problem_url)
+ course_task = task_api.submit_rescore_problem_for_all_students(request, course_id, problem_url)
if course_task is None:
msg += 'Failed to create a background task for rescoring "{0}".'.format(problem_url)
else:
@@ -266,7 +266,7 @@ def instructor_dashboard(request, course_id):
problem_urlname = request.POST.get('problem_for_all_students', '')
problem_url = get_module_url(problem_urlname)
try:
- course_task = task_submit.submit_reset_problem_attempts_for_all_students(request, course_id, problem_url)
+ course_task = task_api.submit_reset_problem_attempts_for_all_students(request, course_id, problem_url)
if course_task is None:
msg += 'Failed to create a background task for resetting "{0}".'.format(problem_url)
else:
@@ -357,7 +357,7 @@ def instructor_dashboard(request, course_id):
else:
# "Rescore student's problem submission" case
try:
- course_task = task_submit.submit_rescore_problem_for_student(request, course_id, module_state_key, student)
+ course_task = task_api.submit_rescore_problem_for_student(request, course_id, module_state_key, student)
if course_task is None:
msg += 'Failed to create a background task for rescoring "{0}" for student {1}.'.format(module_state_key, unique_student_identifier)
else:
@@ -722,7 +722,7 @@ def instructor_dashboard(request, course_id):
# generate list of pending background tasks
if settings.MITX_FEATURES.get('ENABLE_COURSE_BACKGROUND_TASKS'):
- course_tasks = task_submit.get_running_course_tasks(course_id)
+ course_tasks = task_api.get_running_course_tasks(course_id)
else:
course_tasks = None
@@ -1299,7 +1299,7 @@ def get_background_task_table(course_id, problem_url, student=None):
Returns a tuple of (msg, datatable), where the msg is a possible error message,
and the datatable is the datatable to be used for display.
"""
- history_entries = task_submit.get_course_task_history(course_id, problem_url, student)
+ history_entries = task_api.get_instructor_task_history(course_id, problem_url, student)
datatable = None
msg = ""
# first check to see if there is any history at all
diff --git a/lms/djangoapps/instructor_task/__init__.py b/lms/djangoapps/instructor_task/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/lms/djangoapps/instructor_task/api.py b/lms/djangoapps/instructor_task/api.py
new file mode 100644
index 0000000000..a79e574937
--- /dev/null
+++ b/lms/djangoapps/instructor_task/api.py
@@ -0,0 +1,133 @@
+"""
+API for submitting background tasks by an instructor for a course.
+
+TODO:
+
+"""
+
+from celery.states import READY_STATES
+
+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)
+
+from instructor_task.api_helper import (check_arguments_for_rescoring,
+ encode_problem_and_student_input,
+ submit_task)
+
+
+def get_running_instructor_tasks(course_id):
+ """
+ Returns a query of InstructorTask objects of running tasks for a given course.
+
+ Used to generate a list of tasks to display on the instructor dashboard.
+ """
+ instructor_tasks = InstructorTask.objects.filter(course_id=course_id)
+ # exclude states that are "ready" (i.e. not "running", e.g. failure, success, revoked):
+ for state in READY_STATES:
+ instructor_tasks = instructor_tasks.exclude(task_state=state)
+ return instructor_tasks
+
+
+def get_instructor_task_history(course_id, problem_url, student=None):
+ """
+ Returns a query of InstructorTask objects of historical tasks for a given course,
+ that match a particular problem and optionally a student.
+ """
+ _, task_key = encode_problem_and_student_input(problem_url, student)
+
+ instructor_tasks = InstructorTask.objects.filter(course_id=course_id, task_key=task_key)
+ return instructor_tasks.order_by('-id')
+
+
+def submit_rescore_problem_for_student(request, course_id, problem_url, student):
+ """
+ Request a problem to be rescored as a background task.
+
+ The problem will be rescored for the specified student only. Parameters are the `course_id`,
+ the `problem_url`, and the `student` as a User object.
+ The url must specify the location of the problem, using i4x-type notation.
+
+ 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.
+ """
+ # check arguments: let exceptions return up to the caller.
+ check_arguments_for_rescoring(course_id, problem_url)
+
+ task_type = 'rescore_problem'
+ task_class = rescore_problem
+ task_input, task_key = encode_problem_and_student_input(problem_url, student)
+ return submit_task(request, task_type, task_class, course_id, task_input, task_key)
+
+
+def submit_rescore_problem_for_all_students(request, course_id, problem_url):
+ """
+ Request a problem to be rescored as a background task.
+
+ The problem will be rescored for all students who have accessed the
+ particular problem in a course and have provided and checked an answer.
+ Parameters are the `course_id` and the `problem_url`.
+ The url must specify the location of the problem, using i4x-type notation.
+
+ 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.
+ """
+ # check arguments: let exceptions return up to the caller.
+ check_arguments_for_rescoring(course_id, problem_url)
+
+ # check to see if task is already running, and reserve it otherwise
+ task_type = 'rescore_problem'
+ task_class = rescore_problem
+ 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_reset_problem_attempts_for_all_students(request, course_id, problem_url):
+ """
+ Request to have attempts reset for a problem as a background task.
+
+ The problem's attempts will be reset for all students who have accessed the
+ particular problem in a course. Parameters are the `course_id` and
+ the `problem_url`. The url must specify the location of the problem,
+ using i4x-type notation.
+
+ ItemNotFoundException is raised if the problem doesn't exist, or AlreadyRunningError
+ if the problem is already being reset.
+ """
+ # check arguments: make sure that the problem_url is defined
+ # (since that's currently typed in). If the corresponding module descriptor doesn't exist,
+ # an exception will be raised. Let it pass up to the caller.
+ modulestore().get_instance(course_id, problem_url)
+
+ task_type = 'reset_problem_attempts'
+ task_class = reset_problem_attempts
+ 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_delete_problem_state_for_all_students(request, course_id, problem_url):
+ """
+ Request to have state deleted for a problem as a background task.
+
+ The problem's state will be deleted for all students who have accessed the
+ particular problem in a course. Parameters are the `course_id` and
+ the `problem_url`. The url must specify the location of the problem,
+ using i4x-type notation.
+
+ ItemNotFoundException is raised if the problem doesn't exist, or AlreadyRunningError
+ if the particular problem is already being deleted.
+ """
+ # check arguments: make sure that the problem_url is defined
+ # (since that's currently typed in). If the corresponding module descriptor doesn't exist,
+ # an exception will be raised. Let it pass up to the caller.
+ modulestore().get_instance(course_id, problem_url)
+
+ task_type = 'delete_problem_state'
+ 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)
diff --git a/lms/djangoapps/instructor_task/api_helper.py b/lms/djangoapps/instructor_task/api_helper.py
new file mode 100644
index 0000000000..13bb9af87c
--- /dev/null
+++ b/lms/djangoapps/instructor_task/api_helper.py
@@ -0,0 +1,330 @@
+import hashlib
+import json
+import logging
+# from django.http import HttpResponse
+from django.db import transaction
+
+from celery.result import AsyncResult
+from celery.states import READY_STATES, SUCCESS, FAILURE, REVOKED
+
+from courseware.module_render import get_xqueue_callback_url_prefix
+
+from xmodule.modulestore.django import modulestore
+from instructor_task.models import InstructorTask
+# from instructor_task.views import get_task_completion_info
+from instructor_task.tasks_helper import PROGRESS
+
+
+log = logging.getLogger(__name__)
+
+# define a "state" used in InstructorTask
+QUEUING = 'QUEUING'
+
+
+class AlreadyRunningError(Exception):
+ pass
+
+
+def _task_is_running(course_id, task_type, task_key):
+ """Checks if a particular task is already running"""
+ runningTasks = InstructorTask.objects.filter(course_id=course_id, task_type=task_type, task_key=task_key)
+ # exclude states that are "ready" (i.e. not "running", e.g. failure, success, revoked):
+ for state in READY_STATES:
+ runningTasks = runningTasks.exclude(task_state=state)
+ return len(runningTasks) > 0
+
+
+@transaction.autocommit
+def _reserve_task(course_id, task_type, task_key, task_input, requester):
+ """
+ Creates a database entry to indicate that a task is in progress.
+
+ Throws AlreadyRunningError if the task is already in progress.
+
+ Autocommit annotation makes sure the database entry is committed.
+ """
+
+ if _task_is_running(course_id, task_type, task_key):
+ raise AlreadyRunningError("requested task is already running")
+
+ # Create log entry now, so that future requests won't: no task_id yet....
+ tasklog_args = {'course_id': course_id,
+ 'task_type': task_type,
+ 'task_key': task_key,
+ 'task_input': json.dumps(task_input),
+ 'task_state': 'QUEUING',
+ 'requester': requester}
+
+ instructor_task = InstructorTask.objects.create(**tasklog_args)
+ return instructor_task
+
+
+@transaction.autocommit
+def _update_task(instructor_task, task_result):
+ """
+ Updates a database entry with information about the submitted task.
+
+ Autocommit annotation makes sure the database entry is committed.
+ """
+ # we at least update the entry with the task_id, and for ALWAYS_EAGER mode,
+ # we update other status as well. (For non-ALWAYS_EAGER modes, the entry
+ # should not have changed except for setting PENDING state and the
+ # addition of the task_id.)
+ _update_instructor_task(instructor_task, task_result)
+ instructor_task.save()
+
+
+def _get_xmodule_instance_args(request):
+ """
+ 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.
+ """
+ request_info = {'username': request.user.username,
+ 'ip': request.META['REMOTE_ADDR'],
+ 'agent': request.META.get('HTTP_USER_AGENT', ''),
+ 'host': request.META['SERVER_NAME'],
+ }
+
+ xmodule_instance_args = {'xqueue_callback_url_prefix': get_xqueue_callback_url_prefix(request),
+ 'request_info': request_info,
+ }
+ return xmodule_instance_args
+
+
+def _update_instructor_task(instructor_task, task_result):
+ """
+ Updates and possibly saves a InstructorTask entry based on a task Result.
+
+ Used when a task initially returns, as well as when updated status is
+ requested.
+
+ The `instructor_task` that is passed in is updated in-place, but
+ is usually not saved. In general, tasks that have finished (either with
+ success or failure) should have their entries updated by the task itself,
+ so are not updated here. Tasks that are still running are not updated
+ while they run. So the one exception to the no-save rule are tasks that
+ are in a "revoked" state. This may mean that the task never had the
+ opportunity to update the InstructorTask entry.
+
+ Calculates json to store in "task_output" field of the `instructor_task`,
+ as well as updating the task_state and task_id (which may not yet be set
+ if this is the first call after the task is submitted).
+
+TODO: Update -- no longer return anything, or maybe the resulting instructor_task.
+
+ Returns a dict, with the following keys:
+ 'message': status message reporting on progress, or providing exception message if failed.
+ '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.
+ 'task_traceback': optional, returned if task failed and produced a traceback.
+ '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.
+
+ """
+ # Pull values out of the result object as close to each other as possible.
+ # If we wait and check the values later, the values for the state and result
+ # are more likely to have changed. Pull the state out first, and
+ # then code assuming that the result may not exactly match the state.
+ task_id = task_result.task_id
+ result_state = task_result.state
+ returned_result = task_result.result
+ result_traceback = task_result.traceback
+
+ # Assume we don't always update the InstructorTask entry if we don't have to:
+ entry_needs_saving = False
+ output = {}
+
+ if result_state in [PROGRESS, SUCCESS]:
+ # construct a status message directly from the task result's result:
+ # it needs to go back with the entry passed in.
+ instructor_task.task_output = json.dumps(returned_result)
+# output['task_progress'] = returned_result
+ log.info("background task (%s), succeeded: %s", task_id, returned_result)
+
+ elif result_state == FAILURE:
+ # on failure, the result's result contains the exception that caused the failure
+ exception = returned_result
+ traceback = result_traceback if result_traceback is not None else ''
+ task_progress = {'exception': type(exception).__name__, 'message': str(exception.message)}
+# output['message'] = exception.message
+ log.warning("background task (%s) failed: %s %s", task_id, returned_result, traceback)
+ if result_traceback is not None:
+# output['task_traceback'] = result_traceback
+ # truncate any traceback that goes into the InstructorTask model:
+ task_progress['traceback'] = result_traceback[:700]
+ # save progress into the entry, even if it's not being saved:
+ # when celery is run in "ALWAYS_EAGER" mode, progress needs to go back
+ # with the entry passed in.
+ instructor_task.task_output = json.dumps(task_progress)
+# output['task_progress'] = task_progress
+
+ elif result_state == REVOKED:
+ # on revocation, the result's result doesn't contain anything
+ # but we cannot rely on the worker thread to set this status,
+ # so we set it here.
+ entry_needs_saving = True
+ message = 'Task revoked before running'
+# output['message'] = message
+ log.warning("background task (%s) revoked.", task_id)
+ task_progress = {'message': message}
+ instructor_task.task_output = json.dumps(task_progress)
+# output['task_progress'] = task_progress
+
+ # Always update the local version of the entry if the state has changed.
+ # This is important for getting the task_id into the initial version
+ # of the instructor_task, and also for development environments
+ # when this code is executed when celery is run in "ALWAYS_EAGER" mode.
+ if result_state != instructor_task.task_state:
+ instructor_task.task_state = result_state
+ instructor_task.task_id = task_id
+
+ if entry_needs_saving:
+ instructor_task.save()
+
+ return output
+
+
+def _get_updated_instructor_task(task_id):
+ # First check if the task_id is known
+ try:
+ instructor_task = InstructorTask.objects.get(task_id=task_id)
+ except InstructorTask.DoesNotExist:
+ log.warning("query for InstructorTask status failed: task_id=(%s) not found", task_id)
+ return None
+
+ # if the task is not already known to be done, then we need to query
+ # the underlying task's result object:
+ if instructor_task.task_state not in READY_STATES:
+ result = AsyncResult(task_id)
+ _update_instructor_task(instructor_task, result)
+
+ return instructor_task
+
+
+# def _get_instructor_task_status(task_id):
+def _get_instructor_task_status(instructor_task):
+ """
+ Get the status for a given task_id.
+
+ Returns a dict, with the following keys:
+ 'task_id'
+ 'task_state'
+ 'in_progress': boolean indicating if the task is still running.
+ 'message': status message reporting on progress, or providing exception message if failed.
+ '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.
+ 'task_traceback': optional, returned if task failed and produced a traceback.
+ '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.
+
+ If task doesn't exist, returns None.
+
+ If task has been REVOKED, the InstructorTask entry will be updated.
+ """
+# # First check if the task_id is known
+# try:
+# instructor_task = InstructorTask.objects.get(task_id=task_id)
+# except InstructorTask.DoesNotExist:
+# log.warning("query for InstructorTask status failed: task_id=(%s) not found", task_id)
+# return None
+
+ status = {}
+
+ # if the task is not already known to be done, then we need to query
+ # the underlying task's result object:
+# if instructor_task.task_state not in READY_STATES:
+# result = AsyncResult(task_id)
+# status.update(_update_instructor_task(instructor_task, result))
+
+# elif instructor_task.task_output is not None:
+ # task is already known to have finished, but report on its status:
+ if instructor_task.task_output is not None:
+ status['task_progress'] = json.loads(instructor_task.task_output)
+
+ # status basic information matching what's stored in InstructorTask:
+ status['task_id'] = instructor_task.task_id
+ status['task_state'] = instructor_task.task_state
+ status['in_progress'] = instructor_task.task_state not in READY_STATES
+
+# if instructor_task.task_state in READY_STATES:
+# succeeded, message = get_task_completion_info(instructor_task)
+# status['message'] = message
+# status['succeeded'] = succeeded
+
+ return status
+
+
+def check_arguments_for_rescoring(course_id, problem_url):
+ """
+ Do simple checks on the descriptor to confirm that it supports rescoring.
+
+ Confirms first that the problem_url is defined (since that's currently typed
+ in). An ItemNotFoundException is raised if the corresponding module
+ descriptor doesn't exist. NotImplementedError is raised if the
+ corresponding module doesn't support rescoring calls.
+ """
+ descriptor = modulestore().get_instance(course_id, problem_url)
+ if not hasattr(descriptor, 'module_class') or not hasattr(descriptor.module_class, 'rescore_problem'):
+ msg = "Specified module does not support rescoring."
+ raise NotImplementedError(msg)
+
+
+def encode_problem_and_student_input(problem_url, student=None):
+ """
+ Encode 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
+ """
+ if student is not None:
+ task_input = {'problem_url': problem_url, 'student': student.username}
+ task_key_stub = "{student}_{problem}".format(student=student.id, problem=problem_url)
+ else:
+ task_input = {'problem_url': problem_url}
+ task_key_stub = "{student}_{problem}".format(student="", problem=problem_url)
+
+ # create the key value by using MD5 hash:
+ task_key = hashlib.md5(task_key_stub).hexdigest()
+
+ return task_input, task_key
+
+
+def submit_task(request, task_type, task_class, course_id, task_input, task_key):
+ """
+ Helper method to submit a task.
+
+ Reserves the requested task, based on the `course_id`, `task_type`, and `task_key`,
+ checking to see if the task is already running. The `task_input` is also passed so that
+ it can be stored in the resulting InstructorTask entry. Arguments are extracted from
+ the `request` provided by the originating server request. Then the task is submitted to run
+ asynchronously, using the specified `task_class`. Finally the InstructorTask entry is
+ updated in order to store the task_id.
+
+ `AlreadyRunningError` is raised if the task is already running.
+ """
+ # check to see if task is already running, and reserve it otherwise:
+ instructor_task = _reserve_task(course_id, task_type, task_key, task_input, request.user)
+
+ # submit task:
+ task_args = [instructor_task.id, course_id, task_input, _get_xmodule_instance_args(request)]
+ task_result = task_class.apply_async(task_args)
+
+ # Update info in table with the resulting task_id (and state).
+ _update_task(instructor_task, task_result)
+
+ return instructor_task
diff --git a/lms/djangoapps/instructor_task/migrations/0001_initial.py b/lms/djangoapps/instructor_task/migrations/0001_initial.py
new file mode 100644
index 0000000000..4e12f292c1
--- /dev/null
+++ b/lms/djangoapps/instructor_task/migrations/0001_initial.py
@@ -0,0 +1,86 @@
+# -*- 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 model 'InstructorTask'
+ db.create_table('instructor_task_instructortask', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('task_type', self.gf('django.db.models.fields.CharField')(max_length=50, db_index=True)),
+ ('course_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
+ ('task_key', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
+ ('task_input', self.gf('django.db.models.fields.CharField')(max_length=255)),
+ ('task_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
+ ('task_state', self.gf('django.db.models.fields.CharField')(max_length=50, null=True, db_index=True)),
+ ('task_output', self.gf('django.db.models.fields.CharField')(max_length=1024, null=True)),
+ ('requester', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
+ ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, null=True, blank=True)),
+ ('updated', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)),
+ ))
+ db.send_create_signal('instructor_task', ['InstructorTask'])
+
+
+ def backwards(self, orm):
+ # Deleting model 'InstructorTask'
+ db.delete_table('instructor_task_instructortask')
+
+
+ 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']"}),
+ '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
diff --git a/lms/djangoapps/instructor_task/migrations/__init__.py b/lms/djangoapps/instructor_task/migrations/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/lms/djangoapps/instructor_task/models.py b/lms/djangoapps/instructor_task/models.py
new file mode 100644
index 0000000000..4f70615450
--- /dev/null
+++ b/lms/djangoapps/instructor_task/models.py
@@ -0,0 +1,63 @@
+"""
+WE'RE USING MIGRATIONS!
+
+If you make changes to this model, be sure to create an appropriate migration
+file and check it in at the same time as your model changes. To do that,
+
+1. Go to the edx-platform dir
+2. ./manage.py schemamigration courseware --auto description_of_your_change
+ 3. Add the migration file created in edx-platform/lms/djangoapps/instructor_task/migrations/
+
+
+ASSUMPTIONS: modules have unique IDs, even across different module_types
+
+"""
+from django.contrib.auth.models import User
+from django.db import models
+
+
+class InstructorTask(models.Model):
+ """
+ Stores information about background tasks that have been submitted to
+ perform work by an instructor (or course staff).
+ 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 'InstructorTask<%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))
diff --git a/lms/djangoapps/instructor_task/tasks.py b/lms/djangoapps/instructor_task/tasks.py
new file mode 100644
index 0000000000..ba5acc6f43
--- /dev/null
+++ b/lms/djangoapps/instructor_task/tasks.py
@@ -0,0 +1,82 @@
+"""
+This file contains tasks that are designed to perform background operations on the
+running state of a course.
+
+
+
+"""
+from celery import task
+from instructor_task.tasks_helper import (_update_problem_module_state,
+ _rescore_problem_module_state,
+ _reset_problem_attempts_module_state,
+ _delete_problem_module_state)
+
+
+@task
+def rescore_problem(entry_id, course_id, task_input, xmodule_instance_args):
+ """Rescores problem in `course_id`.
+
+ `entry_id` is the id value of the InstructorTask entry that corresponds to this task.
+ `course_id` identifies the course.
+ `task_input` should be a dict with the following entries:
+
+ 'problem_url': the full URL to the problem to be rescored. (required)
+ 'student': the identifier (username or email) of a particular user whose
+ problem submission should be rescored. If not specified, all problem
+ submissions will be rescored.
+
+ `xmodule_instance_args` provides information needed by _get_module_instance_for_task()
+ to instantiate an xmodule instance.
+ """
+ action_name = 'rescored'
+ update_fcn = _rescore_problem_module_state
+ filter_fcn = lambda(modules_to_update): modules_to_update.filter(state__contains='"done": true')
+ problem_url = task_input.get('problem_url')
+ student_ident = None
+ if 'student' in task_input:
+ student_ident = task_input['student']
+ return _update_problem_module_state(entry_id, course_id, problem_url, student_ident,
+ update_fcn, action_name, filter_fcn=filter_fcn,
+ xmodule_instance_args=xmodule_instance_args)
+
+
+@task
+def reset_problem_attempts(entry_id, course_id, task_input, xmodule_instance_args):
+ """Resets problem attempts to zero for `problem_url` in `course_id` for all students.
+
+ `entry_id` is the id value of the InstructorTask entry that corresponds to this task.
+ `course_id` identifies the course.
+ `task_input` should be a dict with the following entries:
+
+ 'problem_url': the full URL to the problem to be rescored. (required)
+
+ `xmodule_instance_args` provides information needed by _get_module_instance_for_task()
+ to instantiate an xmodule instance.
+ """
+ action_name = 'reset'
+ update_fcn = _reset_problem_attempts_module_state
+ problem_url = task_input.get('problem_url')
+ return _update_problem_module_state(entry_id, course_id, problem_url, None,
+ update_fcn, action_name, filter_fcn=None,
+ xmodule_instance_args=xmodule_instance_args)
+
+
+@task
+def delete_problem_state(entry_id, course_id, task_input, xmodule_instance_args):
+ """Deletes problem state entirely for `problem_url` in `course_id` for all students.
+
+ `entry_id` is the id value of the InstructorTask entry that corresponds to this task.
+ `course_id` identifies the course.
+ `task_input` should be a dict with the following entries:
+
+ 'problem_url': the full URL to the problem to be rescored. (required)
+
+ `xmodule_instance_args` provides information needed by _get_module_instance_for_task()
+ to instantiate an xmodule instance.
+ """
+ action_name = 'deleted'
+ update_fcn = _delete_problem_module_state
+ problem_url = task_input.get('problem_url')
+ return _update_problem_module_state(entry_id, course_id, problem_url, None,
+ update_fcn, action_name, filter_fcn=None,
+ xmodule_instance_args=xmodule_instance_args)
diff --git a/lms/djangoapps/courseware/tasks.py b/lms/djangoapps/instructor_task/tasks_helper.py
similarity index 80%
rename from lms/djangoapps/courseware/tasks.py
rename to lms/djangoapps/instructor_task/tasks_helper.py
index 6d86f35d81..a5a2d758ac 100644
--- a/lms/djangoapps/courseware/tasks.py
+++ b/lms/djangoapps/instructor_task/tasks_helper.py
@@ -11,7 +11,7 @@ from time import time
from sys import exc_info
from traceback import format_exc
-from celery import task, current_task
+from celery import current_task
from celery.utils.log import get_task_logger
from celery.states import SUCCESS, FAILURE
@@ -24,10 +24,10 @@ from xmodule.modulestore.django import modulestore
import mitxmako.middleware as middleware
from track.views import task_track
-from courseware.models import StudentModule, CourseTask
+from courseware.models import StudentModule
from courseware.model_data import ModelDataCache
from courseware.module_render import get_module_for_descriptor_internal
-
+from instructor_task.models import InstructorTask
# define different loggers for use within tasks and on client side
TASK_LOG = get_task_logger(__name__)
@@ -78,7 +78,7 @@ def _perform_module_state_update(course_id, module_state_key, student_identifier
'duration_ms': how long the task has (or had) been running.
Because this is run internal to a task, it does not catch exceptions. These are allowed to pass up to the
- next level, so that it can set the failure modes and capture the error trace in the CourseTask and the
+ next level, so that it can set the failure modes and capture the error trace in the InstructorTask and the
result object.
"""
@@ -157,7 +157,7 @@ def _perform_module_state_update(course_id, module_state_key, student_identifier
@transaction.autocommit
def _save_course_task(course_task):
- """Writes CourseTask course_task immediately, ensuring the transaction is committed."""
+ """Writes InstructorTask course_task immediately, ensuring the transaction is committed."""
course_task.save()
@@ -166,7 +166,7 @@ def _update_problem_module_state(entry_id, course_id, module_state_key, student_
"""
Performs generic update by visiting StudentModule instances with the update_fcn provided.
- The `entry_id` is the primary key for the CourseTask entry representing the task. This function
+ The `entry_id` is the primary key for the InstructorTask entry representing the task. This function
updates the entry on success and failure of the _perform_module_state_update function it
wraps. It is setting the entry's value for task_state based on what Celery would set it to once
the task returns to Celery: FAILURE if an exception is encountered, and SUCCESS if it returns normally.
@@ -181,9 +181,9 @@ def _update_problem_module_state(entry_id, course_id, module_state_key, student_
Pass-through of input `action_name`.
'duration_ms': how long the task has (or had) been running.
- Before returning, this is also JSON-serialized and stored in the task_output column of the CourseTask entry.
+ Before returning, this is also JSON-serialized and stored in the task_output column of the InstructorTask entry.
- If exceptions were raised internally, they are caught and recorded in the CourseTask entry.
+ If exceptions were raised internally, they are caught and recorded in the InstructorTask entry.
This is also a JSON-serialized dict, stored in the task_output column, containing the following keys:
'exception': type of exception object
@@ -199,9 +199,9 @@ def _update_problem_module_state(entry_id, course_id, module_state_key, student_
fmt = 'Starting to update problem modules as task "{task_id}": course "{course_id}" problem "{state_key}": nothing {action} yet'
TASK_LOG.info(fmt.format(task_id=task_id, course_id=course_id, state_key=module_state_key, action=action_name))
- # get the CourseTask to be updated. If this fails, then let the exception return to Celery.
+ # get the InstructorTask to be updated. If this fails, then let the exception return to Celery.
# There's no point in catching it here.
- entry = CourseTask.objects.get(pk=entry_id)
+ entry = InstructorTask.objects.get(pk=entry_id)
entry.task_id = task_id
_save_course_task(entry)
@@ -228,7 +228,7 @@ def _update_problem_module_state(entry_id, course_id, module_state_key, student_
_save_course_task(entry)
raise
- # if we get here, we assume we've succeeded, so update the CourseTask entry in anticipation:
+ # if we get here, we assume we've succeeded, so update the InstructorTask entry in anticipation:
entry.task_output = json.dumps(task_progress)
entry.task_state = SUCCESS
_save_course_task(entry)
@@ -329,39 +329,6 @@ def _rescore_problem_module_state(module_descriptor, student_module, xmodule_ins
return True
-def _filter_module_state_for_done(modules_to_update):
- """Filter to apply for rescoring, to limit module instances to those marked as done"""
- return modules_to_update.filter(state__contains='"done": true')
-
-
-@task
-def rescore_problem(entry_id, course_id, task_input, xmodule_instance_args):
- """Rescores problem in `course_id`.
-
- `entry_id` is the id value of the CourseTask entry that corresponds to this task.
- `course_id` identifies the course.
- `task_input` should be a dict with the following entries:
-
- 'problem_url': the full URL to the problem to be rescored. (required)
- 'student': the identifier (username or email) of a particular user whose
- problem submission should be rescored. If not specified, all problem
- submissions will be rescored.
-
- `xmodule_instance_args` provides information needed by _get_module_instance_for_task()
- to instantiate an xmodule instance.
- """
- action_name = 'rescored'
- update_fcn = _rescore_problem_module_state
- filter_fcn = lambda(modules_to_update): modules_to_update.filter(state__contains='"done": true')
- problem_url = task_input.get('problem_url')
- student_ident = None
- if 'student' in task_input:
- student_ident = task_input['student']
- return _update_problem_module_state(entry_id, course_id, problem_url, student_ident,
- update_fcn, action_name, filter_fcn=filter_fcn,
- xmodule_instance_args=xmodule_instance_args)
-
-
@transaction.autocommit
def _reset_problem_attempts_module_state(_module_descriptor, student_module, xmodule_instance_args=None):
"""
@@ -388,27 +355,6 @@ def _reset_problem_attempts_module_state(_module_descriptor, student_module, xmo
return True
-@task
-def reset_problem_attempts(entry_id, course_id, task_input, xmodule_instance_args):
- """Resets problem attempts to zero for `problem_url` in `course_id` for all students.
-
- `entry_id` is the id value of the CourseTask entry that corresponds to this task.
- `course_id` identifies the course.
- `task_input` should be a dict with the following entries:
-
- 'problem_url': the full URL to the problem to be rescored. (required)
-
- `xmodule_instance_args` provides information needed by _get_module_instance_for_task()
- to instantiate an xmodule instance.
- """
- action_name = 'reset'
- update_fcn = _reset_problem_attempts_module_state
- problem_url = task_input.get('problem_url')
- return _update_problem_module_state(entry_id, course_id, problem_url, None,
- update_fcn, action_name, filter_fcn=None,
- xmodule_instance_args=xmodule_instance_args)
-
-
@transaction.autocommit
def _delete_problem_module_state(_module_descriptor, student_module, xmodule_instance_args=None):
"""
@@ -423,24 +369,3 @@ def _delete_problem_module_state(_module_descriptor, student_module, xmodule_ins
task_info = {"student": student_module.student.username, "task_id": _get_task_id_from_xmodule_args(xmodule_instance_args)}
task_track(request_info, task_info, 'problem_delete_state', {}, page='x_module_task')
return True
-
-
-@task
-def delete_problem_state(entry_id, course_id, task_input, xmodule_instance_args):
- """Deletes problem state entirely for `problem_url` in `course_id` for all students.
-
- `entry_id` is the id value of the CourseTask entry that corresponds to this task.
- `course_id` identifies the course.
- `task_input` should be a dict with the following entries:
-
- 'problem_url': the full URL to the problem to be rescored. (required)
-
- `xmodule_instance_args` provides information needed by _get_module_instance_for_task()
- to instantiate an xmodule instance.
- """
- action_name = 'deleted'
- update_fcn = _delete_problem_module_state
- problem_url = task_input.get('problem_url')
- return _update_problem_module_state(entry_id, course_id, problem_url, None,
- update_fcn, action_name, filter_fcn=None,
- xmodule_instance_args=xmodule_instance_args)
diff --git a/lms/djangoapps/instructor_task/tests/__init__.py b/lms/djangoapps/instructor_task/tests/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/lms/djangoapps/instructor_task/tests/factories.py b/lms/djangoapps/instructor_task/tests/factories.py
new file mode 100644
index 0000000000..e54c007a81
--- /dev/null
+++ b/lms/djangoapps/instructor_task/tests/factories.py
@@ -0,0 +1,19 @@
+import json
+
+from factory import DjangoModelFactory, SubFactory
+from student.tests.factories import UserFactory as StudentUserFactory
+from instructor_task.models import InstructorTask
+from celery.states import PENDING
+
+
+class InstructorTaskFactory(DjangoModelFactory):
+ FACTORY_FOR = InstructorTask
+
+ task_type = 'rescore_problem'
+ course_id = "MITx/999/Robot_Super_Course"
+ task_input = json.dumps({})
+ task_key = None
+ task_id = None
+ task_state = PENDING
+ task_output = None
+ requester = SubFactory(StudentUserFactory)
diff --git a/lms/djangoapps/courseware/tests/test_task_submit.py b/lms/djangoapps/instructor_task/tests/test_api.py
similarity index 82%
rename from lms/djangoapps/courseware/tests/test_task_submit.py
rename to lms/djangoapps/instructor_task/tests/test_api.py
index d9afabacbf..80a1701cc2 100644
--- a/lms/djangoapps/courseware/tests/test_task_submit.py
+++ b/lms/djangoapps/instructor_task/tests/test_api.py
@@ -13,17 +13,20 @@ from django.test.testcases import TestCase
from xmodule.modulestore.exceptions import ItemNotFoundError
-from courseware.tests.factories import UserFactory, CourseTaskFactory
-from courseware.tasks import PROGRESS
-from courseware.task_submit import (QUEUING,
- get_running_course_tasks,
- course_task_status,
- _encode_problem_and_student_input,
- AlreadyRunningError,
- 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 courseware.tests.factories import UserFactory
+from instructor_task.tests.factories import InstructorTaskFactory
+from instructor_task.tasks_helper import PROGRESS
+from instructor_task.views import instructor_task_status
+from instructor_task.api import (get_running_instructor_tasks,
+ 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.api_helper import (QUEUING,
+ AlreadyRunningError,
+ encode_problem_and_student_input,
+ )
log = logging.getLogger(__name__)
@@ -52,12 +55,12 @@ class TaskSubmitTestCase(TestCase):
problem_url_name=problem_url_name)
def _create_entry(self, task_state=QUEUING, task_output=None, student=None):
- """Creates a CourseTask entry for testing."""
+ """Creates a InstructorTask entry for testing."""
task_id = str(uuid4())
progress_json = json.dumps(task_output)
- task_input, task_key = _encode_problem_and_student_input(self.problem_url, student)
+ task_input, task_key = encode_problem_and_student_input(self.problem_url, student)
- course_task = CourseTaskFactory.create(course_id=TEST_COURSE_ID,
+ course_task = InstructorTaskFactory.create(course_id=TEST_COURSE_ID,
requester=self.instructor,
task_input=json.dumps(task_input),
task_key=task_key,
@@ -67,7 +70,7 @@ class TaskSubmitTestCase(TestCase):
return course_task
def _create_failure_entry(self):
- """Creates a CourseTask entry representing a failed task."""
+ """Creates a InstructorTask entry representing a failed task."""
# view task entry for task failure
progress = {'message': TEST_FAILURE_MESSAGE,
'exception': 'RandomCauseError',
@@ -75,11 +78,11 @@ class TaskSubmitTestCase(TestCase):
return self._create_entry(task_state=FAILURE, task_output=progress)
def _create_success_entry(self, student=None):
- """Creates a CourseTask entry representing a successful task."""
+ """Creates a InstructorTask entry representing a successful task."""
return self._create_progress_entry(student, task_state=SUCCESS)
def _create_progress_entry(self, student=None, task_state=PROGRESS):
- """Creates a CourseTask entry representing a task in progress."""
+ """Creates a InstructorTask entry representing a task in progress."""
progress = {'attempted': 3,
'updated': 2,
'total': 10,
@@ -88,26 +91,26 @@ class TaskSubmitTestCase(TestCase):
}
return self._create_entry(task_state=task_state, task_output=progress, student=student)
- def test_fetch_running_tasks(self):
+ def test_get_running_instructor_tasks(self):
# when fetching running tasks, we get all running tasks, and only running tasks
for _ in range(1, 5):
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_course_tasks(TEST_COURSE_ID)]
+ task_ids = [course_task.task_id for course_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):
request = Mock()
request.REQUEST = {'task_id': task_id}
- return course_task_status(request)
+ return instructor_task_status(request)
def test_course_task_status(self):
course_task = self._create_failure_entry()
task_id = course_task.task_id
request = Mock()
request.REQUEST = {'task_id': task_id}
- response = course_task_status(request)
+ response = instructor_task_status(request)
output = json.loads(response.content)
self.assertEquals(output['task_id'], task_id)
@@ -118,7 +121,7 @@ class TaskSubmitTestCase(TestCase):
task_ids = [(self._create_failure_entry()).task_id for _ in range(1, 5)]
request = Mock()
request.REQUEST = MultiValueDict({'task_ids[]': task_ids})
- response = course_task_status(request)
+ response = instructor_task_status(request)
output = json.loads(response.content)
self.assertEquals(len(output), len(task_ids))
for task_id in task_ids:
@@ -221,7 +224,7 @@ class TaskSubmitTestCase(TestCase):
self.assertEquals(output['task_state'], SUCCESS)
self.assertFalse(output['in_progress'])
- def teBROKENst_success_messages(self):
+ def test_success_messages(self):
_, output = self._get_output_for_task_success(0, 0, 10)
self.assertTrue("Unable to find any students with submissions to be rescored" in output['message'])
self.assertFalse(output['succeeded'])
@@ -266,15 +269,16 @@ class TaskSubmitTestCase(TestCase):
with self.assertRaises(ItemNotFoundError):
submit_delete_problem_state_for_all_students(request, course_id, problem_url)
- 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
- # requester doesn't have to be the same when determining if a task is already running
- request = Mock()
- request.user = self.student
- with self.assertRaises(AlreadyRunningError):
- # just skip making the argument check, so we don't have to fake it deeper down
- with patch('courseware.task_submit._check_arguments_for_rescoring'):
- submit_rescore_problem_for_all_students(request, course_id, problem_url)
+# 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
+# # requester doesn't have to be the same when determining if a task is already running
+# request = Mock()
+# request.user = self.instructor
+# with self.assertRaises(AlreadyRunningError):
+# # just skip making the argument check, so we don't have to fake it deeper down
+# with patch('instructor_task.api_helper.check_arguments_for_rescoring') as mock_check:
+# mock_check.return_value = None
+# submit_rescore_problem_for_all_students(request, course_id, problem_url)
diff --git a/lms/djangoapps/courseware/tests/test_tasks.py b/lms/djangoapps/instructor_task/tests/test_tasks.py
similarity index 97%
rename from lms/djangoapps/courseware/tests/test_tasks.py
rename to lms/djangoapps/instructor_task/tests/test_tasks.py
index 0baea0f429..b3552f0239 100644
--- a/lms/djangoapps/courseware/tests/test_tasks.py
+++ b/lms/djangoapps/instructor_task/tests/test_tasks.py
@@ -22,13 +22,14 @@ from xmodule.modulestore.exceptions import ItemNotFoundError
from student.tests.factories import CourseEnrollmentFactory, UserFactory, AdminFactory
from courseware.model_data import StudentModule
-from courseware.task_submit import (submit_rescore_problem_for_all_students,
- submit_rescore_problem_for_student,
- course_task_status,
- submit_reset_problem_attempts_for_all_students,
- submit_delete_problem_state_for_all_students)
+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 courseware.tests.factories import CourseTaskFactory
+from instructor_task.tests.factories import InstructorTaskFactory
log = logging.getLogger(__name__)
@@ -197,10 +198,10 @@ class TestRescoringBase(LoginEnrollmentTestCase, ModuleStoreTestCase):
student)
def _create_course_task(self, task_state="QUEUED", task_input=None, student=None):
- """Creates a CourseTask entry for testing."""
+ """Creates a InstructorTask entry for testing."""
task_id = str(uuid4())
task_key = "dummy value"
- course_task = CourseTaskFactory.create(requester=self.instructor,
+ course_task = InstructorTaskFactory.create(requester=self.instructor,
task_input=json.dumps(task_input),
task_key=task_key,
task_id=task_id,
@@ -321,7 +322,7 @@ class TestRescoring(TestRescoringBase):
# check status returned:
mock_request = Mock()
mock_request.REQUEST = {'task_id': course_task.task_id}
- response = course_task_status(mock_request)
+ response = instructor_task_status(mock_request)
status = json.loads(response.content)
self.assertEqual(status['message'], expected_message)
@@ -371,7 +372,7 @@ class TestRescoring(TestRescoringBase):
mock_request = Mock()
mock_request.REQUEST = {'task_id': course_task.task_id}
- response = course_task_status(mock_request)
+ response = instructor_task_status(mock_request)
status = json.loads(response.content)
self.assertEqual(status['message'], "Problem's definition does not support rescoring")
@@ -532,7 +533,7 @@ class TestResetAttempts(TestRescoringBase):
# check status returned:
mock_request = Mock()
mock_request.REQUEST = {'task_id': course_task.task_id}
- response = course_task_status(mock_request)
+ response = instructor_task_status(mock_request)
status = json.loads(response.content)
self.assertEqual(status['message'], expected_message)
@@ -610,7 +611,7 @@ class TestDeleteProblem(TestRescoringBase):
# check status returned:
mock_request = Mock()
mock_request.REQUEST = {'task_id': course_task.task_id}
- response = course_task_status(mock_request)
+ response = instructor_task_status(mock_request)
status = json.loads(response.content)
self.assertEqual(status['message'], expected_message)
diff --git a/lms/djangoapps/instructor_task/views.py b/lms/djangoapps/instructor_task/views.py
new file mode 100644
index 0000000000..5af0d46d46
--- /dev/null
+++ b/lms/djangoapps/instructor_task/views.py
@@ -0,0 +1,116 @@
+
+import json
+import logging
+
+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)
+
+
+log = logging.getLogger(__name__)
+
+
+def instructor_task_status(request):
+ """
+ View method that returns the status of a course-related task or tasks.
+
+ 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 three 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
+
+ * by making a request containing 'task_ids' as a parameter,
+ with a list of task_id values.
+ Returns a dict of dicts, with the task_id as key, and the corresponding
+ dict containing status information for the specified task_id
+
+ Task_id values that are unrecognized are skipped.
+
+ """
+ def get_instructor_task_status(task_id):
+ instructor_task = _get_updated_instructor_task(task_id)
+ status = _get_instructor_task_status(instructor_task)
+ if instructor_task.task_state in READY_STATES:
+ succeeded, message = get_task_completion_info(instructor_task)
+ status['message'] = message
+ status['succeeded'] = succeeded
+ return status
+
+ output = {}
+ if 'task_id' in request.REQUEST:
+ task_id = request.REQUEST['task_id']
+ output = get_instructor_task_status(task_id)
+ elif 'task_ids[]' in request.REQUEST:
+ tasks = request.REQUEST.getlist('task_ids[]')
+ for task_id in tasks:
+ task_output = get_instructor_task_status(task_id)
+ if task_output is not None:
+ output[task_id] = task_output
+
+ return HttpResponse(json.dumps(output, indent=4))
+
+
+def get_task_completion_info(instructor_task):
+ """
+ Construct progress message from progress information in InstructorTask entry.
+
+ Returns (boolean, message string) duple, where the boolean indicates
+ whether the task completed without incident. (It is possible for a
+ task to attempt many sub-tasks, such as rescoring many students' problem
+ responses, and while the task runs to completion, some of the students'
+ responses could not be rescored.)
+
+ Used for providing messages to instructor_task_status(), as well as
+ external calls for providing course task submission history information.
+ """
+ succeeded = False
+
+ if instructor_task.task_output is None:
+ log.warning("No task_output information found for instructor_task {0}".format(instructor_task.task_id))
+ return (succeeded, "No status information available")
+
+ task_output = json.loads(instructor_task.task_output)
+ if instructor_task.task_state in [FAILURE, REVOKED]:
+ return(succeeded, task_output['message'])
+
+ action_name = task_output['action_name']
+ num_attempted = task_output['attempted']
+ num_updated = task_output['updated']
+ num_total = task_output['total']
+
+ if instructor_task.task_input is None:
+ log.warning("No task_input information found for instructor_task {0}".format(instructor_task.task_id))
+ return (succeeded, "No status information available")
+ task_input = json.loads(instructor_task.task_input)
+ problem_url = task_input.get('problem_url')
+ student = task_input.get('student')
+ if student is not None:
+ if num_attempted == 0:
+ msg_format = "Unable to find submission to be {action} for student '{student}'"
+ elif num_updated == 0:
+ msg_format = "Problem failed to be {action} for student '{student}'"
+ else:
+ succeeded = True
+ msg_format = "Problem successfully {action} for student '{student}'"
+ elif 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"
+ elif num_updated == num_attempted:
+ succeeded = True
+ msg_format = "Problem successfully {action} for {attempted} students"
+ else: # num_updated < num_attempted
+ msg_format = "Problem {action} for {updated} of {attempted} students"
+
+ if student is not None and num_attempted != num_total:
+ msg_format += " (out of {total})"
+
+ # Update status in task result object itself:
+ message = msg_format.format(action=action_name, updated=num_updated, attempted=num_attempted, total=num_total,
+ student=student, problem=problem_url)
+ return (succeeded, message)
diff --git a/lms/envs/common.py b/lms/envs/common.py
index 3b795b6089..076528e91e 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -124,8 +124,8 @@ MITX_FEATURES = {
# Do autoplay videos for students
'AUTOPLAY_VIDEOS': True,
- # Enable instructor dash to submit course-level background tasks
- 'ENABLE_COURSE_BACKGROUND_TASKS': True,
+ # Enable instructor dash to submit background tasks
+ 'ENABLE_INSTRUCTOR_BACKGROUND_TASKS': True,
}
# Used for A/B testing
@@ -694,6 +694,7 @@ INSTALLED_APPS = (
'util',
'certificates',
'instructor',
+ 'instructor_task',
'open_ended_grading',
'psychometrics',
'licenses',
diff --git a/lms/urls.py b/lms/urls.py
index 1b4d22c1b0..1d34ebf3af 100644
--- a/lms/urls.py
+++ b/lms/urls.py
@@ -58,7 +58,6 @@ urlpatterns = ('', # nopep8
name='auth_password_reset_done'),
url(r'^heartbeat$', include('heartbeat.urls')),
- url(r'^course_task_status/$', 'courseware.task_submit.course_task_status', name='course_task_status'),
)
# University profiles only make sense in the default edX context
@@ -395,6 +394,11 @@ if settings.MITX_FEATURES.get('ENABLE_SERVICE_STATUS'):
url(r'^status/', include('service_status.urls')),
)
+if settings.MITX_FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS'):
+ urlpatterns += (
+ url(r'^instructor_task_status/$', 'instructor_task.views.instructor_task_status', name='instructor_task_status'),
+ )
+
# FoldIt views
urlpatterns += (
# The path is hardcoded into their app...