api.py 10 KB
Newer Older
1 2 3
"""
API for submitting background tasks by an instructor for a course.

Brian Wilson committed
4 5 6
Also includes methods for getting information about tasks that have
already been submitted, filtered either by running state or input
arguments.
7 8

"""
9
import hashlib
10 11 12 13 14 15 16 17

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,
18
                                   delete_problem_state,
19 20
                                   send_bulk_course_email,
                                   calculate_grades_csv)
21 22 23 24

from instructor_task.api_helper import (check_arguments_for_rescoring,
                                        encode_problem_and_student_input,
                                        submit_task)
25
from bulk_email.models import CourseEmail
26 27 28 29 30 31 32 33 34 35 36 37


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)
38
    return instructor_tasks.order_by('-id')
39 40


41
def get_instructor_task_history(course_id, problem_url=None, student=None, task_type=None):
42 43
    """
    Returns a query of InstructorTask objects of historical tasks for a given course,
44
    that optionally match a particular problem, a student, and/or a task type.
45
    """
46 47 48 49 50 51
    instructor_tasks = InstructorTask.objects.filter(course_id=course_id)
    if problem_url is not None or student is not None:
        _, task_key = encode_problem_and_student_input(problem_url, student)
        instructor_tasks = instructor_tasks.filter(task_key=task_key)
    if task_type is not None:
        instructor_tasks = instructor_tasks.filter(task_type=task_type)
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66

    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.
Brian Wilson committed
67 68 69 70 71 72 73 74

    This method makes sure the InstructorTask entry is committed.
    When called from any view that is wrapped by TransactionMiddleware,
    and thus in a "commit-on-success" transaction, an autocommit buried within here
    will cause any pending transaction to be committed by a successful
    save here.  Any future database operations will take place in a
    separate transaction.

75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
    """
    # 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.
Brian Wilson committed
97 98 99 100 101 102 103

    This method makes sure the InstructorTask entry is committed.
    When called from any view that is wrapped by TransactionMiddleware,
    and thus in a "commit-on-success" transaction, an autocommit buried within here
    will cause any pending transaction to be committed by a successful
    save here.  Any future database operations will take place in a
    separate transaction.
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125
    """
    # 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.
Brian Wilson committed
126 127 128 129 130 131 132

    This method makes sure the InstructorTask entry is committed.
    When called from any view that is wrapped by TransactionMiddleware,
    and thus in a "commit-on-success" transaction, an autocommit buried within here
    will cause any pending transaction to be committed by a successful
    save here.  Any future database operations will take place in a
    separate transaction.
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154
    """
    # 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
155
    if the particular problem's state is already being deleted.
Brian Wilson committed
156 157 158 159 160 161 162

    This method makes sure the InstructorTask entry is committed.
    When called from any view that is wrapped by TransactionMiddleware,
    and thus in a "commit-on-success" transaction, an autocommit buried within here
    will cause any pending transaction to be committed by a successful
    save here.  Any future database operations will take place in a
    separate transaction.
163 164 165 166 167 168 169 170 171 172
    """
    # 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)
173 174 175 176 177 178 179 180 181


def submit_bulk_course_email(request, course_id, email_id):
    """
    Request to have bulk email sent as a background task.

    The specified CourseEmail object will be sent be updated for all students who have enrolled
    in a course.  Parameters are the `course_id` and the `email_id`, the id of the CourseEmail object.

182 183
    AlreadyRunningError is raised if the same recipients are already being emailed with the same
    CourseEmail object.
184 185 186 187 188 189 190 191

    This method makes sure the InstructorTask entry is committed.
    When called from any view that is wrapped by TransactionMiddleware,
    and thus in a "commit-on-success" transaction, an autocommit buried within here
    will cause any pending transaction to be committed by a successful
    save here.  Any future database operations will take place in a
    separate transaction.
    """
192 193 194
    # Assume that the course is defined, and that the user has already been verified to have
    # appropriate access to the course. But make sure that the email exists.
    # We also pull out the To argument here, so that is displayed in
195 196 197 198 199 200
    # the InstructorTask status.
    email_obj = CourseEmail.objects.get(id=email_id)
    to_option = email_obj.to_option

    task_type = 'bulk_course_email'
    task_class = send_bulk_course_email
201 202 203 204
    # Pass in the to_option as a separate argument, even though it's (currently)
    # in the CourseEmail.  That way it's visible in the progress status.
    # (At some point in the future, we might take the recipient out of the CourseEmail,
    # so that the same saved email can be sent to different recipients, as it is tested.)
205 206 207 208 209
    task_input = {'email_id': email_id, 'to_option': to_option}
    task_key_stub = "{email_id}_{to_option}".format(email_id=email_id, to_option=to_option)
    # create the key value by using MD5 hash:
    task_key = hashlib.md5(task_key_stub).hexdigest()
    return submit_task(request, task_type, task_class, course_id, task_input, task_key)
210

211

212 213 214 215 216 217 218 219 220 221
def submit_calculate_grades_csv(request, course_id):
    """
    AlreadyRunningError is raised if the course's grades are already being updated.
    """
    task_type = 'grade_course'
    task_class = calculate_grades_csv
    task_input = {}
    task_key = ""

    return submit_task(request, task_type, task_class, course_id, task_input, task_key)