def check_worker_allowed_new_assignment(worker): """ Check if the worker can be assigned to a new task. Args: worker (orchestra.models.Worker): The worker submitting the task. task_status (orchestra.models.Task.Status): The status of the desired new task assignment. Returns: allowed_new_assignment (bool): True if the worker can be assigned to a new task. Raises: orchestra.core.errors.TaskAssignmentError: Worker has pending reviewer feedback or is assigned to the maximum number of tasks. orchestra.core.errors.TaskStatusError: New task assignment is not permitted for the given status. """ if (worker_assigned_to_rejected_task(worker) and settings.ORCHESTRA_ENFORCE_NO_NEW_TASKS_DURING_REVIEW): raise TaskAssignmentError('Worker has pending reviewer feedback that ' 'must be addressed.') elif worker_assigned_to_max_tasks(worker): raise TaskAssignmentError('Worker assigned to max number of tasks.')
def save_task(task_id, task_data, worker): """ Save the latest data to the database for a task assignment, overwriting previously saved data. Args: task_id (int): The ID of the task to save. task_data (str): A JSON blob of task data to commit to the database. worker (orchestra.models.Worker): The worker saving the task. Returns: None Raises: orchestra.core.errors.TaskAssignmentError: The provided worker is not assigned to the given task or the assignment is in a non-processing state. """ task = Task.objects.get(id=task_id) if not task.is_worker_assigned(worker): raise TaskAssignmentError('Worker is not associated with task') # Use select_for_update to prevent concurrency issues with submit_task. # See https://github.com/b12io/orchestra/issues/2. assignment = (TaskAssignment.objects.select_for_update().get( task=task, worker=worker)) if assignment.status != TaskAssignment.Status.PROCESSING: raise TaskAssignmentError('Worker is not allowed to save') assignment.in_progress_task_data = task_data assignment.save()
def _check_worker_allowed_new_assignment(worker, task_status): """ Check if the worker can be assigned to a new task. Args: worker (orchestra.models.Worker): The worker submitting the task. task_status (orchestra.models.Task.Status): The status of the desired new task assignment. Returns: allowed_new_assignment (bool): True if the worker can be assigned to a new task. Raises: orchestra.core.errors.TaskAssignmentError: Worker has pending reviewer feedback or is assigned to the maximum number of tasks. orchestra.core.errors.TaskStatusError: New task assignment is not permitted for the given status. """ valid_statuses = [ Task.Status.AWAITING_PROCESSING, Task.Status.PENDING_REVIEW ] if task_status not in valid_statuses: raise TaskStatusError('Invalid status for new task assignment.') elif worker_assigned_to_rejected_task(worker): raise TaskAssignmentError('Worker has pending reviewer feedback that ' 'must be addressed.') elif worker_assigned_to_max_tasks(worker): raise TaskAssignmentError('Worker assigned to max number of tasks.')
def get_task_overview_for_worker(task_id, worker): """ Get information about `task` and its assignment for `worker`. Args: task_id (int): The ID of the desired task object. worker (orchestra.models.Worker): The specified worker object. Returns: task_assignment_details (dict): Information about `task` and its assignment for `worker`. """ task = Task.objects.get(id=task_id) task_details = get_task_details(task_id) task_details['is_project_admin'] = worker.is_project_admin() if task.is_worker_assigned(worker): task_assignment = TaskAssignment.objects.get(worker=worker, task=task) elif worker.is_project_admin(): task_assignment = assignment_history(task).last() task_details['is_read_only'] = True else: raise TaskAssignmentError('Worker is not associated with task') task_assignment_details = get_task_assignment_details(task_assignment) task_assignment_details.update(task_details) return task_assignment_details
def assign_task(worker_id, task_id): """ Return a given task after assigning or reassigning it to the specified worker. Args: worker_id (int): The ID of the worker to be assigned. task_id (int): The ID of the task to be assigned. Returns: task (orchestra.models.Task): The newly assigned task. Raises: orchestra.core.errors.TaskAssignmentError: The specified worker is already assigned to the given task or the task status is not compatible with new assignment. orchestra.core.errors.WorkerCertificationError: The specified worker is not certified for the given task. """ worker = Worker.objects.get(id=worker_id) task = Task.objects.get(id=task_id) required_role = role_required_for_new_task(task) assignment = current_assignment(task) if not is_worker_certified_for_task(worker, task, required_role): raise WorkerCertificationError('Worker not certified for this task.') if task.is_worker_assigned(worker): raise TaskAssignmentError('Worker already assigned to this task.') assignment_counter = task.assignments.count() in_progress_task_data = {} if assignment: # In-progress task data is the latest # submission by a previous worker in_progress_task_data = assignment.in_progress_task_data previous_status = task.status if previous_status == Task.Status.AWAITING_PROCESSING: task.status = Task.Status.PROCESSING elif previous_status == Task.Status.PENDING_REVIEW: task.status = Task.Status.REVIEWING task.save() assignment = (TaskAssignment.objects.create( worker=worker, task=task, status=TaskAssignment.Status.PROCESSING, assignment_counter=assignment_counter, in_progress_task_data=in_progress_task_data)) Iteration.objects.create(assignment=assignment, start_datetime=assignment.start_datetime) add_worker_to_project_team(worker, task.project) notify_status_change(task, previous_status) return task
def role_counter_required_for_new_task(task): """ Given a task find a level of expert that is required to be assigned for task. """ if task.status not in [Task.Status.AWAITING_PROCESSING, Task.Status.PENDING_REVIEW]: raise TaskAssignmentError('Status incompatible with new assignment') elif (task.status == Task.Status.AWAITING_PROCESSING and task.assignments.filter(assignment_counter=0).exists()): raise TaskAssignmentError('Task is in incorrect state') assignments_count = task.assignments.count() for assignment_counter in range(assignments_count): task_assignment = (task.assignments .filter(assignment_counter=assignment_counter)) if not task_assignment.exists(): return assignment_counter return assignments_count
def role_required_for_new_task(task): roles = { Task.Status.AWAITING_PROCESSING: WorkerCertification.Role.ENTRY_LEVEL, Task.Status.PENDING_REVIEW: WorkerCertification.Role.REVIEWER } required_role = roles.get(task.status) if required_role is None: raise TaskAssignmentError('Status incompatible with new assignment') return required_role
def reassign_assignment(worker_id, assignment_id, staffing_request_inquiry=None): """ Return a given assignment after reassigning it to the specified worker. Args: worker_id (int): The ID of the worker to be assigned. assignment_id (int): The ID of the assignment to be assigned. Returns: assignment (orchestra.models.TaskAssignment): The newly assigned assignment. Raises: orchestra.core.errors.TaskAssignmentError: The specified worker is already assigned to the given task. orchestra.core.errors.WorkerCertificationError: The specified worker is not certified for the given assignment. """ worker = Worker.objects.get(id=worker_id) assignment = TaskAssignment.objects.get(id=assignment_id) if assignment.assignment_counter > 0: role = WorkerCertification.Role.REVIEWER else: role = WorkerCertification.Role.ENTRY_LEVEL if not is_worker_certified_for_task(worker, assignment.task, role): raise WorkerCertificationError( 'Worker not certified for this assignment.') if assignment.task.is_worker_assigned(worker): raise TaskAssignmentError('Worker already assigned to this task.') assignment.worker = worker assignment.save() mark_worker_as_winner(worker, assignment.task, assignment.assignment_counter, staffing_request_inquiry) add_worker_to_project_team(worker, assignment.task.project) return assignment
def get_task_overview_for_worker(task_id, worker): """ Get information about `task` and its assignment for `worker`. Args: task_id (int): The ID of the desired task object. worker (orchestra.models.Worker): The specified worker object. Returns: task_assignment_details (dict): Information about `task` and its assignment for `worker`. """ task = Task.objects.get(id=task_id) if not task.is_worker_assigned(worker): raise TaskAssignmentError('Worker is not associated with task') task_details = get_task_details(task_id) task_assignment = TaskAssignment.objects.get(worker=worker, task=task) task_assignment_details = get_task_assignment_details(task_assignment) task_assignment_details.update(task_details) return task_assignment_details
def current_assignment(task): """ Return the in-progress assignment for `task`. Args: task (orchestra.models.Task): The specified task object. Returns: current_assignment (orchestra.models.TaskAssignment): The in-progress assignment for `task`. """ assignments = assignment_history(task) processing = task.assignments.filter( status=TaskAssignment.Status.PROCESSING) if not processing.exists(): return assignments.last() else: if processing.count() > 1: raise TaskAssignmentError( 'More than one processing assignment for task {}'.format( task.id)) else: return processing.first()
def submit_task(task_id, task_data, iteration_status, worker): """ Returns a dict mapping task prerequisites onto their latest task assignment information. The dict is of the form: {'previous-slug': {task_assignment_data}, ...} Args: task_id (int): The ID of the task to submit. task_data (str): A JSON blob of task data to submit. iteration_status (orchestra.models.Iteration.Status): The action taken upon task submission (i.e., REQUESTED_REVIEW or PROVIDED_REVIEW). worker (orchestra.models.Worker): The worker submitting the task. Returns: task (orchestra.models.Task): The modified task object. Raises: orchestra.core.errors.IllegalTaskSubmission: Submission prerequisites for the task are incomplete or the assignment is in a non-processing state. orchestra.core.errors.TaskAssignmentError: Worker belongs to more than one assignment for the given task. orchestra.core.errors.TaskStatusError: Task has already been completed. """ submit_datetime = timezone.now() task = Task.objects.select_related('step', 'project').get(id=task_id) step = task.step if not _are_desired_steps_completed_on_project(step.submission_depends_on, project=task.project): raise IllegalTaskSubmission('Submission prerequisites are not ' 'complete.') if task.status == Task.Status.COMPLETE: raise TaskStatusError('Task already completed') # Use select_for_update to prevent concurrency issues with save_task. # See https://github.com/b12io/orchestra/issues/2. assignments = (TaskAssignment.objects.select_for_update().filter( worker=worker, task=task)) # Worker can belong to only one assignment for a given task. if not assignments.count() == 1: raise TaskAssignmentError( 'Task assignment with worker is in broken state.') assignment = assignments[0] if assignment.status != TaskAssignment.Status.PROCESSING: raise IllegalTaskSubmission('Worker is not allowed to submit') next_status = get_next_task_status(task, iteration_status) assignment.in_progress_task_data = task_data # Submit latest iteration latest_iteration = get_latest_iteration(assignment) latest_iteration.status = iteration_status latest_iteration.submitted_data = assignment.in_progress_task_data latest_iteration.end_datetime = submit_datetime latest_iteration.save() assignment.status = TaskAssignment.Status.SUBMITTED assignment.save() previous_status = task.status task.status = next_status task.save() if task.status == Task.Status.PENDING_REVIEW: # Check the assignment policy to try to assign a reviewer automatically task = _preassign_workers(task, AssignmentPolicyType.REVIEWER) elif task.status == Task.Status.REVIEWING: update_related_assignment_status(task, assignment.assignment_counter + 1, assignment.in_progress_task_data, submit_datetime) elif task.status == Task.Status.POST_REVIEW_PROCESSING: update_related_assignment_status(task, assignment.assignment_counter - 1, assignment.in_progress_task_data, submit_datetime) elif task.status == Task.Status.COMPLETE: create_subsequent_tasks(task.project) notify_status_change(task, previous_status) return task
def submit_task(task_id, task_data, snapshot_type, worker, work_time_seconds): """ Returns a dict mapping task prerequisites onto their latest task assignment information. The dict is of the form: {'previous-slug': {task_assignment_data}, ...} Args: task_id (int): The ID of the task to submit. task_data (str): A JSON blob of task data to submit. snapshot_type (orchestra.models.TaskAssignment.SnapshotType): The action to take upon task submission (e.g., SUBMIT, ACCEPT, REJECT). worker (orchestra.models.Worker): The worker submitting the task. work_time_seconds (int): The time taken by the worker on the latest iteration of their task assignment. Returns: task (orchestra.models.Task): The modified task object. Raises: orchestra.core.errors.IllegalTaskSubmission: Submission prerequisites for the task are incomplete or the assignment is in a non-processing state. orchestra.core.errors.TaskAssignmentError: Worker belongs to more than one assignment for the given task. orchestra.core.errors.TaskStatusError: Task has already been completed. """ task = Task.objects.select_related('step', 'project').get(id=task_id) step = task.step if not _are_desired_steps_completed_on_project(step.submission_depends_on, project=task.project): raise IllegalTaskSubmission('Submission prerequisites are not ' 'complete.') if task.status == Task.Status.COMPLETE: raise TaskStatusError('Task already completed') # Use select_for_update to prevent concurrency issues with save_task. # See https://github.com/unlimitedlabs/orchestra/issues/2. assignments = (TaskAssignment.objects.select_for_update().filter( worker=worker, task=task)) # Worker can belong to only one assignment for a given task. if not assignments.count() == 1: raise TaskAssignmentError( 'Task assignment with worker is in broken state.') assignment = assignments[0] if assignment.status != TaskAssignment.Status.PROCESSING: raise IllegalTaskSubmission('Worker is not allowed to submit') next_status = get_next_task_status(task, snapshot_type) assignment.in_progress_task_data = task_data assignment.snapshots['snapshots'].append({ 'data': assignment.in_progress_task_data, 'datetime': timezone.now().isoformat(), 'type': snapshot_type, 'work_time_seconds': work_time_seconds }) assignment.status = TaskAssignment.Status.SUBMITTED assignment.save() previous_status = task.status task.status = next_status task.save() if task.status == Task.Status.REVIEWING: update_related_assignment_status(task, assignment.assignment_counter + 1, assignment.in_progress_task_data) elif task.status == Task.Status.POST_REVIEW_PROCESSING: update_related_assignment_status(task, assignment.assignment_counter - 1, assignment.in_progress_task_data) elif task.status == Task.Status.COMPLETE: create_subsequent_tasks(task.project) notify_status_change(task, previous_status) return task