views.py 11.5 KB
Newer Older
1 2 3 4

import json
import logging

5
from celery.states import FAILURE, READY_STATES, REVOKED
6
from django.http import HttpResponse
7
from django.utils.translation import ugettext as _
8

9
from lms.djangoapps.instructor_task.api_helper import get_status_from_instructor_task, get_updated_instructor_task
10
from lms.djangoapps.instructor_task.models import PROGRESS
11 12 13

log = logging.getLogger(__name__)

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

17

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


35 36 37 38 39 40
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.

41
    The task_id can be specified to this view in one of two ways:
42 43 44 45 46 47 48 49 50 51 52

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

74 75
    """
    output = {}
76 77 78
    task_id = request.GET.get('task_id') or request.POST.get('task_id')
    tasks = request.GET.get('task_ids[]') or request.POST.get('task_ids[]')
    if task_id:
79
        output = _get_instructor_task_status(task_id)
80
    elif tasks:
81
        for task_id in tasks:
82
            task_output = _get_instructor_task_status(task_id)
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
            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
104
    if instructor_task.task_state not in STATES_WITH_STATUS:
105
        return (succeeded, _("No status information available"))
Brian Wilson committed
106 107

    # we're more surprised if there is no output for a completed task, but just warn:
108
    if instructor_task.task_output is None:
109 110
        log.warning(_("No task_output information found for instructor_task {0}").format(instructor_task.task_id))
        return (succeeded, _("No status information available"))
111

Brian Wilson committed
112 113 114
    try:
        task_output = json.loads(instructor_task.task_output)
    except ValueError:
115
        fmt = _("No parsable task_output information found for instructor_task {0}: {1}")
Brian Wilson committed
116
        log.warning(fmt.format(instructor_task.task_id, instructor_task.task_output))
117
        return (succeeded, _("No parsable status information available"))
118

Brian Wilson committed
119
    if instructor_task.task_state in [FAILURE, REVOKED]:
120
        return (succeeded, task_output.get('message', _('No message provided')))
Brian Wilson committed
121

122
    if any([key not in task_output for key in ['action_name', 'attempted', 'total']]):
123
        fmt = _("Invalid task_output information found for instructor_task {0}: {1}")
Brian Wilson committed
124
        log.warning(fmt.format(instructor_task.task_id, instructor_task.task_output))
125
        return (succeeded, _("No progress status information available"))
Brian Wilson committed
126

127
    action_name = _(task_output['action_name'])    # pylint: disable=translation-of-non-string
128 129
    num_attempted = task_output['attempted']
    num_total = task_output['total']
Brian Wilson committed
130

131 132 133 134
    # 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.
135 136 137
    num_succeeded = task_output.get('updated', 0) + task_output.get('succeeded', 0)
    num_skipped = task_output.get('skipped', 0)

Brian Wilson committed
138
    student = None
139 140
    problem_url = None
    email_id = None
Brian Wilson committed
141 142 143
    try:
        task_input = json.loads(instructor_task.task_input)
    except ValueError:
144
        fmt = _("No parsable task_input information found for instructor_task {0}: {1}")
Brian Wilson committed
145 146 147
        log.warning(fmt.format(instructor_task.task_id, instructor_task.task_input))
    else:
        student = task_input.get('student')
148
        problem_url = task_input.get('problem_url')
149
        entrance_exam_url = task_input.get('entrance_exam_url')
150
        email_id = task_input.get('email_id')
151

Brian Wilson committed
152 153
    if instructor_task.task_state == PROGRESS:
        # special message for providing progress updates:
154
        # Translators: {action} is a past-tense verb that is localized separately. {attempted} and {succeeded} are counts.
155
        msg_format = _("Progress: {action} {succeeded} of {attempted} so far")
156 157
    elif student is not None and problem_url is not None:
        # this reports on actions on problems for a particular student:
158
        if num_attempted == 0:
159 160
            # 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}'")
161
        elif num_succeeded == 0:
162 163
            # 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}'")
164 165
        else:
            succeeded = True
166 167
            # Translators: {action} is a past-tense verb that is localized separately. {student} is a student identifier.
            msg_format = _("Problem successfully {action} for student '{student}'")
168 169 170 171 172 173 174 175 176 177 178
    elif student is not None and entrance_exam_url is not None:
        # this reports on actions on entrance exam 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 entrance exam submission 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 = _("Entrance exam successfully {action} for student '{student}'")
179 180 181
    elif student is None and problem_url is not None:
        # this reports on actions on problems for all students:
        if num_attempted == 0:
182 183
            # Translators: {action} is a past-tense verb that is localized separately.
            msg_format = _("Unable to find any students with submissions to be {action}")
184
        elif num_succeeded == 0:
185 186
            # 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")
187
        elif num_succeeded == num_attempted:
188
            succeeded = True
189 190
            # Translators: {action} is a past-tense verb that is localized separately. {attempted} is a count.
            msg_format = _("Problem successfully {action} for {attempted} students")
191
        else:  # num_succeeded < num_attempted
192 193
            # 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")
194 195 196
    elif email_id is not None:
        # this reports on actions on bulk emails
        if num_attempted == 0:
197 198
            # Translators: {action} is a past-tense verb that is localized separately.
            msg_format = _("Unable to find any recipients to be {action}")
199
        elif num_succeeded == 0:
200 201
            # 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 ")
202
        elif num_succeeded == num_attempted:
203
            succeeded = True
204 205
            # Translators: {action} is a past-tense verb that is localized separately. {attempted} is a count.
            msg_format = _("Message successfully {action} for {attempted} recipients")
206
        else:  # num_succeeded < num_attempted
207 208
            # 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")
209 210
    else:
        # provide a default:
211 212
        # Translators: {action} is a past-tense verb that is localized separately. {succeeded} and {attempted} are counts.
        msg_format = _("Status: {action} {succeeded} of {attempted}")
213 214

    if num_skipped > 0:
215 216
        # Translators: {skipped} is a count.  This message is appended to task progress status messages.
        msg_format += _(" (skipping {skipped})")
217

Brian Wilson committed
218
    if student is None and num_attempted != num_total:
219 220
        # Translators: {total} is a count.  This message is appended to task progress status messages.
        msg_format += _(" (out of {total})")
221 222

    # Update status in task result object itself:
223 224 225 226 227 228
    message = msg_format.format(
        action=action_name,
        succeeded=num_succeeded,
        attempted=num_attempted,
        total=num_total,
        skipped=num_skipped,
229 230
        student=student
    )
231
    return (succeeded, message)