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 get_previously_completed_task_data(step, project): """ Returns a dict mapping task prerequisites onto their latest task assignment information. The dict is of the form: {'previous-slug': {latest_assignment_data}, ...} Args: step (orchestra.models.Step): The specified step object. project (orchestra.models.Project): The specified project object. Returns: prerequisites (dict): A dict mapping task prerequisites onto their latest task assignment information. """ # Find all prerequisite steps in graph to_visit = list(step.creation_depends_on.all()) prerequisite_steps = set(to_visit) while to_visit: current_step = to_visit.pop() for step in current_step.creation_depends_on.all(): if step not in prerequisite_steps: to_visit.append(step) prerequisite_steps.add(step) prerequisite_data = {} for step in prerequisite_steps: required_task = Task.objects.get(step=step, project=project) if required_task.status != Task.Status.COMPLETE: raise TaskDependencyError('Task depenency is not satisfied') assignment = assignment_history(required_task).last() prerequisite_data[step.slug] = assignment.in_progress_task_data return prerequisite_data
def previously_completed_task_data(task): """ 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 (orchestra.models.Task): The specified task object. Returns: prerequisites (dict): A dict mapping task prerequisites onto their latest task assignment information. """ step = task.step prerequisites = {} for required_step in step.creation_depends_on.all(): required_task = Task.objects.get(step=required_step, project=task.project) if required_task.status != Task.Status.COMPLETE: raise TaskDependencyError('Task depenency is not satisfied') task_details = get_task_details(required_task.id) assignment = assignment_history(required_task).last() task_assignment_details = {} if assignment: # Task assignment should be present unless task was skipped. task_assignment_details = get_task_assignment_details(assignment) task_assignment_details.update(task_details) # TODO(kkamalov): check for circular prerequisites prerequisites[required_step.slug] = task_assignment_details return prerequisites
def previously_completed_task_data(task): """ Returns a dict mapping task prerequisites onto their latest task assignment information. The dict is of the form: {'previous-slug': {latest_assignment_data}, ...} Args: task (orchestra.models.Task): The specified task object. Returns: prerequisites (dict): A dict mapping task prerequisites onto their latest task assignment information. """ # Find all prerequisite steps in graph to_visit = list(task.step.creation_depends_on.all()) prerequisite_steps = set(to_visit) while to_visit: current_step = to_visit.pop() for step in current_step.creation_depends_on.all(): if step not in prerequisite_steps: to_visit.append(step) prerequisite_steps.add(step) prerequisite_data = {} for step in prerequisite_steps: required_task = Task.objects.get(step=step, project=task.project) if required_task.status != Task.Status.COMPLETE: raise TaskDependencyError('Task depenency is not satisfied') assignment = assignment_history(required_task).last() prerequisite_data[step.slug] = assignment.in_progress_task_data return prerequisite_data
def _expected_audit(self, complete_task, reverted_status=None, assignment_changes=None, iteration_changes=None): assignments = assignment_history(complete_task) iterations = get_iteration_history(complete_task) audit = { 'task': TaskSerializer(complete_task).data, 'assignments': [{ 'assignment': (TaskAssignmentSerializer(assignments.first()).data), 'change': 0, 'iterations': [{ 'change': 0, 'iteration': (IterationSerializer(iterations.all()[0]).data) }, { 'change': 0, 'iteration': (IterationSerializer(iterations.all()[2]).data) }] }, { 'assignment': (TaskAssignmentSerializer(assignments.last()).data), 'change': 0, 'iterations': [{ 'change': 0, 'iteration': (IterationSerializer(iterations.all()[1]).data) }, { 'change': 0, 'iteration': (IterationSerializer(iterations.all()[3]).data) }] }], } if reverted_status is not None: audit['reverted_status'] = reverted_status if assignment_changes is not None: for i, assignment_change in enumerate(assignment_changes): audit['assignments'][i]['change'] = assignment_change if iteration_changes is not None: for i, changes_per_assignment in enumerate(iteration_changes): for j, iteration_change in enumerate(changes_per_assignment): audit['assignments'][i]['iterations'][j][ 'change'] = iteration_change return audit
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 _expected_audit(self, complete_task, reverted_status=None, assignment_changes=None, iteration_changes=None): assignments = assignment_history(complete_task) iterations = get_iteration_history(complete_task) audit = { 'task': TaskSerializer(complete_task).data, 'assignments': [ { 'assignment': ( TaskAssignmentSerializer(assignments.first()).data), 'change': 0, 'iterations': [ { 'change': 0, 'iteration': ( IterationSerializer(iterations.all()[0]).data) }, { 'change': 0, 'iteration': ( IterationSerializer(iterations.all()[2]).data) } ] }, { 'assignment': ( TaskAssignmentSerializer(assignments.last()).data), 'change': 0, 'iterations': [ { 'change': 0, 'iteration': ( IterationSerializer(iterations.all()[1]).data) }, { 'change': 0, 'iteration': ( IterationSerializer(iterations.all()[3]).data) } ] } ], } if reverted_status is not None: audit['reverted_status'] = reverted_status if assignment_changes is not None: for i, assignment_change in enumerate(assignment_changes): audit['assignments'][i]['change'] = assignment_change if iteration_changes is not None: for i, changes_per_assignment in enumerate(iteration_changes): for j, iteration_change in enumerate(changes_per_assignment): audit['assignments'][i][ 'iterations'][j]['change'] = iteration_change return audit
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 setup_task_history(test): task = test.tasks['rejected_review'] test._submit_assignment( test.clients[6], task.id, seconds=35) test._submit_assignment( test.clients[7], task.id, command='reject', seconds=36) test._submit_assignment( test.clients[6], task.id, seconds=37) test._submit_assignment( test.clients[7], task.id, command='accept', seconds=38) # Fill out the snapshots for all assignments assignments = assignment_history(task) first_assignment = assignments[0] second_assignment = assignments[1] third_assignment = assignments[2] first_assignment.snapshots['snapshots'] = deepcopy( second_assignment.snapshots['snapshots']) second_assignment.snapshots['snapshots'] = ( deepcopy(third_assignment.snapshots['snapshots']) + second_assignment.snapshots['snapshots'])[:-1] def fix_datetimes(snapshots, new_datetimes): for snapshot, new_datetime in zip(snapshots, new_datetimes): snapshot['datetime'] = new_datetime # Explicitly set the iteration datetimes. If we didn't, the timestamps # would be `datetime.now`, which we can't test against. The explicitly set # times are predictable distance apart, so we can test the # resulting latency reports. fix_datetimes( first_assignment.snapshots['snapshots'], ['2015-10-12T02:02:00+00:00', '2015-10-12T03:05:00+00:00']) fix_datetimes( second_assignment.snapshots['snapshots'], ['2015-10-12T03:01:00+00:00', '2015-10-12T03:07:00+00:00', '2015-10-12T04:03:00+00:00', '2015-10-12T04:10:00+00:00']) fix_datetimes( third_assignment.snapshots['snapshots'], ['2015-10-12T04:02:00+00:00', '2015-10-12T04:13:00+00:00']) first_assignment.save() second_assignment.save() third_assignment.save() return task
def setup_task_history(test): task = test.tasks['rejected_review'] test._submit_assignment(test.clients[6], task.id, seconds=35) test._submit_assignment(test.clients[7], task.id, command='reject', seconds=36) test._submit_assignment(test.clients[6], task.id, seconds=37) test._submit_assignment(test.clients[7], task.id, command='accept', seconds=38) # Fill out the snapshots for all assignments assignments = assignment_history(task) first_assignment = assignments[0] second_assignment = assignments[1] third_assignment = assignments[2] first_assignment.snapshots['snapshots'] = deepcopy( second_assignment.snapshots['snapshots']) second_assignment.snapshots['snapshots'] = ( deepcopy(third_assignment.snapshots['snapshots']) + second_assignment.snapshots['snapshots'])[:-1] def fix_datetimes(snapshots, new_datetimes): for snapshot, new_datetime in zip(snapshots, new_datetimes): snapshot['datetime'] = new_datetime # Explicitly set the iteration datetimes. If we didn't, the timestamps # would be `datetime.now`, which we can't test against. The explicitly set # times are predictable distance apart, so we can test the # resulting latency reports. fix_datetimes(first_assignment.snapshots['snapshots'], ['2015-10-12T02:02:00+00:00', '2015-10-12T03:05:00+00:00']) fix_datetimes(second_assignment.snapshots['snapshots'], [ '2015-10-12T03:01:00+00:00', '2015-10-12T03:07:00+00:00', '2015-10-12T04:03:00+00:00', '2015-10-12T04:10:00+00:00' ]) fix_datetimes(third_assignment.snapshots['snapshots'], ['2015-10-12T04:02:00+00:00', '2015-10-12T04:13:00+00:00']) first_assignment.save() second_assignment.save() third_assignment.save() return task
def previously_completed_steps(task, related_steps, **kwargs): """ Assign a new task to the entry-level worker of the specified tasks. If no worker can be assigned, return the unmodified task. Args: task (orchestra.models.Task): The newly created task to assign. related_steps ([str]): List of step slugs from which to attempt to assign a worker. Returns: task (orchestra.models.Task): The modified task object. Raises: orchestra.core.errors.AssignmentPolicyError: Machine steps cannot be included in an assignment policy. """ if related_steps is None: raise AssignmentPolicyError('No related steps given') workflow_version = task.step.workflow_version for step_slug in related_steps: step = workflow_version.steps.get(slug=step_slug) if not step.is_human: raise AssignmentPolicyError('Machine step should not be ' 'member of assignment policy') related_tasks = (Task.objects.filter( step__slug__in=related_steps, project=task.project).select_related('step')) for related_task in related_tasks: entry_level_assignment = assignment_history(related_task).first() if entry_level_assignment and entry_level_assignment.worker: try: return assign_task(entry_level_assignment.worker.id, task.id) except WorkerCertificationError: # Task could not be assigned to related worker, try with # another related worker logger.warning( 'Tried to assign worker %s to step %s, for ' 'which they are not certified', entry_level_assignment.worker.id, task.step.slug, exc_info=True) except Exception: logger.warning('Unable to assign task.', exc_info=True) return task
def test_invalid_revert_before(self): task = setup_complete_task(self) task.status = Task.Status.ABORTED task.save() # Ensure reviewer assignment has more than one iteration reviewer_assignment = assignment_history(task).last() self.assertGreater(reviewer_assignment.iterations.count(), 1) # Attempt to revert before an iteration that isn't the assignment's # first with self.assertRaises(InvalidRevertError): self._revert_task( task, get_latest_iteration(reviewer_assignment), revert_before=True, commit=True) task.delete()
def _build_revert_audit(task, revert_iteration, revert_before): if task.status == Task.Status.ABORTED: raise InvalidRevertError('Cannot revert aborted task.') task_audit = { 'task': TaskSerializer(task).data, 'assignments': [], } for assignment in assignment_history(task).all(): assignment_audit = (_build_assignment_revert_audit( assignment, revert_iteration, revert_before)) task_audit['assignments'].append(assignment_audit) task_audit['reverted_status'] = _reverted_task_status( task_audit, revert_before) return task_audit
def end_project(project_id): """ Mark the specified project and its component tasks as aborted. Args: project_id (int): The ID of the project to abort. Returns: None """ project = Project.objects.get(id=project_id) project.status = Project.Status.ABORTED project.save() for task in project.tasks.all(): task.status = Task.Status.ABORTED task.save() notify_status_change(task, assignment_history(task))
def previously_completed_steps(task, related_steps, **kwargs): """ Assign a new task to the entry-level worker of the specified tasks. If no worker can be assigned, return the unmodified task. Args: task (orchestra.models.Task): The newly created task to assign. related_steps ([str]): List of step slugs from which to attempt to assign a worker. Returns: task (orchestra.models.Task): The modified task object. Raises: orchestra.core.errors.AssignmentPolicyError: Machine steps cannot be included in an assignment policy. """ if related_steps is None: raise AssignmentPolicyError('No related steps given') workflow_version = task.step.workflow_version for step_slug in related_steps: step = workflow_version.steps.get(slug=step_slug) if not step.is_human: raise AssignmentPolicyError('Machine step should not be ' 'member of assignment policy') related_tasks = ( Task.objects .filter(step__slug__in=related_steps, project=task.project) .select_related('step')) for related_task in related_tasks: entry_level_assignment = assignment_history(related_task).first() if entry_level_assignment and entry_level_assignment.worker: try: return assign_task(entry_level_assignment.worker.id, task.id) except WorkerCertificationError: # Task could not be assigned to related worker, try with # another related worker logger.warning('Tried to assign worker %s to step %s, for ' 'which they are not certified', entry_level_assignment.worker.id, task.step.slug, exc_info=True) except Exception: logger.warning('Unable to assign task.', exc_info=True) return task
def _build_revert_audit(task, revert_iteration, revert_before): if task.status == Task.Status.ABORTED: raise InvalidRevertError('Cannot revert aborted task.') task_audit = { 'task': TaskSerializer(task).data, 'assignments': [], } for assignment in assignment_history(task).all(): assignment_audit = ( _build_assignment_revert_audit( assignment, revert_iteration, revert_before)) task_audit['assignments'].append(assignment_audit) task_audit['reverted_status'] = _reverted_task_status( task_audit, revert_before) return task_audit
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_worker_from_previously_completed_steps(task, related_steps): """ Assign a new task to the entry-level worker of the specified tasks. If no worker can be assigned, return the unmodified task. Args: task (orchestra.models.Task): The newly created task to assign. related_steps ([orchestra.workflow.steps]): List of steps from which to attempt to assign a worker. Returns: task (orchestra.models.Task): The modified task object. Raises: orchestra.core.errors.AssignmentPolicyError: Machine steps cannot be included in an assignment policy. """ workflow = get_workflow_by_slug(task.project.workflow_slug) for slug in related_steps: if workflow.get_step(slug).worker_type == Step.WorkerType.MACHINE: raise AssignmentPolicyError('Machine step should not be ' 'member of assignment policy') related_tasks = Task.objects.filter(step_slug__in=related_steps, project=task.project) for related_task in related_tasks: entry_level_assignment = assignment_history(related_task).first() if entry_level_assignment and entry_level_assignment.worker: try: return assign_task(entry_level_assignment.worker.id, task.id) except: # Task could not be assigned to related worker, try with # another related worker logger.warning('Tried to assign worker %s to step %s, for ' 'which they are not certified', entry_level_assignment.worker.id, task.step_slug, exc_info=True) return task
def setup_complete_task(test_case): # Microseconds are truncated when manually saving models test_start = timezone.now().replace(microsecond=0) times = { 'awaiting_pickup': test_start, 'entry_pickup': test_start + timedelta(hours=1), 'entry_submit': test_start + timedelta(hours=2), 'reviewer_pickup': test_start + timedelta(hours=3), 'reviewer_reject': test_start + timedelta(hours=4), 'entry_resubmit': test_start + timedelta(hours=5), 'reviewer_accept': test_start + timedelta(hours=6), } task = TaskFactory( project=test_case.projects['empty_project'], status=Task.Status.AWAITING_PROCESSING, step=test_case.test_step, start_datetime=times['awaiting_pickup']) workers = { 'entry': test_case.workers[0], 'reviewer': test_case.workers[1] } assign_task(workers['entry'].id, task.id) task.refresh_from_db() test_case.assertEqual(task.status, Task.Status.PROCESSING) submit_task( task.id, {'test': 'entry_submit'}, Iteration.Status.REQUESTED_REVIEW, workers['entry']) task.refresh_from_db() test_case.assertEqual(task.status, Task.Status.PENDING_REVIEW) assign_task(workers['reviewer'].id, task.id) reviewer_assignment = task.assignments.get( worker=workers['reviewer']) # Modify assignment with correct datetime reviewer_assignment.start_datetime = times['reviewer_pickup'] reviewer_assignment.save() task.refresh_from_db() test_case.assertEqual(task.status, Task.Status.REVIEWING) submit_task( task.id, {'test': 'reviewer_reject'}, Iteration.Status.PROVIDED_REVIEW, workers['reviewer']) task.refresh_from_db() test_case.assertEqual(task.status, Task.Status.POST_REVIEW_PROCESSING) submit_task( task.id, {'test': 'entry_resubmit'}, Iteration.Status.REQUESTED_REVIEW, workers['entry']) task.refresh_from_db() test_case.assertEqual(task.status, Task.Status.REVIEWING) with patch('orchestra.utils.task_lifecycle._is_review_needed', return_value=False): submit_task( task.id, {'test': 'reviewer_accept'}, Iteration.Status.REQUESTED_REVIEW, workers['reviewer']) task.refresh_from_db() test_case.assertEqual(task.status, Task.Status.COMPLETE) test_case.assertEqual(task.assignments.count(), 2) for assignment in task.assignments.all(): test_case.assertEqual( assignment.status, TaskAssignment.Status.SUBMITTED) test_case.assertEqual(assignment.iterations.count(), 2) # Modify assignments with correct datetime new_datetime_labels = ('entry_pickup', 'reviewer_pickup') for i, assignment in enumerate(assignment_history(task).all()): assignment.start_datetime = times[new_datetime_labels[i]] assignment.save() # Modify iterations with correct datetime new_datetime_labels = ( ('entry_pickup', 'entry_submit'), ('reviewer_pickup', 'reviewer_reject'), ('reviewer_reject', 'entry_resubmit'), ('entry_resubmit', 'reviewer_accept') ) new_datetimes = [ (times[start_label], times[end_label]) for start_label, end_label in new_datetime_labels] for i, iteration in enumerate(get_iteration_history(task)): iteration.start_datetime, iteration.end_datetime = new_datetimes[i] iteration.save() verify_iterations(task.id) return task
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 _revert_task_status(task): """ Reverts the status of an otherwise-reverted task. Args: task (orchestra.models.Task): The task with status to revert. Returns: task (orchestra.models.Task): The task with reverted status. """ assignments = assignment_history(task) num_assignments = assignments.count() reverted_status = None current_assignment_counter = None previous_assignment_counter = None if num_assignments == 0: # No assignment is present reverted_status = Task.Status.AWAITING_PROCESSING else: assignment = last_snapshotted_assignment(task.id) if not assignment: # Task has an assignment but no snapshots reverted_status = Task.Status.PROCESSING current_assignment_counter = 0 else: latest_counter = assignment.assignment_counter snapshot = assignment.snapshots['snapshots'][-1] if snapshot['type'] == TaskAssignment.SnapshotType.REJECT: # Task was last rejected back to previous worker reverted_status = Task.Status.POST_REVIEW_PROCESSING current_assignment_counter = latest_counter - 1 previous_assignment_counter = latest_counter elif (snapshot['type'] in (TaskAssignment.SnapshotType.SUBMIT, TaskAssignment.SnapshotType.ACCEPT)): if latest_counter == num_assignments - 1: # Task was last submitted and no higher-level # assignments are present (reverted tasks will never end # in a completed state) reverted_status = Task.Status.PENDING_REVIEW else: reverted_status = Task.Status.REVIEWING previous_assignment_counter = latest_counter current_assignment_counter = latest_counter + 1 if current_assignment_counter is not None: current_assignment = task.assignments.get( assignment_counter=current_assignment_counter) current_assignment.status = TaskAssignment.Status.PROCESSING current_assignment.save() if previous_assignment_counter is not None: previous_assignment = task.assignments.get( assignment_counter=previous_assignment_counter) previous_assignment.status = TaskAssignment.Status.SUBMITTED previous_assignment.save() # TODO(jrbotros): The revert methos should "peel off" snapshots, # rather than deleting them in bulk and recalculating the assignment # and task statuses; this logic needs to be cleaned up. for assignment in task.assignments.all(): if assignment.status == TaskAssignment.Status.SUBMITTED: latest_snapshot = assignment.snapshots['snapshots'][-1] assignment.in_progress_task_data = latest_snapshot['data'] assignment.save() task.status = reverted_status task.save() 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()
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 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_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 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 revert_task_to_datetime(task_id, revert_datetime, fake=False): """ Reverts a task to its state immediately before the specified datetime. Args: task_id (int): The ID of the task to be reverted. revert_datetime (datetime.datetime): The datetime before which to revert the task. fake (bool): Determines whether the revert is actually carried out; otherwise, an audit trail is passively generated. Returns: audit (dict): An audit trail for the revert, e.g., { 'task': {...}, 'change': 'reverted' 'assignments': [ { # worker_0 assignment 'assignment': {...}, 'change': 'reverted', 'snapshots': [ { 'snapshot': {...}, 'change': 'unchanged' } { 'snapshot': {...}, 'change': 'deleted' }, ] }, { # worker_1 assignment 'assignment': {...}, 'change': 'deleted', 'snapshots': [ { 'snapshot': {...}, 'change': 'deleted' }, { 'snapshot': {...}, 'change': 'deleted' } ] } ], } """ task = Task.objects.get(id=task_id) audit = { 'task': TaskSerializer(task).data, 'change': 'unchanged', 'assignments': [] } reverted = False for assignment in assignment_history(task).all(): assignment_audit = _revert_assignment_to_datetime( assignment, revert_datetime, fake) audit['assignments'].append(assignment_audit) if assignment_audit['change'] != 'unchanged': reverted = True # Delete task if it starts after revert_datetime if task.start_datetime >= revert_datetime: audit['change'] = 'deleted' if not fake: task.delete() elif reverted: audit['change'] = 'reverted' if not fake: _revert_task_status(task) return audit
def test_project_information_api(self): project = self.projects['project_management_project'] response = self.api_client.post( reverse( 'orchestra:orchestra:project_management:' 'project_information'), json.dumps({ 'project_id': project.id, }), content_type='application/json') self.assertEquals(response.status_code, 200) returned = load_encoded_json(response.content) # Skipping the `tasks` key/value pair for this test expected_project = { 'task_class': project.task_class, 'start_datetime': '2015-10-12T00:00:00Z', 'team_messages_url': None, 'admin_url': settings.ORCHESTRA_URL + reverse( 'admin:orchestra_project_change', args=(project.id,)), 'priority': project.priority, 'project_data': project.project_data, 'short_description': project.short_description, 'id': project.id, 'workflow_slug': project.workflow_version.workflow.slug } self.assertEquals( expected_project, {k: returned['project'][k] for k in expected_project.keys()}) sample_task = returned['tasks']['step1'] task = project.tasks.get(step__slug='step1') expected_task = { 'status': 'Post-review Processing', 'start_datetime': '2015-10-12T01:00:00Z', 'admin_url': settings.ORCHESTRA_URL + reverse( 'admin:orchestra_task_change', args=(task.id,)), 'latest_data': { 'test_key': 'test_value' }, 'project': task.project.id, 'step_slug': 'step1', 'id': task.id } self.assertEquals( expected_task, {k: sample_task[k] for k in expected_task.keys()}) sample_assignment = sample_task['assignments'][0] assignment = assignment_history(task)[0] expected_assignment = { 'status': 'Submitted', 'start_datetime': '2015-10-12T02:00:00Z', 'task': assignment.task.id, 'admin_url': settings.ORCHESTRA_URL + reverse( 'admin:orchestra_taskassignment_change', args=(assignment.id,)), 'worker': { 'username': assignment.worker.user.username, 'first_name': assignment.worker.user.first_name, 'last_name': assignment.worker.user.last_name, 'id': assignment.worker.id, }, 'iterations': [ { 'id': assignment.iterations.first().id, 'admin_url': settings.ORCHESTRA_URL + reverse( 'admin:orchestra_iteration_change', args=(assignment.iterations.first().id,)), 'assignment': assignment.id, 'start_datetime': '2015-10-12T02:00:00Z', 'end_datetime': '2015-10-12T03:00:00Z', 'status': 'Requested Review', 'submitted_data': {} } ], 'in_progress_task_data': {}, 'id': assignment.id } self.assertEquals( expected_assignment, {k: sample_assignment[k] for k in expected_assignment.keys()})
def test_project_information_api(self): project = self.projects['project_management_project'] response = self.api_client.post(reverse( 'orchestra:orchestra:project_management:' 'project_information'), json.dumps({ 'project_id': project.id, }), content_type='application/json') self.assertEquals(response.status_code, 200) returned = load_encoded_json(response.content) # Skipping the `tasks` key/value pair for this test expected_project = { 'task_class': project.task_class, 'start_datetime': '2015-10-12T00:00:00Z', 'team_messages_url': None, 'admin_url': settings.ORCHESTRA_URL + reverse('admin:orchestra_project_change', args=(project.id, )), 'priority': project.priority, 'project_data': project.project_data, 'short_description': project.short_description, 'id': project.id, 'workflow_slug': project.workflow_version.workflow.slug } self.assertEquals( expected_project, {k: returned['project'][k] for k in expected_project.keys()}) sample_task = returned['tasks']['step1'] task = project.tasks.get(step__slug='step1') expected_task = { 'status': 'Post-review Processing', 'start_datetime': '2015-10-12T01:00:00Z', 'admin_url': settings.ORCHESTRA_URL + reverse('admin:orchestra_task_change', args=(task.id, )), 'latest_data': { 'test_key': 'test_value' }, 'project': task.project.id, 'step_slug': 'step1', 'id': task.id } self.assertEquals(expected_task, {k: sample_task[k] for k in expected_task.keys()}) sample_assignment = sample_task['assignments'][0] assignment = assignment_history(task)[0] expected_assignment = { 'status': 'Submitted', 'start_datetime': '2015-10-12T02:00:00Z', 'task': assignment.task.id, 'admin_url': settings.ORCHESTRA_URL + reverse('admin:orchestra_taskassignment_change', args=(assignment.id, )), 'worker': { 'username': assignment.worker.user.username, 'first_name': assignment.worker.user.first_name, 'last_name': assignment.worker.user.last_name, 'id': assignment.worker.id, }, 'iterations': [{ 'id': assignment.iterations.first().id, 'admin_url': settings.ORCHESTRA_URL + reverse('admin:orchestra_iteration_change', args=(assignment.iterations.first().id, )), 'assignment': assignment.id, 'start_datetime': '2015-10-12T02:00:00Z', 'end_datetime': '2015-10-12T03:00:00Z', 'status': 'Requested Review', 'submitted_data': {} }], 'in_progress_task_data': {}, 'id': assignment.id } self.assertEquals( expected_assignment, {k: sample_assignment[k] for k in expected_assignment.keys()})
def setup_complete_task(test_case): # Microseconds are truncated when manually saving models test_start = timezone.now().replace(microsecond=0) times = { 'awaiting_pickup': test_start, 'entry_pickup': test_start + timedelta(hours=1), 'entry_submit': test_start + timedelta(hours=2), 'reviewer_pickup': test_start + timedelta(hours=3), 'reviewer_reject': test_start + timedelta(hours=4), 'entry_resubmit': test_start + timedelta(hours=5), 'reviewer_accept': test_start + timedelta(hours=6), } task = TaskFactory( project=test_case.projects['empty_project'], status=Task.Status.AWAITING_PROCESSING, step=test_case.test_step, start_datetime=times['awaiting_pickup']) workers = { 'entry': test_case.workers[0], 'reviewer': test_case.workers[1] } assign_task(workers['entry'].id, task.id) task.refresh_from_db() test_case.assertEquals(task.status, Task.Status.PROCESSING) submit_task( task.id, {'test': 'entry_submit'}, Iteration.Status.REQUESTED_REVIEW, workers['entry']) task.refresh_from_db() test_case.assertEquals(task.status, Task.Status.PENDING_REVIEW) assign_task(workers['reviewer'].id, task.id) reviewer_assignment = task.assignments.get( worker=workers['reviewer']) # Modify assignment with correct datetime reviewer_assignment.start_datetime = times['reviewer_pickup'] reviewer_assignment.save() task.refresh_from_db() test_case.assertEquals(task.status, Task.Status.REVIEWING) submit_task( task.id, {'test': 'reviewer_reject'}, Iteration.Status.PROVIDED_REVIEW, workers['reviewer']) task.refresh_from_db() test_case.assertEquals(task.status, Task.Status.POST_REVIEW_PROCESSING) submit_task( task.id, {'test': 'entry_resubmit'}, Iteration.Status.REQUESTED_REVIEW, workers['entry']) task.refresh_from_db() test_case.assertEquals(task.status, Task.Status.REVIEWING) with patch('orchestra.utils.task_lifecycle._is_review_needed', return_value=False): submit_task( task.id, {'test': 'reviewer_accept'}, Iteration.Status.REQUESTED_REVIEW, workers['reviewer']) task.refresh_from_db() test_case.assertEquals(task.status, Task.Status.COMPLETE) test_case.assertEquals(task.assignments.count(), 2) for assignment in task.assignments.all(): test_case.assertEquals( assignment.status, TaskAssignment.Status.SUBMITTED) test_case.assertEquals(assignment.iterations.count(), 2) # Modify assignments with correct datetime new_datetime_labels = ('entry_pickup', 'reviewer_pickup') for i, assignment in enumerate(assignment_history(task).all()): assignment.start_datetime = times[new_datetime_labels[i]] assignment.save() # Modify iterations with correct datetime new_datetime_labels = ( ('entry_pickup', 'entry_submit'), ('reviewer_pickup', 'reviewer_reject'), ('reviewer_reject', 'entry_resubmit'), ('entry_resubmit', 'reviewer_accept') ) new_datetimes = [ (times[start_label], times[end_label]) for start_label, end_label in new_datetime_labels] for i, iteration in enumerate(get_iteration_history(task)): iteration.start_datetime, iteration.end_datetime = new_datetimes[i] iteration.save() verify_iterations(task.id) return task