def test_is_worker_assigned_to_task(self): task = self.tasks['review_task'] # worker is not related to any task self.assertFalse(is_worker_assigned_to_task(self.workers[2], task)) # worker is assigned to a task. self.assertTrue(is_worker_assigned_to_task(self.workers[0], task))
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 is_worker_assigned_to_task(worker, task): raise TaskAssignmentError('Worker is not associated with task') # Use select_for_update to prevent concurrency issues with submit_task. # See https://github.com/unlimitedlabs/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 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 is_worker_assigned_to_task(worker, task): raise TaskAssignmentError('Worker is not associated with task') # Use select_for_update to prevent concurrency issues with submit_task. # See https://github.com/unlimitedlabs/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 test_assign_task(self): entry_task = TaskFactory(project=self.projects['base_test_project'], status=Task.Status.AWAITING_PROCESSING, step=self.test_step) # Assign entry-level task to entry-level worker entry_task = assign_task(self.workers[0].id, entry_task.id) self.assertTrue(is_worker_assigned_to_task(self.workers[0], entry_task)) self.assertEquals(entry_task.status, Task.Status.PROCESSING) self.assertEquals(entry_task.assignments.count(), 1) # Attempt to assign task which isn't awaiting a new assignment invalid = (Task.Status.PROCESSING, Task.Status.ABORTED, Task.Status.REVIEWING, Task.Status.COMPLETE, Task.Status.POST_REVIEW_PROCESSING) for status in invalid: invalid_status_task = Task.objects.create( project=self.projects['base_test_project'], status=status, step=self.test_step) with self.assertRaises(TaskAssignmentError): invalid_status_task = assign_task(self.workers[0].id, invalid_status_task.id) # Attempt to assign review task to worker already in review hierarchy review_task = Task.objects.create( project=self.projects['base_test_project'], status=Task.Status.PENDING_REVIEW, step=self.test_step) test_data = {'test_assign': True} TaskAssignmentFactory(worker=self.workers[1], task=review_task, status=TaskAssignment.Status.SUBMITTED, in_progress_task_data=test_data, snapshots=empty_snapshots()) with self.assertRaises(TaskAssignmentError): assign_task(self.workers[1].id, review_task.id) self.assertEqual( current_assignment(review_task).in_progress_task_data, test_data) # Attempt to assign review task to worker not certified for task with self.assertRaises(WorkerCertificationError): assign_task(self.workers[2].id, review_task.id) self.assertEqual( current_assignment(review_task).in_progress_task_data, test_data) # Assign review task to review worker self.assertEquals(review_task.assignments.count(), 1) review_task = assign_task(self.workers[3].id, review_task.id) self.assertEquals(review_task.assignments.count(), 2) self.assertEqual( current_assignment(review_task).worker, self.workers[3]) self.assertEqual( current_assignment(review_task).in_progress_task_data, test_data) self.assertEquals(review_task.status, Task.Status.REVIEWING)
def test_preassign_workers(self): project = self.projects['assignment_policy'] # Create first task in test project create_subsequent_tasks(project) self.assertEquals(project.tasks.count(), 1) # Assign initial task to worker 0 initial_task = assign_task(self.workers[0].id, project.tasks.first().id) # Submit task; next task should be created with patch('orchestra.utils.task_lifecycle._is_review_needed', return_value=False): initial_task = submit_task(initial_task.id, {}, TaskAssignment.SnapshotType.SUBMIT, self.workers[0], 0) self.assertEquals(project.tasks.count(), 2) related_task = project.tasks.exclude(id=initial_task.id).first() # Worker 0 not certified for related tasks, so should not have been # auto-assigned self.assertEquals(related_task.assignments.count(), 0) self.assertEquals(related_task.status, Task.Status.AWAITING_PROCESSING) # Reset project project.tasks.all().delete() # Create first task in test project create_subsequent_tasks(project) self.assertEquals(project.tasks.count(), 1) # Assign initial task to worker 4 initial_task = assign_task(self.workers[4].id, project.tasks.first().id) # Submit task; next task should be created with patch('orchestra.utils.task_lifecycle._is_review_needed', return_value=False): initial_task = submit_task(initial_task.id, {}, TaskAssignment.SnapshotType.SUBMIT, self.workers[4], 0) self.assertEquals(project.tasks.count(), 2) related_task = project.tasks.exclude(id=initial_task.id).first() # Worker 4 is certified for related task and should have been assigned self.assertEquals(related_task.assignments.count(), 1) self.assertEquals(related_task.status, Task.Status.PROCESSING) self.assertTrue(is_worker_assigned_to_task(self.workers[4], related_task)) # Reset project project.tasks.all().delete()
def test_preassign_workers(self): project = self.projects['assignment_policy'] # Create first task in test project create_subsequent_tasks(project) self.assertEquals(project.tasks.count(), 1) # Assign initial task to worker 0 initial_task = assign_task(self.workers[0].id, project.tasks.first().id) # Submit task; next task should be created with patch('orchestra.utils.task_lifecycle._is_review_needed', return_value=False): initial_task = submit_task(initial_task.id, {}, TaskAssignment.SnapshotType.SUBMIT, self.workers[0], 0) self.assertEquals(project.tasks.count(), 2) related_task = project.tasks.exclude(id=initial_task.id).first() # Worker 0 not certified for related tasks, so should not have been # auto-assigned self.assertEquals(related_task.assignments.count(), 0) self.assertEquals(related_task.status, Task.Status.AWAITING_PROCESSING) # Reset project project.tasks.all().delete() # Create first task in test project create_subsequent_tasks(project) self.assertEquals(project.tasks.count(), 1) # Assign initial task to worker 4 initial_task = assign_task(self.workers[4].id, project.tasks.first().id) # Submit task; next task should be created with patch('orchestra.utils.task_lifecycle._is_review_needed', return_value=False): initial_task = submit_task(initial_task.id, {}, TaskAssignment.SnapshotType.SUBMIT, self.workers[4], 0) self.assertEquals(project.tasks.count(), 2) related_task = project.tasks.exclude(id=initial_task.id).first() # Worker 4 is certified for related task and should have been assigned self.assertEquals(related_task.assignments.count(), 1) self.assertEquals(related_task.status, Task.Status.PROCESSING) self.assertTrue( is_worker_assigned_to_task(self.workers[4], related_task)) # Reset project project.tasks.all().delete()
def reassign_assignment(worker_id, assignment_id): """ 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 _worker_certified_for_task(worker, assignment.task, role): raise WorkerCertificationError( 'Worker not certified for this assignment.') if is_worker_assigned_to_task(worker, assignment.task): raise TaskAssignmentError('Worker already assigned to this task.') assignment.worker = worker assignment.save() 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 is_worker_assigned_to_task(worker, task): 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 test_notify_status_change(self): project = self.projects['empty_project'] internal_name = settings.SLACK_INTERNAL_NOTIFICATION_CHANNEL.strip('#') internal_groups = [ group for group in self.slack.groups.list().body['groups'] if group['name'] == internal_name] internal_group_id = internal_groups[0]['id'] internal_slack_messages = self.slack.get_messages(internal_group_id) experts_slack_messages = self.slack.get_messages( project.slack_group_id) def _validate_slack_messages(message_stub): """ Check that correct slack message was sent if API key present. """ self.assertIn(message_stub, internal_slack_messages.pop()) self.assertIn(message_stub, experts_slack_messages.pop()) task = TaskFactory(project=project, step_slug=self.test_step_slug, status=Task.Status.AWAITING_PROCESSING) # Entry-level worker picks up task self.assertEquals(task.status, Task.Status.AWAITING_PROCESSING) task = assign_task(self.workers[0].id, task.id) self.assertTrue(is_worker_assigned_to_task(self.workers[0], task)) # Notification should be sent to entry-level worker self.assertEquals(len(self.mail.inbox), 1) notification = self.mail.inbox.pop() self.assertEquals(notification['recipient'], self.workers[0].user.email) self.assertEquals(notification['subject'], "You've been assigned to a new task!") _validate_slack_messages('Task has been picked up by a worker.') self.assertEquals(len(internal_slack_messages), 0) self.assertEquals(len(experts_slack_messages), 0) with patch('orchestra.utils.task_lifecycle._is_review_needed', return_value=True): # Entry-level worker submits task task = submit_task(task.id, {}, TaskAssignment.SnapshotType.SUBMIT, self.workers[0], 0) self.assertEquals(task.status, Task.Status.PENDING_REVIEW) # Notification should be sent to entry-level worker self.assertEquals(len(self.mail.inbox), 1) notification = self.mail.inbox.pop() self.assertEquals(notification['recipient'], self.workers[0].user.email) self.assertEquals(notification['subject'], 'Your task is under review!') _validate_slack_messages('Task is awaiting review.') self.assertEquals(len(internal_slack_messages), 0) self.assertEquals(len(experts_slack_messages), 0) # Reviewer picks up task task = assign_task(self.workers[1].id, task.id) self.assertEquals(task.status, Task.Status.REVIEWING) # No notification should be sent self.assertEquals(len(self.mail.inbox), 0) _validate_slack_messages('Task is under review.') self.assertEquals(len(internal_slack_messages), 0) self.assertEquals(len(experts_slack_messages), 0) # Reviewer rejects task task = submit_task(task.id, {}, TaskAssignment.SnapshotType.REJECT, self.workers[1], 0) self.assertEquals(task.status, Task.Status.POST_REVIEW_PROCESSING) # Notification should be sent to original worker self.assertEquals(len(self.mail.inbox), 1) notification = self.mail.inbox.pop() self.assertEquals(notification['recipient'], self.workers[0].user.email) self.assertEquals(notification['subject'], 'Your task has been returned') _validate_slack_messages('Task was returned by reviewer.') self.assertEquals(len(internal_slack_messages), 0) self.assertEquals(len(experts_slack_messages), 0) # Entry-level worker resubmits task task = submit_task(task.id, {}, TaskAssignment.SnapshotType.SUBMIT, self.workers[0], 0) self.assertEquals(task.status, Task.Status.REVIEWING) # Notification should be sent to reviewer self.assertEquals(len(self.mail.inbox), 1) notification = self.mail.inbox.pop() self.assertEquals(notification['recipient'], self.workers[1].user.email) self.assertEquals(notification['subject'], 'A task is ready for re-review!') _validate_slack_messages('Task is under review.') self.assertEquals(len(internal_slack_messages), 0) self.assertEquals(len(experts_slack_messages), 0) # First reviewer accepts task with patch('orchestra.utils.task_lifecycle._is_review_needed', return_value=True): task = submit_task(task.id, {}, TaskAssignment.SnapshotType.ACCEPT, self.workers[1], 0) self.assertEquals(task.status, Task.Status.PENDING_REVIEW) # Notification should be sent to first reviewer self.assertEquals(len(self.mail.inbox), 1) notification = self.mail.inbox.pop() self.assertEquals(notification['recipient'], self.workers[1].user.email) self.assertEquals(notification['subject'], 'Your task is under review!') _validate_slack_messages('Task is awaiting review.') self.assertEquals(len(internal_slack_messages), 0) self.assertEquals(len(experts_slack_messages), 0) # Second reviewer picks up task task = assign_task(self.workers[3].id, task.id) self.assertEquals(task.status, Task.Status.REVIEWING) # No notification should be sent self.assertEquals(len(self.mail.inbox), 0) _validate_slack_messages('Task is under review.') self.assertEquals(len(internal_slack_messages), 0) self.assertEquals(len(experts_slack_messages), 0) # Second reviewer rejects task task = submit_task(task.id, {}, TaskAssignment.SnapshotType.REJECT, self.workers[3], 0) self.assertEquals(task.status, Task.Status.POST_REVIEW_PROCESSING) # Notification should be sent to first reviewer self.assertEquals(len(self.mail.inbox), 1) notification = self.mail.inbox.pop() self.assertEquals(notification['recipient'], self.workers[1].user.email) self.assertEquals(notification['subject'], 'Your task has been returned') _validate_slack_messages('Task was returned by reviewer.') self.assertEquals(len(internal_slack_messages), 0) self.assertEquals(len(experts_slack_messages), 0) # First reviewer resubmits task task = submit_task(task.id, {}, TaskAssignment.SnapshotType.SUBMIT, self.workers[1], 0) self.assertEquals(task.status, Task.Status.REVIEWING) # Notification should be sent to second reviewer self.assertEquals(len(self.mail.inbox), 1) notification = self.mail.inbox.pop() self.assertEquals(notification['recipient'], self.workers[3].user.email) self.assertEquals(notification['subject'], 'A task is ready for re-review!') _validate_slack_messages('Task is under review.') self.assertEquals(len(internal_slack_messages), 0) self.assertEquals(len(experts_slack_messages), 0) # Second reviewer accepts task; task is complete with patch('orchestra.utils.task_lifecycle._is_review_needed', return_value=False): task = submit_task(task.id, {}, TaskAssignment.SnapshotType.ACCEPT, self.workers[3], 0) self.assertEquals(task.status, Task.Status.COMPLETE) # Notification should be sent to all workers on task self.assertEquals(len(self.mail.inbox), 3) recipients = {mail['recipient'] for mail in self.mail.inbox} subjects = {mail['subject'] for mail in self.mail.inbox} self.assertEquals(recipients, {self.workers[uid].user.email for uid in (0, 1, 3)}) self.assertEquals(subjects, {'Task complete!'}) self.mail.clear() _validate_slack_messages('Task has been completed.') self.assertEquals(len(internal_slack_messages), 0) self.assertEquals(len(experts_slack_messages), 0) # End project end_project(task.project.id) task = Task.objects.get(id=task.id) self.assertEquals(task.status, Task.Status.ABORTED) # Notification should be sent to all workers on task self.assertEquals(len(self.mail.inbox), 3) recipients = {mail['recipient'] for mail in self.mail.inbox} subjects = {mail['subject'] for mail in self.mail.inbox} self.assertEquals(recipients, {self.workers[uid].user.email for uid in (0, 1, 3)}) self.assertEquals(subjects, {'A task you were working on has been ended'}) self.mail.clear() for task in project.tasks.all(): _validate_slack_messages('Task has been aborted.') self.assertEquals(len(internal_slack_messages), 0) self.assertEquals(len(experts_slack_messages), 0)
def test_notify_status_change(self): project = self.projects['empty_project'] internal_name = settings.SLACK_INTERNAL_NOTIFICATION_CHANNEL.strip('#') internal_groups = [ group for group in self.slack.groups.list().body['groups'] if group['name'] == internal_name ] internal_group_id = internal_groups[0]['id'] internal_slack_messages = self.slack.get_messages(internal_group_id) experts_slack_messages = self.slack.get_messages( project.slack_group_id) def _validate_slack_messages(message_stub): """ Check that correct slack message was sent if API key present. """ self.assertIn(message_stub, internal_slack_messages.pop()) self.assertIn(message_stub, experts_slack_messages.pop()) task = TaskFactory(project=project, step=self.test_step, status=Task.Status.AWAITING_PROCESSING) # Entry-level worker picks up task self.assertEquals(task.status, Task.Status.AWAITING_PROCESSING) task = assign_task(self.workers[0].id, task.id) self.assertTrue(is_worker_assigned_to_task(self.workers[0], task)) # Notification should be sent to entry-level worker self.assertEquals(len(self.mail.inbox), 1) notification = self.mail.inbox.pop() self.assertEquals(notification['recipient'], self.workers[0].user.email) self.assertEquals(notification['subject'], "You've been assigned to a new task!") _validate_slack_messages('Task has been picked up by a worker.') self.assertEquals(len(internal_slack_messages), 0) self.assertEquals(len(experts_slack_messages), 0) with patch('orchestra.utils.task_lifecycle._is_review_needed', return_value=True): # Entry-level worker submits task task = submit_task(task.id, {}, TaskAssignment.SnapshotType.SUBMIT, self.workers[0], 0) self.assertEquals(task.status, Task.Status.PENDING_REVIEW) # Notification should be sent to entry-level worker self.assertEquals(len(self.mail.inbox), 1) notification = self.mail.inbox.pop() self.assertEquals(notification['recipient'], self.workers[0].user.email) self.assertEquals(notification['subject'], 'Your task is under review!') _validate_slack_messages('Task is awaiting review.') self.assertEquals(len(internal_slack_messages), 0) self.assertEquals(len(experts_slack_messages), 0) # Reviewer picks up task task = assign_task(self.workers[1].id, task.id) self.assertEquals(task.status, Task.Status.REVIEWING) # No notification should be sent self.assertEquals(len(self.mail.inbox), 0) _validate_slack_messages('Task is under review.') self.assertEquals(len(internal_slack_messages), 0) self.assertEquals(len(experts_slack_messages), 0) # Reviewer rejects task task = submit_task(task.id, {}, TaskAssignment.SnapshotType.REJECT, self.workers[1], 0) self.assertEquals(task.status, Task.Status.POST_REVIEW_PROCESSING) # Notification should be sent to original worker self.assertEquals(len(self.mail.inbox), 1) notification = self.mail.inbox.pop() self.assertEquals(notification['recipient'], self.workers[0].user.email) self.assertEquals(notification['subject'], 'Your task has been returned') _validate_slack_messages('Task was returned by reviewer.') self.assertEquals(len(internal_slack_messages), 0) self.assertEquals(len(experts_slack_messages), 0) # Entry-level worker resubmits task task = submit_task(task.id, {}, TaskAssignment.SnapshotType.SUBMIT, self.workers[0], 0) self.assertEquals(task.status, Task.Status.REVIEWING) # Notification should be sent to reviewer self.assertEquals(len(self.mail.inbox), 1) notification = self.mail.inbox.pop() self.assertEquals(notification['recipient'], self.workers[1].user.email) self.assertEquals(notification['subject'], 'A task is ready for re-review!') _validate_slack_messages('Task is under review.') self.assertEquals(len(internal_slack_messages), 0) self.assertEquals(len(experts_slack_messages), 0) # First reviewer accepts task with patch('orchestra.utils.task_lifecycle._is_review_needed', return_value=True): task = submit_task(task.id, {}, TaskAssignment.SnapshotType.ACCEPT, self.workers[1], 0) self.assertEquals(task.status, Task.Status.PENDING_REVIEW) # Notification should be sent to first reviewer self.assertEquals(len(self.mail.inbox), 1) notification = self.mail.inbox.pop() self.assertEquals(notification['recipient'], self.workers[1].user.email) self.assertEquals(notification['subject'], 'Your task is under review!') _validate_slack_messages('Task is awaiting review.') self.assertEquals(len(internal_slack_messages), 0) self.assertEquals(len(experts_slack_messages), 0) # Second reviewer picks up task task = assign_task(self.workers[3].id, task.id) self.assertEquals(task.status, Task.Status.REVIEWING) # No notification should be sent self.assertEquals(len(self.mail.inbox), 0) _validate_slack_messages('Task is under review.') self.assertEquals(len(internal_slack_messages), 0) self.assertEquals(len(experts_slack_messages), 0) # Second reviewer rejects task task = submit_task(task.id, {}, TaskAssignment.SnapshotType.REJECT, self.workers[3], 0) self.assertEquals(task.status, Task.Status.POST_REVIEW_PROCESSING) # Notification should be sent to first reviewer self.assertEquals(len(self.mail.inbox), 1) notification = self.mail.inbox.pop() self.assertEquals(notification['recipient'], self.workers[1].user.email) self.assertEquals(notification['subject'], 'Your task has been returned') _validate_slack_messages('Task was returned by reviewer.') self.assertEquals(len(internal_slack_messages), 0) self.assertEquals(len(experts_slack_messages), 0) # First reviewer resubmits task task = submit_task(task.id, {}, TaskAssignment.SnapshotType.SUBMIT, self.workers[1], 0) self.assertEquals(task.status, Task.Status.REVIEWING) # Notification should be sent to second reviewer self.assertEquals(len(self.mail.inbox), 1) notification = self.mail.inbox.pop() self.assertEquals(notification['recipient'], self.workers[3].user.email) self.assertEquals(notification['subject'], 'A task is ready for re-review!') _validate_slack_messages('Task is under review.') self.assertEquals(len(internal_slack_messages), 0) self.assertEquals(len(experts_slack_messages), 0) # Second reviewer accepts task; task is complete with patch('orchestra.utils.task_lifecycle._is_review_needed', return_value=False): task = submit_task(task.id, {}, TaskAssignment.SnapshotType.ACCEPT, self.workers[3], 0) self.assertEquals(task.status, Task.Status.COMPLETE) # Notification should be sent to all workers on task self.assertEquals(len(self.mail.inbox), 3) recipients = {mail['recipient'] for mail in self.mail.inbox} subjects = {mail['subject'] for mail in self.mail.inbox} self.assertEquals(recipients, {self.workers[uid].user.email for uid in (0, 1, 3)}) self.assertEquals(subjects, {'Task complete!'}) self.mail.clear() _validate_slack_messages('Task has been completed.') self.assertEquals(len(internal_slack_messages), 0) self.assertEquals(len(experts_slack_messages), 0) # End project end_project(task.project.id) task = Task.objects.get(id=task.id) self.assertEquals(task.status, Task.Status.ABORTED) # Notification should be sent to all workers on task self.assertEquals(len(self.mail.inbox), 3) recipients = {mail['recipient'] for mail in self.mail.inbox} subjects = {mail['subject'] for mail in self.mail.inbox} self.assertEquals(recipients, {self.workers[uid].user.email for uid in (0, 1, 3)}) self.assertEquals(subjects, {'A task you were working on has been ended'}) self.mail.clear() for task in project.tasks.all(): _validate_slack_messages('Task has been aborted.') self.assertEquals(len(internal_slack_messages), 0) self.assertEquals(len(experts_slack_messages), 0)
def test_assign_task(self): entry_task = TaskFactory( project=self.projects['base_test_project'], status=Task.Status.AWAITING_PROCESSING, step=self.test_step) # Assign entry-level task to entry-level worker entry_task = assign_task(self.workers[0].id, entry_task.id) self.assertTrue(is_worker_assigned_to_task(self.workers[0], entry_task)) self.assertEquals(entry_task.status, Task.Status.PROCESSING) self.assertEquals(entry_task.assignments.count(), 1) # Attempt to assign task which isn't awaiting a new assignment invalid = (Task.Status.PROCESSING, Task.Status.ABORTED, Task.Status.REVIEWING, Task.Status.COMPLETE, Task.Status.POST_REVIEW_PROCESSING) for status in invalid: invalid_status_task = Task.objects.create( project=self.projects['base_test_project'], status=status, step=self.test_step) with self.assertRaises(TaskAssignmentError): invalid_status_task = assign_task( self.workers[0].id, invalid_status_task.id) # Attempt to assign review task to worker already in review hierarchy review_task = Task.objects.create( project=self.projects['base_test_project'], status=Task.Status.PENDING_REVIEW, step=self.test_step) test_data = {'test_assign': True} TaskAssignmentFactory( worker=self.workers[1], task=review_task, status=TaskAssignment.Status.SUBMITTED, in_progress_task_data=test_data, snapshots=empty_snapshots()) with self.assertRaises(TaskAssignmentError): assign_task(self.workers[1].id, review_task.id) self.assertEqual( current_assignment(review_task).in_progress_task_data, test_data) # Attempt to assign review task to worker not certified for task with self.assertRaises(WorkerCertificationError): assign_task(self.workers[2].id, review_task.id) self.assertEqual( current_assignment(review_task).in_progress_task_data, test_data) # Assign review task to review worker self.assertEquals(review_task.assignments.count(), 1) review_task = assign_task(self.workers[3].id, review_task.id) self.assertEquals(review_task.assignments.count(), 2) self.assertEqual( current_assignment(review_task).worker, self.workers[3]) self.assertEqual( current_assignment(review_task).in_progress_task_data, test_data) self.assertEquals( review_task.status, Task.Status.REVIEWING)
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, requires_reassign = _role_required_to_assign(task) assignment = current_assignment(task) if not _worker_certified_for_task(worker, task, required_role): raise WorkerCertificationError('Worker not certified for this task.') if is_worker_assigned_to_task(worker, task): raise TaskAssignmentError('Worker already assigned to this task.') # If task is currently in progress, reassign it if requires_reassign: assignment.worker = worker assignment.save() add_worker_to_project_team(worker, task.project) return task # Otherwise, create new assignment assignment_counter = task.assignments.count() in_progress_task_data = {} if required_role == WorkerCertification.Role.REVIEWER: # 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 else: raise TaskAssignmentError('Status incompatible with new assignment') task.save() (TaskAssignment.objects .create(worker=worker, task=task, status=TaskAssignment.Status.PROCESSING, assignment_counter=assignment_counter, in_progress_task_data=in_progress_task_data, snapshots=empty_snapshots())) add_worker_to_project_team(worker, task.project) notify_status_change(task, previous_status) return task
def test_task_form_save(self): """ Test task form save for new, human and machine tasks """ # Workflow steps are hard-coded on `choices` for `Project` models # regardless of `settings.py`. Once we move workflows back into the # database, we should use the test workflows rather than the production # ones in `settings.py.` Until then, the hack below suffices. workflows = get_workflows() test_workflow_slug = 'website_design' workflow = workflows[test_workflow_slug] human_steps = {step_slug: step for step_slug, step in workflow.steps.items() if step.worker_type == Step.WorkerType.HUMAN} step_slug, step = human_steps.popitem() project = ProjectFactory(workflow_slug=test_workflow_slug) for certification_slug in step.required_certifications: certification = CertificationFactory(slug=certification_slug) for uname in (0, 1, 3, 6): WorkerCertificationFactory( certification=certification, worker=self.workers[uname], role=WorkerCertification.Role.ENTRY_LEVEL) for uname in (3, 6): WorkerCertificationFactory( certification=certification, worker=self.workers[uname], role=WorkerCertification.Role.REVIEWER) # Add new task to project form = TaskForm({'project': project.id, 'status': Task.Status.AWAITING_PROCESSING, 'step_slug': step_slug}) form.is_valid() self.assertTrue(form.is_valid()) task = form.save() self.assertFalse(task.assignments.exists()) # Add new task to project and assign to entry_level worker (0) form = TaskForm({'project': project.id, 'status': Task.Status.AWAITING_PROCESSING, 'step_slug': step_slug}) self.assertTrue(form.is_valid()) form.cleaned_data['currently_assigned_to'] = self.workers[0].id task = form.save() self.assertTrue(is_worker_assigned_to_task(self.workers[0], task)) self.assertEquals(assignment_history(task).count(), 1) self.assertTrue(task.assignments.exists()) self.assertEquals(task.status, Task.Status.PROCESSING) # Render task with preexisting entry_level assignment (0) and reassign # to another entry_level worker (1) form = TaskForm(model_to_dict(task), instance=task) self.assertEquals(form.fields['currently_assigned_to'].initial, self.workers[0].id) form.is_valid() self.assertTrue(form.is_valid()) form.cleaned_data['currently_assigned_to'] = self.workers[1].id task = form.save() self.assertTrue(is_worker_assigned_to_task(self.workers[1], task)) self.assertEquals(assignment_history(task).count(), 1) self.assertEquals(task.status, Task.Status.PROCESSING) # Submit task with patch('orchestra.utils.task_lifecycle._is_review_needed', return_value=True): task = submit_task(task.id, {}, TaskAssignment.SnapshotType.SUBMIT, self.workers[1], 0) # Assign to reviewer (3) and reassign to another reviewer (6) task = assign_task(self.workers[3].id, task.id) self.assertTrue(task.status, Task.Status.REVIEWING) self.assertTrue(is_worker_assigned_to_task(self.workers[3], task)) task = Task.objects.get(id=task.id) form = TaskForm(model_to_dict(task), instance=task) self.assertEquals(form.fields['currently_assigned_to'].initial, self.workers[3].id) self.assertTrue(form.is_valid()) form.cleaned_data['currently_assigned_to'] = self.workers[6].id task = form.save() self.assertTrue(is_worker_assigned_to_task(self.workers[6], task)) self.assertEquals(assignment_history(task).count(), 2) self.assertEquals(task.status, Task.Status.REVIEWING) # Attempt to reassign to non-certified worker (2) form = TaskForm(model_to_dict(task), instance=task) self.assertTrue(form.is_valid()) form.cleaned_data['currently_assigned_to'] = self.workers[2].id with self.assertRaises(WorkerCertificationError): form.save()
def test_assign_task(self): # Assign entry-level task to entry-level worker entry_tasks = Task.objects.filter( status=Task.Status.AWAITING_PROCESSING) self.assertEquals(entry_tasks.count(), 1) entry_task = entry_tasks.first() entry_task = assign_task(self.workers[0].id, entry_task.id) self.assertTrue(is_worker_assigned_to_task(self.workers[0], entry_task)) self.assertEquals(entry_task.status, Task.Status.PROCESSING) self.assertEquals(entry_task.assignments.count(), 1) # Attempt to reassign task to same worker with self.assertRaises(TaskAssignmentError): entry_task = assign_task(self.workers[0].id, entry_task.id) self.assertTrue(is_worker_assigned_to_task(self.workers[0], entry_task)) self.assertEquals(entry_task.status, Task.Status.PROCESSING) self.assertEquals(entry_task.assignments.count(), 1) # Reassign entry-level task to another entry-level worker entry_task = assign_task(self.workers[1].id, entry_task.id) self.assertFalse(is_worker_assigned_to_task(self.workers[0], entry_task)) self.assertTrue(is_worker_assigned_to_task(self.workers[1], entry_task)) self.assertEquals(entry_task.assignments.count(), 1) self.assertEquals(entry_task.status, Task.Status.PROCESSING) # Assign review task to review worker review_tasks = Task.objects.filter(status=Task.Status.PENDING_REVIEW) self.assertEquals(review_tasks.count(), 1) review_task = review_tasks.first() self.assertEquals(review_task.assignments.count(), 1) review_task = assign_task(self.workers[1].id, review_task.id) self.assertEquals(review_task.assignments.count(), 2) self.assertEqual(current_assignment(review_task).worker, self.workers[1]) self.assertEquals(review_task.status, Task.Status.REVIEWING) # Attempt to reassign review task to entry-level worker with self.assertRaises(WorkerCertificationError): review_task = assign_task(self.workers[0].id, review_task.id) self.assertEquals(review_task.assignments.count(), 2) self.assertEqual(current_assignment(review_task).worker, self.workers[1]) self.assertEquals(review_task.status, Task.Status.REVIEWING) # Reassign review task to another reviewer review_task = assign_task(self.workers[3].id, review_task.id) self.assertEquals(review_task.assignments.count(), 2) self.assertEqual(current_assignment(review_task).worker, self.workers[3]) self.assertEquals(review_task.status, Task.Status.REVIEWING) # Reassign rejected entry-level task to another entry-level worker reject_entry_tasks = Task.objects.filter( status=Task.Status.POST_REVIEW_PROCESSING, project=self.projects['reject_entry_proj']) self.assertEquals(reject_entry_tasks.count(), 1) reject_entry_task = reject_entry_tasks.first() reject_entry_task = assign_task(self.workers[5].id, reject_entry_task.id) self.assertFalse(is_worker_assigned_to_task(self.workers[4], reject_entry_task)) self.assertTrue(is_worker_assigned_to_task(self.workers[5], reject_entry_task)) self.assertEquals(reject_entry_task.status, Task.Status.POST_REVIEW_PROCESSING) self.assertEquals(reject_entry_task.assignments.count(), 2) # In-progress data preserved after successful reassign self.assertEquals((current_assignment(reject_entry_task) .in_progress_task_data), {'test_key': 'test_value'}) # Attempt to reassign rejected review task to entry-level worker reject_tasks = Task.objects.filter( status=Task.Status.POST_REVIEW_PROCESSING, project=self.projects['reject_rev_proj']) self.assertEquals(reject_tasks.count(), 1) reject_review_task = reject_tasks.first() with self.assertRaises(WorkerCertificationError): reject_review_task = assign_task(self.workers[4].id, reject_review_task.id) self.assertFalse(is_worker_assigned_to_task(self.workers[4], reject_review_task)) self.assertTrue(is_worker_assigned_to_task(self.workers[6], reject_review_task)) self.assertEquals(reject_review_task.status, Task.Status.POST_REVIEW_PROCESSING) self.assertEquals(reject_review_task.assignments.count(), 3) # Reassign reviewer post-review task to another reviewer reject_review_task = assign_task(self.workers[8].id, reject_review_task.id) self.assertFalse(is_worker_assigned_to_task(self.workers[6], reject_review_task)) self.assertTrue(is_worker_assigned_to_task(self.workers[8], reject_review_task)) self.assertEquals(reject_review_task.status, Task.Status.POST_REVIEW_PROCESSING) self.assertEquals(reject_review_task.assignments.count(), 3) # Attempt to reassign aborted task aborted_tasks = Task.objects.filter(status=Task.Status.ABORTED) self.assertEquals(aborted_tasks.count(), 1) aborted_task = aborted_tasks.first() with self.assertRaises(TaskStatusError): aborted_task = assign_task(self.workers[5].id, aborted_task.id) self.assertEquals(aborted_task.assignments.count(), 1) self.assertEqual(current_assignment(aborted_task).worker, self.workers[4])
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) 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') assignment = current_assignment(task) if not _worker_certified_for_task(worker, task, required_role): raise WorkerCertificationError('Worker not certified for this task.') if is_worker_assigned_to_task(worker, task): 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() (TaskAssignment.objects.create(worker=worker, task=task, status=TaskAssignment.Status.PROCESSING, assignment_counter=assignment_counter, in_progress_task_data=in_progress_task_data, snapshots=empty_snapshots())) add_worker_to_project_team(worker, task.project) notify_status_change(task, previous_status) return task
def test_task_form_save(self): """ Test task form save for new, human and machine tasks """ workflow_version = self.workflow_versions['test_workflow'] human_step = self.workflow_steps[workflow_version.slug]['step1'] project = ProjectFactory(workflow_version=workflow_version) # Add new task to project form = TaskForm({'project': project.id, 'status': Task.Status.AWAITING_PROCESSING, 'step': human_step.id, 'start_datetime': timezone.now()}) form.is_valid() self.assertTrue(form.is_valid()) task = form.save() self.assertFalse(task.assignments.exists()) # Add new task to project and assign to entry_level worker (0) form = TaskForm({'project': project.id, 'status': Task.Status.AWAITING_PROCESSING, 'step': human_step.id, 'start_datetime': timezone.now()}) self.assertTrue(form.is_valid()) form.cleaned_data['currently_assigned_to'] = self.workers[0].id task = form.save() self.assertTrue(is_worker_assigned_to_task(self.workers[0], task)) self.assertEquals(assignment_history(task).count(), 1) self.assertTrue(task.assignments.exists()) self.assertEquals(task.status, Task.Status.PROCESSING) # Render task with preexisting entry_level assignment (0) and reassign # to another entry_level worker (4) form = TaskForm(model_to_dict(task), instance=task) self.assertEquals(form.fields['currently_assigned_to'].initial, self.workers[0].id) form.is_valid() self.assertTrue(form.is_valid()) form.cleaned_data['currently_assigned_to'] = self.workers[4].id task = form.save() self.assertTrue(is_worker_assigned_to_task(self.workers[4], task)) self.assertEquals(assignment_history(task).count(), 1) self.assertEquals(task.status, Task.Status.PROCESSING) # Submit task with patch('orchestra.utils.task_lifecycle._is_review_needed', return_value=True): task = submit_task(task.id, {}, TaskAssignment.SnapshotType.SUBMIT, self.workers[4], 0) # Assign to reviewer (1) and reassign to another reviewer (3) task = assign_task(self.workers[1].id, task.id) self.assertTrue(task.status, Task.Status.REVIEWING) self.assertTrue(is_worker_assigned_to_task(self.workers[1], task)) task = Task.objects.get(id=task.id) form = TaskForm(model_to_dict(task), instance=task) self.assertEquals(form.fields['currently_assigned_to'].initial, self.workers[1].id) self.assertTrue(form.is_valid()) form.cleaned_data['currently_assigned_to'] = self.workers[3].id task = form.save() self.assertTrue(is_worker_assigned_to_task(self.workers[3], task)) self.assertEquals(assignment_history(task).count(), 2) self.assertEquals(task.status, Task.Status.REVIEWING) # Attempt to reassign to non-certified worker (2) form = TaskForm(model_to_dict(task), instance=task) self.assertTrue(form.is_valid()) form.cleaned_data['currently_assigned_to'] = self.workers[2].id with self.assertRaises(WorkerCertificationError): form.save()