api.py 19.3 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

from celery.states import READY_STATES

from xmodule.modulestore.django import modulestore

from instructor_task.models import InstructorTask
16 17 18 19 20
from instructor_task.tasks import (
    rescore_problem,
    reset_problem_attempts,
    delete_problem_state,
    send_bulk_course_email,
21
    calculate_problem_responses_csv,
22
    calculate_grades_csv,
23
    calculate_problem_grade_report,
24 25
    calculate_students_features_csv,
    cohort_students,
26 27
    enrollment_report_features_csv,
    calculate_may_enroll_csv,
28 29
    exec_summary_report_csv,
    generate_certificates,
30
    proctored_exam_results_csv
31
)
32

33 34 35 36 37 38 39
from instructor_task.api_helper import (
    check_arguments_for_rescoring,
    encode_problem_and_student_input,
    encode_entrance_exam_and_student_input,
    check_entrance_exam_problems_for_rescoring,
    submit_task,
)
40
from bulk_email.models import CourseEmail
41 42 43 44 45 46 47 48 49 50 51 52


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)
53
    return instructor_tasks.order_by('-id')
54 55


56
def get_instructor_task_history(course_id, usage_key=None, student=None, task_type=None):
57 58
    """
    Returns a query of InstructorTask objects of historical tasks for a given course,
59
    that optionally match a particular problem, a student, and/or a task type.
60
    """
61
    instructor_tasks = InstructorTask.objects.filter(course_id=course_id)
62 63
    if usage_key is not None or student is not None:
        _, task_key = encode_problem_and_student_input(usage_key, student)
64 65 66
        instructor_tasks = instructor_tasks.filter(task_key=task_key)
    if task_type is not None:
        instructor_tasks = instructor_tasks.filter(task_type=task_type)
67 68 69 70

    return instructor_tasks.order_by('-id')


71 72 73 74 75 76 77 78 79 80 81 82 83
def get_entrance_exam_instructor_task_history(course_id, usage_key=None, student=None):  # pylint: disable=invalid-name
    """
    Returns a query of InstructorTask objects of historical tasks for a given course,
    that optionally match an entrance exam and student if present.
    """
    instructor_tasks = InstructorTask.objects.filter(course_id=course_id)
    if usage_key is not None or student is not None:
        _, task_key = encode_entrance_exam_and_student_input(usage_key, student)
        instructor_tasks = instructor_tasks.filter(task_key=task_key)

    return instructor_tasks.order_by('-id')


84 85
# Disabling invalid-name because this fn name is longer than 30 chars.
def submit_rescore_problem_for_student(request, usage_key, student):  # pylint: disable=invalid-name
86 87 88 89 90 91 92 93 94 95
    """
    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
96 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
    """
    # check arguments:  let exceptions return up to the caller.
106
    check_arguments_for_rescoring(usage_key)
107 108 109

    task_type = 'rescore_problem'
    task_class = rescore_problem
110 111
    task_input, task_key = encode_problem_and_student_input(usage_key, student)
    return submit_task(request, task_type, task_class, usage_key.course_key, task_input, task_key)
112 113


114
def submit_rescore_problem_for_all_students(request, usage_key):  # pylint: disable=invalid-name
115 116 117 118 119 120 121 122 123 124 125
    """
    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
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
    """
    # check arguments:  let exceptions return up to the caller.
135
    check_arguments_for_rescoring(usage_key)
136 137 138 139

    # check to see if task is already running, and reserve it otherwise
    task_type = 'rescore_problem'
    task_class = rescore_problem
140 141
    task_input, task_key = encode_problem_and_student_input(usage_key)
    return submit_task(request, task_type, task_class, usage_key.course_key, task_input, task_key)
142 143


144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175
def submit_rescore_entrance_exam_for_student(request, usage_key, student=None):  # pylint: disable=invalid-name
    """
    Request entrance exam problems to be re-scored as a background task.

    The entrance exam problems will be re-scored for given student or if student
    is None problems for all students who have accessed the entrance exam.

    Parameters are `usage_key`, which must be a :class:`Location`
    representing entrance exam section and the `student` as a User object.

    ItemNotFoundError is raised if entrance exam does not exists for given
    usage_key, AlreadyRunningError is raised if the entrance exam
    is already being re-scored, or NotImplementedError if the problem doesn't
    support rescoring.

    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.
    """
    # check problems for rescoring:  let exceptions return up to the caller.
    check_entrance_exam_problems_for_rescoring(usage_key)

    # 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_entrance_exam_and_student_input(usage_key, student)
    return submit_task(request, task_type, task_class, usage_key.course_key, task_input, task_key)


176
def submit_reset_problem_attempts_for_all_students(request, usage_key):  # pylint: disable=invalid-name
177 178 179 180 181
    """
    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
182
    the `usage_key`, which must be a :class:`Location`.
183 184 185

    ItemNotFoundException is raised if the problem doesn't exist, or AlreadyRunningError
    if the problem is already being reset.
Brian Wilson committed
186 187 188 189 190 191 192

    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.
193
    """
194
    # check arguments:  make sure that the usage_key is defined
195 196
    # (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.
197
    modulestore().get_item(usage_key)
198 199 200

    task_type = 'reset_problem_attempts'
    task_class = reset_problem_attempts
201 202
    task_input, task_key = encode_problem_and_student_input(usage_key)
    return submit_task(request, task_type, task_class, usage_key.course_key, task_input, task_key)
203 204


205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235
def submit_reset_problem_attempts_in_entrance_exam(request, usage_key, student):  # pylint: disable=invalid-name
    """
    Request to have attempts reset for a entrance exam as a background task.

    Problem attempts for all problems in entrance exam will be reset
    for specified student. If student is None problem attempts will be
    reset for all students.

    Parameters are `usage_key`, which must be a :class:`Location`
    representing entrance exam section and the `student` as a User object.

    ItemNotFoundError is raised if entrance exam does not exists for given
    usage_key, AlreadyRunningError is raised if the entrance exam
    is already being reset.

    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.
    """
    # check arguments:  make sure entrance exam(section) exists for given usage_key
    modulestore().get_item(usage_key)

    task_type = 'reset_problem_attempts'
    task_class = reset_problem_attempts
    task_input, task_key = encode_entrance_exam_and_student_input(usage_key, student)
    return submit_task(request, task_type, task_class, usage_key.course_key, task_input, task_key)


236
def submit_delete_problem_state_for_all_students(request, usage_key):  # pylint: disable=invalid-name
237 238 239 240 241
    """
    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
242
    the `usage_key`, which must be a :class:`Location`.
243 244

    ItemNotFoundException is raised if the problem doesn't exist, or AlreadyRunningError
245
    if the particular problem's state is already being deleted.
Brian Wilson committed
246 247 248 249 250 251 252

    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.
253
    """
254
    # check arguments:  make sure that the usage_key is defined
255 256
    # (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.
257
    modulestore().get_item(usage_key)
258 259 260

    task_type = 'delete_problem_state'
    task_class = delete_problem_state
261 262
    task_input, task_key = encode_problem_and_student_input(usage_key)
    return submit_task(request, task_type, task_class, usage_key.course_key, task_input, task_key)
263 264


265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294
def submit_delete_entrance_exam_state_for_student(request, usage_key, student):  # pylint: disable=invalid-name
    """
    Requests reset of state for entrance exam as a background task.

    Module state for all problems in entrance exam will be deleted
    for specified student.

    Parameters are `usage_key`, which must be a :class:`Location`
    representing entrance exam section and the `student` as a User object.

    ItemNotFoundError is raised if entrance exam does not exists for given
    usage_key, AlreadyRunningError is raised if the entrance exam
    is already being reset.

    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.
    """
    # check arguments:  make sure entrance exam(section) exists for given usage_key
    modulestore().get_item(usage_key)

    task_type = 'delete_problem_state'
    task_class = delete_problem_state
    task_input, task_key = encode_entrance_exam_and_student_input(usage_key, student)
    return submit_task(request, task_type, task_class, usage_key.course_key, task_input, task_key)


295
def submit_bulk_course_email(request, course_key, email_id):
296 297 298 299
    """
    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
300
    in a course.  Parameters are the `course_key` and the `email_id`, the id of the CourseEmail object.
301

302 303
    AlreadyRunningError is raised if the same recipients are already being emailed with the same
    CourseEmail object.
304 305 306 307 308 309 310 311

    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.
    """
312 313 314
    # 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
315 316 317 318 319 320
    # 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
321 322 323 324
    # 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.)
325 326 327 328
    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()
329
    return submit_task(request, task_type, task_class, course_key, task_input, task_key)
330 331 332 333 334 335 336 337 338 339 340 341 342 343 344


def submit_calculate_problem_responses_csv(request, course_key, problem_location):  # pylint: disable=invalid-name
    """
    Submits a task to generate a CSV file containing all student
    answers to a given problem.

    Raises AlreadyRunningError if said file is already being updated.
    """
    task_type = 'problem_responses_csv'
    task_class = calculate_problem_responses_csv
    task_input = {'problem_location': problem_location}
    task_key = ""

    return submit_task(request, task_type, task_class, course_key, task_input, task_key)
345

346

347
def submit_calculate_grades_csv(request, course_key):
348 349 350 351 352 353 354 355
    """
    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 = ""

356
    return submit_task(request, task_type, task_class, course_key, task_input, task_key)
357 358


359 360
def submit_problem_grade_report(request, course_key):
    """
Daniel Friedman committed
361
    Submits a task to generate a CSV grade report containing problem
362 363 364 365 366 367 368 369 370
    values.
    """
    task_type = 'grade_problems'
    task_class = calculate_problem_grade_report
    task_input = {}
    task_key = ""
    return submit_task(request, task_type, task_class, course_key, task_input, task_key)


371 372 373 374 375 376 377 378 379 380 381 382
def submit_calculate_students_features_csv(request, course_key, features):
    """
    Submits a task to generate a CSV containing student profile info.

    Raises AlreadyRunningError if said CSV is already being updated.
    """
    task_type = 'profile_info_csv'
    task_class = calculate_students_features_csv
    task_input = {'features': features}
    task_key = ""

    return submit_task(request, task_type, task_class, course_key, task_input, task_key)
383 384


385 386 387 388 389 390 391 392 393 394 395 396 397 398
def submit_detailed_enrollment_features_csv(request, course_key):  # pylint: disable=invalid-name
    """
    Submits a task to generate a CSV containing detailed enrollment info.

    Raises AlreadyRunningError if said CSV is already being updated.
    """
    task_type = 'detailed_enrollment_report'
    task_class = enrollment_report_features_csv
    task_input = {}
    task_key = ""

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


399 400 401 402 403 404 405 406 407 408 409 410 411 412 413
def submit_calculate_may_enroll_csv(request, course_key, features):
    """
    Submits a task to generate a CSV file containing information about
    invited students who have not enrolled in a given course yet.

    Raises AlreadyRunningError if said file is already being updated.
    """
    task_type = 'may_enroll_info_csv'
    task_class = calculate_may_enroll_csv
    task_input = {'features': features}
    task_key = ""

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


Afzal Wali committed
414 415 416 417 418 419 420 421 422
def submit_executive_summary_report(request, course_key):  # pylint: disable=invalid-name
    """
    Submits a task to generate a HTML File containing the executive summary report.

    Raises AlreadyRunningError if HTML File is already being updated.
    """
    task_type = 'exec_summary_report'
    task_class = exec_summary_report_csv
    task_input = {}
423 424 425 426 427 428 429 430 431 432 433 434 435 436
    task_key = ""

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


def submit_proctored_exam_results_report(request, course_key, features):  # pylint: disable=invalid-name
    """
    Submits a task to generate a HTML File containing the executive summary report.

    Raises AlreadyRunningError if HTML File is already being updated.
    """
    task_type = 'proctored_exam_results_report'
    task_class = proctored_exam_results_csv
    task_input = {'features': features}
Afzal Wali committed
437 438 439 440 441
    task_key = ""

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


442 443 444 445 446 447 448 449 450 451 452 453
def submit_cohort_students(request, course_key, file_name):
    """
    Request to have students cohorted in bulk.

    Raises AlreadyRunningError if students are currently being cohorted.
    """
    task_type = 'cohort_students'
    task_class = cohort_students
    task_input = {'file_name': file_name}
    task_key = ""

    return submit_task(request, task_type, task_class, course_key, task_input, task_key)
454 455 456 457 458 459 460 461 462 463 464 465 466 467


def generate_certificates_for_all_students(request, course_key):   # pylint: disable=invalid-name
    """
    Submits a task to generate certificates for all students enrolled in the course.

    Raises AlreadyRunningError if certificates are currently being generated.
    """
    task_type = 'generate_certificates_all_student'
    task_class = generate_certificates
    task_input = {}
    task_key = ""

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