Files
edx-platform/lms/djangoapps/instructor_task/views.py
David Ormsbee e0aa8cf78a Grade report celery task and direct file push to S3 from the new instructor dashboard.
Hook up display of grade files ready for download to new instructor dashboard.

LMS-58
2013-11-22 10:06:10 -05:00

223 lines
11 KiB
Python

import json
import logging
from django.http import HttpResponse
from django.utils.translation import ugettext as _
from celery.states import FAILURE, REVOKED, READY_STATES
from instructor_task.api_helper import (get_status_from_instructor_task,
get_updated_instructor_task)
from instructor_task.models import PROGRESS
log = logging.getLogger(__name__)
# return status for completed tasks and tasks in progress
STATES_WITH_STATUS = [state for state in READY_STATES] + [PROGRESS]
def _get_instructor_task_status(task_id):
"""
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 is not None and instructor_task.task_state in STATES_WITH_STATUS:
succeeded, message = get_task_completion_info(instructor_task)
status['message'] = message
status['succeeded'] = succeeded
return status
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 two 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.
The dict with status information for a task contains the following keys:
'message': on complete tasks, status message reporting on final progress,
or providing exception message if failed. For tasks in progress,
indicates the current progress.
'succeeded': on complete tasks or tasks in progress, boolean value 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
'succeeded': 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.
"""
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_state not in STATES_WITH_STATUS:
return (succeeded, _("No status information available"))
# we're more surprised if there is no output for a completed task, but just warn:
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"))
try:
task_output = json.loads(instructor_task.task_output)
except ValueError:
fmt = _("No parsable task_output information found for instructor_task {0}: {1}")
log.warning(fmt.format(instructor_task.task_id, instructor_task.task_output))
return (succeeded, _("No parsable status information available"))
if instructor_task.task_state in [FAILURE, REVOKED]:
return (succeeded, task_output.get('message', _('No message provided')))
if any([key not in task_output for key in ['action_name', 'attempted', 'total']]):
fmt = _("Invalid task_output information found for instructor_task {0}: {1}")
log.warning(fmt.format(instructor_task.task_id, instructor_task.task_output))
return (succeeded, _("No progress status information available"))
action_name = _(task_output['action_name'])
num_attempted = task_output['attempted']
num_total = task_output['total']
# In earlier versions of this code, the key 'updated' was used instead of
# (the more general) 'succeeded'. In order to support history that may contain
# output with the old key, we check for values with both the old and the current
# key, and simply sum them.
num_succeeded = task_output.get('updated', 0) + task_output.get('succeeded', 0)
num_skipped = task_output.get('skipped', 0)
student = None
problem_url = None
email_id = None
try:
task_input = json.loads(instructor_task.task_input)
except ValueError:
fmt = _("No parsable task_input information found for instructor_task {0}: {1}")
log.warning(fmt.format(instructor_task.task_id, instructor_task.task_input))
else:
student = task_input.get('student')
problem_url = task_input.get('problem_url')
email_id = task_input.get('email_id')
if instructor_task.task_state == PROGRESS:
# special message for providing progress updates:
# Translators: {action} is a past-tense verb that is localized separately. {attempted} and {succeeded} are counts.
msg_format = _("Progress: {action} {succeeded} of {attempted} so far")
elif student is not None and problem_url is not None:
# this reports on actions on problems for a particular student:
if num_attempted == 0:
# Translators: {action} is a past-tense verb that is localized separately. {student} is a student identifier.
msg_format = _("Unable to find submission to be {action} for student '{student}'")
elif num_succeeded == 0:
# Translators: {action} is a past-tense verb that is localized separately. {student} is a student identifier.
msg_format = _("Problem failed to be {action} for student '{student}'")
else:
succeeded = True
# Translators: {action} is a past-tense verb that is localized separately. {student} is a student identifier.
msg_format = _("Problem successfully {action} for student '{student}'")
elif student is None and problem_url is not None:
# this reports on actions on problems for all students:
if num_attempted == 0:
# Translators: {action} is a past-tense verb that is localized separately.
msg_format = _("Unable to find any students with submissions to be {action}")
elif num_succeeded == 0:
# Translators: {action} is a past-tense verb that is localized separately. {attempted} is a count.
msg_format = _("Problem failed to be {action} for any of {attempted} students")
elif num_succeeded == num_attempted:
succeeded = True
# Translators: {action} is a past-tense verb that is localized separately. {attempted} is a count.
msg_format = _("Problem successfully {action} for {attempted} students")
else: # num_succeeded < num_attempted
# Translators: {action} is a past-tense verb that is localized separately. {succeeded} and {attempted} are counts.
msg_format = _("Problem {action} for {succeeded} of {attempted} students")
elif email_id is not None:
# this reports on actions on bulk emails
if num_attempted == 0:
# Translators: {action} is a past-tense verb that is localized separately.
msg_format = _("Unable to find any recipients to be {action}")
elif num_succeeded == 0:
# Translators: {action} is a past-tense verb that is localized separately. {attempted} is a count.
msg_format = _("Message failed to be {action} for any of {attempted} recipients ")
elif num_succeeded == num_attempted:
succeeded = True
# Translators: {action} is a past-tense verb that is localized separately. {attempted} is a count.
msg_format = _("Message successfully {action} for {attempted} recipients")
else: # num_succeeded < num_attempted
# Translators: {action} is a past-tense verb that is localized separately. {succeeded} and {attempted} are counts.
msg_format = _("Message {action} for {succeeded} of {attempted} recipients")
else:
# provide a default:
# Translators: {action} is a past-tense verb that is localized separately. {succeeded} and {attempted} are counts.
msg_format = _("Status: {action} {succeeded} of {attempted}")
if num_skipped > 0:
# Translators: {skipped} is a count. This message is appended to task progress status messages.
msg_format += _(" (skipping {skipped})")
if student is None and num_attempted != num_total:
# Translators: {total} is a count. This message is appended to task progress status messages.
msg_format += _(" (out of {total})")
# Update status in task result object itself:
message = msg_format.format(
action=action_name,
succeeded=num_succeeded,
attempted=num_attempted,
total=num_total,
skipped=num_skipped,
student=student
)
return (succeeded, message)