views.py 7.41 KB
Newer Older
1 2 3 4 5 6 7 8

import json
import logging

from django.http import HttpResponse

from celery.states import FAILURE, REVOKED, READY_STATES

Brian Wilson committed
9 10
from instructor_task.api_helper import (get_status_from_instructor_task,
                                        get_updated_instructor_task)
11
from instructor_task.models import PROGRESS
12 13 14 15


log = logging.getLogger(__name__)

Brian Wilson committed
16 17 18
# return status for completed tasks and tasks in progress
STATES_WITH_STATUS = [state for state in READY_STATES] + [PROGRESS]

19

20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
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


37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
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.

Brian Wilson committed
55
    The dict with status information for a task contains the following keys:
Brian Wilson committed
56 57 58
      'message': on complete tasks, status message reporting on final progress,
          or providing exception message if failed.  For tasks in progress,
          indicates the current progress.
59
      'succeeded': on complete tasks or tasks in progress, boolean value indicates if the
Brian Wilson committed
60
          task outcome was successful:  did it achieve what it set out to do.
Brian Wilson committed
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
          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.

76 77 78 79 80
    """

    output = {}
    if 'task_id' in request.REQUEST:
        task_id = request.REQUEST['task_id']
81
        output = _get_instructor_task_status(task_id)
82 83 84
    elif 'task_ids[]' in request.REQUEST:
        tasks = request.REQUEST.getlist('task_ids[]')
        for task_id in tasks:
85
            task_output = _get_instructor_task_status(task_id)
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
            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

Brian Wilson committed
107
    if instructor_task.task_state not in STATES_WITH_STATUS:
Brian Wilson committed
108 109 110
        return (succeeded, "No status information available")

    # we're more surprised if there is no output for a completed task, but just warn:
111 112 113 114
    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")

Brian Wilson committed
115 116 117 118 119 120
    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")
121

Brian Wilson committed
122 123 124 125 126 127 128 129
    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', 'updated', '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")

130 131 132 133
    action_name = task_output['action_name']
    num_attempted = task_output['attempted']
    num_updated = task_output['updated']
    num_total = task_output['total']
Brian Wilson committed
134 135 136 137 138 139 140 141 142

    student = 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')
143

Brian Wilson committed
144 145 146 147
    if instructor_task.task_state == PROGRESS:
        # special message for providing progress updates:
        msg_format = "Progress: {action} {updated} of {attempted} so far"
    elif student is not None:
148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
        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"

Brian Wilson committed
165
    if student is None and num_attempted != num_total:
166 167 168
        msg_format += " (out of {total})"

    # Update status in task result object itself:
Brian Wilson committed
169 170 171
    message = msg_format.format(action=action_name, updated=num_updated,
                                attempted=num_attempted, total=num_total,
                                student=student)
172
    return (succeeded, message)