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_reverted_task(self, task, iteration, num_iterations, task_status, latest_data, expected_audit, revert_before=False): response = self._revert_task( task, iteration, revert_before=revert_before, commit=False) self.assertEqual(response.status_code, 200) task.refresh_from_db() fake_audit = load_encoded_json(response.content) self.assertEqual(fake_audit, expected_audit) self.assertEqual(task.status, Task.Status.COMPLETE) self.assertEqual(task.assignments.count(), 2) for assignment in task.assignments.all(): self.assertEqual(assignment.iterations.count(), 2) response = self._revert_task( task, iteration, revert_before=revert_before, commit=True) self.assertEqual(response.status_code, 200) task.refresh_from_db() audit = load_encoded_json(response.content) self.assertEqual(audit, fake_audit) task.refresh_from_db() self.assertEqual(task.status, task_status) self.assertEqual( get_iteration_history(task).count(), num_iterations) verify_iterations(task.id) if num_iterations: self.assertEqual( current_assignment(task).in_progress_task_data, latest_data) verify_iterations(task.id)
def _verify_final_iteration(iteration): # Last iteration should belong to current assignment assignment = iteration.assignment assert assignment == current_assignment(assignment.task) # Map final iteration statuses onto task statuses task_statuses = { Iteration.Status.PROCESSING: [ Task.Status.PROCESSING, Task.Status.REVIEWING, Task.Status.POST_REVIEW_PROCESSING ], Iteration.Status.REQUESTED_REVIEW: [Task.Status.PENDING_REVIEW, Task.Status.COMPLETE], Iteration.Status.PROVIDED_REVIEW: [Task.Status.POST_REVIEW_PROCESSING] } # A task awaiting processing should not have iterations assignment.task.status != Task.Status.AWAITING_PROCESSING for k, v in task_statuses.items(): # An aborted task could have any iteration configuration task_statuses[k].append(Task.Status.ABORTED) if iteration.status == Iteration.Status.PROCESSING: expected_assignment_status = TaskAssignment.Status.PROCESSING else: expected_assignment_status = TaskAssignment.Status.SUBMITTED # Check that task and assignment statuses are correctly set assert assignment.status == expected_assignment_status assert assignment.task.status in task_statuses[iteration.status]
def _verify_final_iteration(iteration): # Last iteration should belong to current assignment assignment = iteration.assignment assert assignment == current_assignment(assignment.task) # Map final iteration statuses onto task statuses task_statuses = { Iteration.Status.PROCESSING: [ Task.Status.PROCESSING, Task.Status.REVIEWING, Task.Status.POST_REVIEW_PROCESSING ], Iteration.Status.REQUESTED_REVIEW: [ Task.Status.PENDING_REVIEW, Task.Status.COMPLETE ], Iteration.Status.PROVIDED_REVIEW: [ Task.Status.POST_REVIEW_PROCESSING ] } # A task awaiting processing should not have iterations assignment.task.status != Task.Status.AWAITING_PROCESSING for k, v in task_statuses.items(): # An aborted task could have any iteration configuration task_statuses[k].append(Task.Status.ABORTED) if iteration.status == Iteration.Status.PROCESSING: expected_assignment_status = TaskAssignment.Status.PROCESSING else: expected_assignment_status = TaskAssignment.Status.SUBMITTED # Check that task and assignment statuses are correctly set assert assignment.status == expected_assignment_status assert assignment.task.status in task_statuses[iteration.status]
def complete_and_skip_task(task_id): """ Submits a task on behalf of the worker working on it. If the task isn't assigned to a worker, marks it and its assignments as complete and creates subsequent tasks. Args: task_id (int): The ID of the task to be marked as complete and skipped. Returns: task (orchestra.models.Task): The completed and skipped task. """ task = Task.objects.get(id=task_id) assignment = current_assignment(task) if assignment and assignment.worker: if assignment.status != TaskAssignment.Status.PROCESSING: return task task_data = assignment.in_progress_task_data or {} task_data.update(_orchestra_internal={'complete_and_skip_task': True}) submit_task(task_id, task_data, Iteration.Status.REQUESTED_REVIEW, assignment.worker) else: task.status = Task.Status.COMPLETE task.save() for assignment in task.assignments.all(): assignment.status = TaskAssignment.Status.SUBMITTED assignment.save() return task
def _test_reverted_task(self, task, iteration, num_iterations, task_status, latest_data, expected_audit, revert_before=False): response = self._revert_task( task, iteration, revert_before=revert_before, commit=False) self.assertEquals(response.status_code, 200) task.refresh_from_db() fake_audit = load_encoded_json(response.content) self.assertEqual(fake_audit, expected_audit) self.assertEquals(task.status, Task.Status.COMPLETE) self.assertEquals(task.assignments.count(), 2) for assignment in task.assignments.all(): self.assertEquals(assignment.iterations.count(), 2) response = self._revert_task( task, iteration, revert_before=revert_before, commit=True) self.assertEqual(response.status_code, 200) task.refresh_from_db() audit = load_encoded_json(response.content) self.assertEqual(audit, fake_audit) task.refresh_from_db() self.assertEqual(task.status, task_status) self.assertEqual( get_iteration_history(task).count(), num_iterations) verify_iterations(task.id) if num_iterations: self.assertEquals( current_assignment(task).in_progress_task_data, latest_data) verify_iterations(task.id)
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 __init__(self, *args, **kwargs): super(TaskForm, self).__init__(*args, **kwargs) workers = Worker.objects.all() choices = [(None, None)] + [(w.id, w.user.username) for w in workers] self.fields["currently_assigned_to"].choices = choices if self._instance_created() and self.instance.assignments.exists(): assignment = current_assignment(self.instance) if assignment.worker: # If human task, select human worker (self.fields["currently_assigned_to"].initial) = assignment.worker.id
def test_task_history_details(self): task = self.tasks['processing_task'] observed = task_history_details(task.id) observed['assignment_history'] = list(observed['assignment_history']) expected = { 'current_assignment': current_assignment(task), 'assignment_history': list(assignment_history(task)) } self.assertEquals( json.dumps(observed, sort_keys=True), json.dumps(expected, sort_keys=True))
def _test_reverted_task(self, times, datetime, status, num_assignments, num_snapshots_per_assignment, latest_data): task = setup_complete_task(self, times) response = self.api_client.post( reverse('orchestra:orchestra:project_management:revert_task'), json.dumps({ 'task_id': task.id, # Convert datetime to timestamp 'revert_datetime': time.mktime(datetime.timetuple()), 'fake': True }), content_type='application/json') self.assertEquals(response.status_code, 200) task.refresh_from_db() self.assertEquals(task.status, Task.Status.COMPLETE) self.assertEquals(task.assignments.count(), 2) for assignment in task.assignments.all(): self.assertEquals( len(assignment.snapshots['snapshots']), 2) response = self.api_client.post( reverse('orchestra:orchestra:project_management:revert_task'), json.dumps({ 'task_id': task.id, # Convert datetime to timestamp 'revert_datetime': time.mktime(datetime.timetuple()), 'fake': False }), content_type='application/json') self.assertEquals(response.status_code, 200) audit = json.loads(response.content.decode('utf-8')) self._test_audit(audit, num_assignments, num_snapshots_per_assignment) task.refresh_from_db() self.assertEquals(task.status, status) assignments = assignment_history(task) self.assertEquals(task.assignments.count(), num_assignments) self.assertEquals( len(num_snapshots_per_assignment), num_assignments) for i, num_snapshots in enumerate(num_snapshots_per_assignment): self.assertEquals( len(assignments[i].snapshots['snapshots']), num_snapshots) if num_assignments: self.assertEquals( current_assignment(task).in_progress_task_data, latest_data) task.delete()
def get_new_task_assignment(worker, task_status): """ Check if new task assignment is available for the provided worker and task status; if so, assign the task to the worker and return the assignment. 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: assignment (orchestra.models.TaskAssignment): The newly created task assignment. Raises: orchestra.core.errors.WorkerCertificationError: No human tasks are available for the given task status except those for which the worker is not certified. orchestra.core.errors.NoTaskAvailable: No human tasks are available for the given task status. """ assert_new_task_status_valid(task_status) check_worker_allowed_new_assignment(worker) tasks = (Task.objects .filter(status=task_status) .exclude(assignments__worker=worker) .order_by('-project__priority') .order_by('project__start_datetime')) certification_error = False for task in tasks.iterator(): try: task = assign_task(worker.id, task.id) return current_assignment(task) except WorkerCertificationError: certification_error = True except ModelSaveError: # Machine task cannot have human worker; treat machine tasks as if # they do not exist pass if certification_error: raise WorkerCertificationError else: raise NoTaskAvailable('No task available for {}'.format(worker))
def get_new_task_assignment(worker, task_status): """ Check if new task assignment is available for the provided worker and task status; if so, assign the task to the worker and return the assignment. 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: assignment (orchestra.models.TaskAssignment): The newly created task assignment. Raises: orchestra.core.errors.WorkerCertificationError: No human tasks are available for the given task status except those for which the worker is not certified. orchestra.core.errors.NoTaskAvailable: No human tasks are available for the given task status. """ assert_new_task_status_valid(task_status) check_worker_allowed_new_assignment(worker) tasks = (Task.objects .filter(status=task_status) .exclude(assignments__worker=worker) .order_by('-project__priority') .order_by('project__start_datetime')) certification_error = False for task in tasks: try: task = assign_task(worker.id, task.id) return current_assignment(task) except WorkerCertificationError: certification_error = True except ModelSaveError: # Machine task cannot have human worker; treat machine tasks as if # they do not exist pass if certification_error: raise WorkerCertificationError else: raise NoTaskAvailable('No task available for {}'.format(worker))
def staff_task(request): data = load_encoded_json(request.body) errors = {} try: task = Task.objects.get(id=data.get('task_id')) request_cause = StaffBotRequest.RequestCause.USER.value bot = StaffBot() assignment = current_assignment(task) is_restaff = assignment is not None if is_restaff: username = assignment.worker.user.username bot.restaff(task.id, username, request_cause=request_cause) else: bot.staff(task.id, request_cause=request_cause) except Exception as e: raise BadRequest(e) success = len(errors) == 0 return {'success': success, 'is_restaff': is_restaff}
def task_history_details(task_id): """ Return assignment details for a specified task. Args: task_id (int): The ID of the desired task object. Returns: details (dict): A dictionary containing the current task assignment and an in-order list of related task assignments. """ task = Task.objects.get(id=task_id) details = { 'current_assignment': current_assignment(task), 'assignment_history': assignment_history(task) } return 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 notify_status_change(task, previous_status=None): """ Notify workers after task has changed state """ task_assignments = assignment_history(task) current_task_assignment = current_assignment(task) current_worker = None if current_task_assignment: current_worker = current_task_assignment.worker message_info = None # Notify worker when task initially picked up if task.status == Task.Status.PROCESSING: message_info = { "subject": "You've been assigned to a new task!", "message": ("You've been assigned to a new task. We can't wait " "to see the great things you'll do!"), "recipient_list": [current_worker.user.email], } # Notify worker when assignment selected for review elif task.status == Task.Status.PENDING_REVIEW: message_info = { "subject": "Your task is under review!", "message": ( "Thanks for all your hard work, {}! The following " "task was randomly selected for review by another " "expert; you should hear back soon!" ).format(current_worker.user.username), "recipient_list": [current_worker.user.email], } # Notify worker when assignment rejected elif task.status == Task.Status.POST_REVIEW_PROCESSING: message_info = { "subject": "Your task has been returned", "message": ( "Your reviewer sent back your task for a bit more " "polish. Check out the feedback as soon as you can!" ), "recipient_list": [current_worker.user.email], } # Notify all workers on a task when it has been completed elif task.status == Task.Status.COMPLETE: message_info = { "subject": "Task complete!", "message": "Congratulations! The task you worked on is complete.", "recipient_list": [ assignment.worker.user.email for assignment in task_assignments if assignment.worker and assignment.worker.user.email ], } # Notify reviewer when task pending update is ready for re-review, but not # for a task moving from PENDING_REVIEW to REVIEWING elif task.status == Task.Status.REVIEWING and previous_status == Task.Status.POST_REVIEW_PROCESSING: message_info = { "subject": "A task is ready for re-review!", "message": ("A task has been updated and is ready for " "re-review!"), "recipient_list": [current_worker.user.email], } # Notify all workers on a task when it has been aborted elif task.status == Task.Status.ABORTED: message_info = { "subject": "A task you were working on has been ended", "message": ( "Unfortunately, the task you were working on has " "been ended. Please reach out to us if you think this " "has been done in error." ), "recipient_list": [ assignment.worker.user.email for assignment in task_assignments if assignment.worker and assignment.worker.user.email ], } _notify_internal_slack_status_change(task, current_worker) if task.project.slack_group_id: _notify_experts_slack_status_change(task, current_worker) if message_info is not None: message_info["message"] += _task_information(task) send_mail(from_email=settings.ORCHESTRA_NOTIFICATIONS_FROM_EMAIL, fail_silently=True, **message_info)
def test_assign_task(self): entry_task = TaskFactory( project=self.projects['base_test_project'], status=Task.Status.AWAITING_PROCESSING, step=self.test_step) # No iterations should be present for task self.assertEqual( Iteration.objects.filter(assignment__task=entry_task).count(), 0) # Assign entry-level task to entry-level worker entry_task = assign_task(self.workers[0].id, entry_task.id) self.assertTrue(entry_task.is_worker_assigned(self.workers[0])) self.assertEqual(entry_task.status, Task.Status.PROCESSING) self.assertEqual(entry_task.assignments.count(), 1) entry_assignment = entry_task.assignments.first() # A single iteration was created for the assignment self.assertEqual(entry_assignment.iterations.count(), 1) self.assertEqual( Iteration.objects.filter(assignment__task=entry_task).count(), 1) self.assertEqual( entry_assignment.iterations.first().start_datetime, entry_assignment.start_datetime) # 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) 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) reviewer_assignment = current_assignment(review_task) self.assertEqual( reviewer_assignment.worker, self.workers[3]) self.assertEqual( reviewer_assignment.in_progress_task_data, test_data) self.assertEquals( reviewer_assignment.iterations.count(), 1) self.assertEqual( reviewer_assignment.iterations.first().start_datetime, reviewer_assignment.start_datetime) self.assertEquals( review_task.status, Task.Status.REVIEWING)
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 notify_status_change(task, previous_status=None): """ Notify workers after task has changed state """ task_assignments = assignment_history(task) current_task_assignment = current_assignment(task) current_worker = None if current_task_assignment: current_worker = current_task_assignment.worker message_info = None # Notify worker when task initially picked up if task.status == Task.Status.PROCESSING: message_info = { 'subject': "You've been assigned to a new task!", 'message': ("You've been assigned to a new task. We can't wait " "to see the great things you'll do!"), 'recipient_list': [current_worker.user.email] } # Notify worker when assignment selected for review elif task.status == Task.Status.PENDING_REVIEW: message_info = { 'subject': 'Your task is under review!', 'message': ('Thanks for all your hard work, {}! The following ' 'task was randomly selected for review by another ' 'expert; you should hear back soon!').format( current_worker.user.username), 'recipient_list': [current_worker.user.email] } # Notify worker when assignment rejected elif task.status == Task.Status.POST_REVIEW_PROCESSING: message_info = { 'subject': 'Your task has been returned', 'message': ('Your reviewer sent back your task for a bit more ' 'polish. Check out the feedback as soon as you can!'), 'recipient_list': [current_worker.user.email] } # Notify all workers on a task when it has been completed elif task.status == Task.Status.COMPLETE: message_info = { 'subject': 'Task complete!', 'message': 'Congratulations! The task you worked on is complete.', 'recipient_list': [assignment.worker.user.email for assignment in task_assignments if assignment.worker and assignment.worker.user.email] } # Notify reviewer when task pending update is ready for re-review, but not # for a task moving from PENDING_REVIEW to REVIEWING elif (task.status == Task.Status.REVIEWING and previous_status == Task.Status.POST_REVIEW_PROCESSING): message_info = { 'subject': 'A task is ready for re-review!', 'message': ('A task has been updated and is ready for ' 're-review!'), 'recipient_list': [current_worker.user.email] } # Notify all workers on a task when it has been aborted elif task.status == Task.Status.ABORTED: message_info = { 'subject': 'A task you were working on has been ended', 'message': ('Unfortunately, the task you were working on has ' 'been ended. Please reach out to us if you think this ' 'has been done in error.'), 'recipient_list': [assignment.worker.user.email for assignment in task_assignments if assignment.worker and assignment.worker.user.email] } _notify_internal_slack_status_change(task, current_worker) if task.project.slack_group_id: _notify_experts_slack_status_change(task, current_worker) if message_info is not None: message_info['message'] += _task_information(task) comm_type = (CommunicationPreference.CommunicationType .TASK_STATUS_CHANGE.value) send_mail(from_email=settings.ORCHESTRA_NOTIFICATIONS_FROM_EMAIL, communication_type=comm_type, fail_silently=True, **message_info)
def test_assign_task(self): entry_task = TaskFactory(project=self.projects['base_test_project'], status=Task.Status.AWAITING_PROCESSING, step=self.test_step) # No iterations should be present for task self.assertEqual( Iteration.objects.filter(assignment__task=entry_task).count(), 0) # Assign entry-level task to entry-level worker entry_task = assign_task(self.workers[0].id, entry_task.id) self.assertTrue(entry_task.is_worker_assigned(self.workers[0])) self.assertEqual(entry_task.status, Task.Status.PROCESSING) self.assertEqual(entry_task.assignments.count(), 1) entry_assignment = entry_task.assignments.first() # A single iteration was created for the assignment self.assertEqual(entry_assignment.iterations.count(), 1) self.assertEqual( Iteration.objects.filter(assignment__task=entry_task).count(), 1) self.assertEqual(entry_assignment.iterations.first().start_datetime, entry_assignment.start_datetime) # 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) 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) reviewer_assignment = current_assignment(review_task) self.assertEqual(reviewer_assignment.worker, self.workers[3]) self.assertEqual(reviewer_assignment.in_progress_task_data, test_data) self.assertEquals(reviewer_assignment.iterations.count(), 1) self.assertEqual(reviewer_assignment.iterations.first().start_datetime, reviewer_assignment.start_datetime) 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 notify_status_change(task, previous_status=None): """ Notify workers after task has changed state """ task_assignments = assignment_history(task) current_task_assignment = current_assignment(task) current_worker = None if current_task_assignment: current_worker = current_task_assignment.worker message_info = None # Notify worker when task initially picked up if task.status == Task.Status.PROCESSING: message_info = { 'subject': "You've been assigned to a new task!", 'message': ("You've been assigned to a new task. We can't wait " "to see the great things you'll do!"), 'recipient_list': [current_worker.user.email] } # Notify worker when assignment selected for review elif task.status == Task.Status.PENDING_REVIEW: message_info = { 'subject': 'Your task is under review!', 'message': ('Thanks for all your hard work, {}! The following ' 'task was randomly selected for review by another ' 'expert; you should hear back soon!').format( current_worker.user.username), 'recipient_list': [current_worker.user.email] } # Notify worker when assignment rejected elif task.status == Task.Status.POST_REVIEW_PROCESSING: message_info = { 'subject': 'Your task has been returned', 'message': ('Your reviewer sent back your task for a bit more ' 'polish. Check out the feedback as soon as you can!'), 'recipient_list': [current_worker.user.email] } # Notify all workers on a task when it has been completed elif task.status == Task.Status.COMPLETE: message_info = { 'subject': 'Task complete!', 'message': 'Congratulations! The task you worked on is complete.', 'recipient_list': [ assignment.worker.user.email for assignment in task_assignments if assignment.worker and assignment.worker.user.email ] } # Notify reviewer when task pending update is ready for re-review, but not # for a task moving from PENDING_REVIEW to REVIEWING elif (task.status == Task.Status.REVIEWING and previous_status == Task.Status.POST_REVIEW_PROCESSING): message_info = { 'subject': 'A task is ready for re-review!', 'message': ('A task has been updated and is ready for ' 're-review!'), 'recipient_list': [current_worker.user.email] } # Notify all workers on a task when it has been aborted elif task.status == Task.Status.ABORTED: message_info = { 'subject': 'A task you were working on has been ended', 'message': ('Unfortunately, the task you were working on has ' 'been ended. Please reach out to us if you think this ' 'has been done in error.'), 'recipient_list': [ assignment.worker.user.email for assignment in task_assignments if assignment.worker and assignment.worker.user.email ] } _notify_internal_slack_status_change(task, current_worker) if task.project.slack_group_id: _notify_experts_slack_status_change(task, current_worker) if message_info is not None: message_info['message'] += _task_information(task) comm_type = ( CommunicationPreference.CommunicationType.TASK_STATUS_CHANGE.value) send_mail(from_email=settings.ORCHESTRA_NOTIFICATIONS_FROM_EMAIL, communication_type=comm_type, fail_silently=True, **message_info)
def _setup_tasks(test_case, tasks): # Create and assign tasks test_case.tasks = {} test_case.test_step = test_case.workflow_steps[ test_case.test_version_slug][test_case.test_step_slug] for task_slug, details in tasks.items(): task_pickup_time = BASE_DATETIME + timedelta(hours=1) task = TaskFactory( project=test_case.projects[details['project_name']], step=test_case.test_step, status=details['status'], start_datetime=task_pickup_time, ) test_case.tasks[task_slug] = task for i, (user_id, task_data, assignment_status) in enumerate(details['assignments']): assignment = TaskAssignmentFactory( worker=test_case.workers[user_id], task=task, status=assignment_status, assignment_counter=i, in_progress_task_data=task_data, start_datetime=_new_assignment_start_datetime(task)) # Each assignment must have at least one corresponding iteration Iteration.objects.create( assignment=assignment, start_datetime=assignment.start_datetime, end_datetime=assignment.start_datetime + ITERATION_DURATION, submitted_data=assignment.in_progress_task_data, status=Iteration.Status.REQUESTED_REVIEW) # Create time entry for each task. TimeEntryFactory(date='2016-04-04', time_worked=timedelta(minutes=30), assignment=assignment, worker=test_case.workers[user_id], description=( 'test description {}'.format(assignment.id))) cur_assignment = current_assignment(task) assignments = assignment_history(task).all() if cur_assignment and ( cur_assignment.status == TaskAssignment.Status.PROCESSING): # If there's a currently processing assignment, we'll need to # adjust the task's iteration sequence processing_counter = cur_assignment.assignment_counter if processing_counter != len(assignments) - 1: # If processing assignment is not the last in the hierarchy, we # need to reconstruct an iteration sequence: REQUESTED_REVIEW # up to the highest assignment counter, then PROVIDED_REVIEW # back down to the current assignment last_iteration = assignments.last().iterations.first() last_iteration.status = Iteration.Status.PROVIDED_REVIEW last_iteration.save() adjust_assignments = list(assignments)[processing_counter:-1] for assignment in reversed(adjust_assignments): last_iteration = get_iteration_history(task).last() Iteration.objects.create( assignment=assignment, start_datetime=last_iteration.end_datetime, end_datetime=( last_iteration.end_datetime + ITERATION_DURATION), submitted_data=assignment.in_progress_task_data, status=Iteration.Status.PROVIDED_REVIEW) # If there is a currently processing assignment, the task's last # iteration should still be processing last_iteration = get_iteration_history(task).last() last_iteration.end_datetime = None last_iteration.submitted_data = {} last_iteration.status = Iteration.Status.PROCESSING last_iteration.save() verify_iterations(task.id)
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)