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_task_assignment_saving(self): """ Ensure that workers are required for human tasks, and no workers are required for machine tasks. """ workflow_version = self.workflow_versions['test_workflow_2'] simple_machine = self.workflow_steps[ workflow_version.slug]['simple_machine'] project = Project.objects.create(workflow_version=workflow_version, short_description='', priority=0, task_class=0) task = Task.objects.create(project=project, status=Task.Status.PROCESSING, step=simple_machine) # We expect an error because a worker # is being saved on a machine task. with self.assertRaises(ModelSaveError): TaskAssignment.objects.create(worker=self.workers[0], task=task, status=0, in_progress_task_data={}, snapshots=empty_snapshots()) human_step = self.workflow_steps[workflow_version.slug]['step4'] task = Task.objects.create(project=project, status=Task.Status.PROCESSING, step=human_step) # We expect an error because no worker # is being saved on a human task with self.assertRaises(ModelSaveError): TaskAssignment.objects.create(task=task, status=0, in_progress_task_data={}, snapshots=empty_snapshots())
def test_task_assignment_saving(self): """ Ensure that workers are required for human tasks, and no workers are required for machine tasks. """ workflow_version = self.workflow_versions['test_workflow_2'] simple_machine = self.workflow_steps[ workflow_version.slug]['simple_machine'] project = Project.objects.create(workflow_version=workflow_version, short_description='', priority=0, task_class=0) task = Task.objects.create(project=project, status=Task.Status.PROCESSING, step=simple_machine) # We expect an error because a worker # is being saved on a machine task. with self.assertRaises(ModelSaveError): TaskAssignment.objects.create(worker=self.workers[0], task=task, status=0, in_progress_task_data={}, snapshots=empty_snapshots()) human_step = self.workflow_steps[workflow_version.slug]['step4'] task = Task.objects.create(project=project, status=Task.Status.PROCESSING, step=human_step) # We expect an error because no worker # is being saved on a human task with self.assertRaises(ModelSaveError): TaskAssignment.objects.create(task=task, status=0, in_progress_task_data={}, snapshots=empty_snapshots())
def test_project_information(self): project = self.projects['base_test_project'] response = self.api_client.post( '/orchestra/api/project/project_information/', {'project_id': project.id}, format='json') self.assertEquals(response.status_code, 200) returned = json.loads(response.content.decode('utf-8')) unimportant_keys = ( 'id', 'task', 'short_description', 'start_datetime', ) def delete_keys(obj): if isinstance(obj, list): for item in obj: delete_keys(item) elif isinstance(obj, dict): for key in unimportant_keys: try: del obj[key] except KeyError: pass for value in obj.values(): delete_keys(value) delete_keys(returned) del returned['tasks']['step1']['project'] expected = { 'project': { 'task_class': 1, 'workflow_slug': 'test_workflow', 'project_data': {}, 'review_document_url': None, 'priority': 0, }, 'tasks': { 'step1': { 'assignments': [{ 'snapshots': empty_snapshots(), 'status': 'Submitted', 'in_progress_task_data': {'test_key': 'test_value'}, 'worker': 'test_user_0', }], 'latest_data': { 'test_key': 'test_value' }, 'status': 'Pending Review', 'step_slug': 'step1', } }, 'steps': [ ['step1', 'The longer description of the first step'], ['step2', 'The longer description of the second step'], ['step3', 'The longer description of the third step'] ] } self.assertEquals(returned, expected) response = self.api_client.post( '/orchestra/api/project/project_information/', {'project_id': -1}, format='json') self.ensure_response(response, {'error': 400, 'message': 'No project for given id'}, 400) # Getting project info without a project_id should fail. response = self.api_client.post( '/orchestra/api/project/project_information/', {'projetc_id': project.id}, # Typo. format='json') self.ensure_response(response, {'error': 400, 'message': 'project_id is required'}, 400) # Retrieve the third project, which has no task assignments. response = self.api_client.post( '/orchestra/api/project/project_information/', {'project_id': self.projects['no_task_assignments'].id}, format='json') returned = json.loads(response.content.decode('utf-8')) del returned['tasks']['step1']['id'] del returned['tasks']['step1']['project'] self.assertEquals(response.status_code, 200) self.assertEquals(returned['tasks'], { 'step1': { 'assignments': [], 'latest_data': None, 'status': 'Awaiting Processing', 'step_slug': 'step1' } })
def assign_task(worker_id, task_id): """ Return a given task after assigning or reassigning it to the specified worker. Args: worker_id (int): The ID of the worker to be assigned. task_id (int): The ID of the task to be assigned. Returns: task (orchestra.models.Task): The newly assigned task. Raises: orchestra.core.errors.TaskAssignmentError: The specified worker is already assigned to the given task or the task status is not compatible with new assignment. orchestra.core.errors.WorkerCertificationError: The specified worker is not certified for the given task. """ worker = Worker.objects.get(id=worker_id) task = Task.objects.get(id=task_id) required_role, requires_reassign = _role_required_to_assign(task) assignment = current_assignment(task) if not _worker_certified_for_task(worker, task, required_role): raise WorkerCertificationError('Worker not certified for this task.') if is_worker_assigned_to_task(worker, task): raise TaskAssignmentError('Worker already assigned to this task.') # If task is currently in progress, reassign it if requires_reassign: assignment.worker = worker assignment.save() add_worker_to_project_team(worker, task.project) return task # Otherwise, create new assignment assignment_counter = task.assignments.count() in_progress_task_data = {} if required_role == WorkerCertification.Role.REVIEWER: # In-progress task data is the latest # submission by a previous worker in_progress_task_data = assignment.in_progress_task_data previous_status = task.status if previous_status == Task.Status.AWAITING_PROCESSING: task.status = Task.Status.PROCESSING elif previous_status == Task.Status.PENDING_REVIEW: task.status = Task.Status.REVIEWING else: raise TaskAssignmentError('Status incompatible with new assignment') task.save() (TaskAssignment.objects .create(worker=worker, task=task, status=TaskAssignment.Status.PROCESSING, assignment_counter=assignment_counter, in_progress_task_data=in_progress_task_data, snapshots=empty_snapshots())) add_worker_to_project_team(worker, task.project) notify_status_change(task, previous_status) return task
def test_project_information(self): project = self.projects['base_test_project'] response = self.api_client.post( '/orchestra/api/project/project_information/', {'project_id': project.id}, format='json') self.assertEquals(response.status_code, 200) returned = json.loads(response.content.decode('utf-8')) unimportant_keys = ( 'id', 'task', 'short_description', 'start_datetime', ) def delete_keys(obj): if isinstance(obj, list): for item in obj: delete_keys(item) elif isinstance(obj, dict): for key in unimportant_keys: try: del obj[key] except KeyError: pass for value in obj.values(): delete_keys(value) delete_keys(returned) del returned['tasks']['step1']['project'] expected = { 'project': { 'task_class': 1, 'workflow_slug': 'w1', 'workflow_version_slug': 'test_workflow', 'project_data': {}, 'team_messages_url': None, 'priority': 0, }, 'tasks': { 'step1': { 'assignments': [{ 'snapshots': empty_snapshots(), 'status': 'Submitted', 'in_progress_task_data': { 'test_key': 'test_value' }, 'worker': { 'username': self.workers[0].user.username, 'first_name': self.workers[0].user.first_name, 'last_name': self.workers[0].user.last_name, }, }], 'latest_data': { 'test_key': 'test_value' }, 'status': 'Pending Review', 'step_slug': 'step1', } }, 'steps': [{ 'slug': 'step1', 'description': 'The longer description of the first step', 'is_human': True }, { 'slug': 'step2', 'description': 'The longer description of the second step', 'is_human': True }, { 'slug': 'step3', 'description': 'The longer description of the third step', 'is_human': True }] } self.assertEquals(returned, expected) response = self.api_client.post( '/orchestra/api/project/project_information/', {'project_id': -1}, format='json') self.ensure_response(response, { 'error': 400, 'message': 'No project for given id' }, 400) # Getting project info without a project_id should fail. response = self.api_client.post( '/orchestra/api/project/project_information/', {'projetc_id': project.id}, # Typo. format='json') self.ensure_response(response, { 'error': 400, 'message': 'project_id is required' }, 400) # Retrieve the third project, which has no task assignments. response = self.api_client.post( '/orchestra/api/project/project_information/', {'project_id': self.projects['no_task_assignments'].id}, format='json') returned = json.loads(response.content.decode('utf-8')) for key in ('id', 'project', 'start_datetime'): del returned['tasks']['step1'][key] self.assertEquals(response.status_code, 200) self.assertEquals( returned['tasks'], { 'step1': { 'assignments': [], 'latest_data': None, 'status': 'Awaiting Processing', 'step_slug': 'step1' } })
def test_legal_get_next_task_status(self): task = self.tasks['processing_task'] workflow = get_workflow_by_slug(task.project.workflow_slug) step = workflow.get_step(task.step_slug) step.review_policy = {} task.status = Task.Status.PROCESSING with self.assertRaises(ReviewPolicyError): get_next_task_status(task, TaskAssignment.SnapshotType.SUBMIT) step.review_policy = {'policy': 'sampled_review', 'rate': 1, 'max_reviews': 1} self.assertEquals( get_next_task_status(task, TaskAssignment.SnapshotType.SUBMIT), Task.Status.PENDING_REVIEW) step.review_policy = {'policy': 'sampled_review', 'rate': 0, 'max_reviews': 1} self.assertEquals( get_next_task_status(task, TaskAssignment.SnapshotType.SUBMIT), Task.Status.COMPLETE) task.status = Task.Status.POST_REVIEW_PROCESSING self.assertEquals( get_next_task_status(task, TaskAssignment.SnapshotType.SUBMIT), Task.Status.REVIEWING) task = self.tasks['review_task'] task.status = Task.Status.REVIEWING step.review_policy = {'policy': 'sampled_review', 'rate': 1, 'max_reviews': 0} self.assertEquals( get_next_task_status(task, TaskAssignment.SnapshotType.ACCEPT), Task.Status.COMPLETE) step.review_policy = {'policy': 'sampled_review', 'rate': 1, 'max_reviews': 2} self.assertEquals( get_next_task_status(task, TaskAssignment.SnapshotType.ACCEPT), Task.Status.PENDING_REVIEW) # after max reviews done a task goes to state complete TaskAssignment.objects.create(worker=self.workers[1], task=task, status=TaskAssignment.Status.SUBMITTED, assignment_counter=1, in_progress_task_data={}, snapshots=empty_snapshots()) task.save() step.review_policy = {'policy': 'sampled_review', 'rate': 1, 'max_reviews': 1} self.assertEquals( get_next_task_status(task, TaskAssignment.SnapshotType.ACCEPT), Task.Status.COMPLETE)
def assign_task(worker_id, task_id): """ Return a given task after assigning or reassigning it to the specified worker. Args: worker_id (int): The ID of the worker to be assigned. task_id (int): The ID of the task to be assigned. Returns: task (orchestra.models.Task): The newly assigned task. Raises: orchestra.core.errors.TaskAssignmentError: The specified worker is already assigned to the given task or the task status is not compatible with new assignment. orchestra.core.errors.WorkerCertificationError: The specified worker is not certified for the given task. """ worker = Worker.objects.get(id=worker_id) task = Task.objects.get(id=task_id) roles = { Task.Status.AWAITING_PROCESSING: WorkerCertification.Role.ENTRY_LEVEL, Task.Status.PENDING_REVIEW: WorkerCertification.Role.REVIEWER } required_role = roles.get(task.status) if required_role is None: raise TaskAssignmentError('Status incompatible with new assignment') assignment = current_assignment(task) if not _worker_certified_for_task(worker, task, required_role): raise WorkerCertificationError('Worker not certified for this task.') if is_worker_assigned_to_task(worker, task): raise TaskAssignmentError('Worker already assigned to this task.') assignment_counter = task.assignments.count() in_progress_task_data = {} if assignment: # In-progress task data is the latest # submission by a previous worker in_progress_task_data = assignment.in_progress_task_data previous_status = task.status if previous_status == Task.Status.AWAITING_PROCESSING: task.status = Task.Status.PROCESSING elif previous_status == Task.Status.PENDING_REVIEW: task.status = Task.Status.REVIEWING task.save() (TaskAssignment.objects.create(worker=worker, task=task, status=TaskAssignment.Status.PROCESSING, assignment_counter=assignment_counter, in_progress_task_data=in_progress_task_data, snapshots=empty_snapshots())) add_worker_to_project_team(worker, task.project) notify_status_change(task, previous_status) return task
def test_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 = json.loads(response.content.decode('utf-8')) # 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, }, 'snapshots': empty_snapshots(), 'iterations': [ { 'start_datetime': '2015-10-12T01:00:00', 'end_datetime': '2015-10-12T02:00:00', } ], 'in_progress_task_data': {}, 'id': assignment.id } self.assertEquals( expected_assignment, {k: sample_assignment[k] for k in expected_assignment.keys()})
def test_legal_get_next_task_status(self): task = self.tasks['awaiting_processing'] step = task.step task.status = Task.Status.PROCESSING step.review_policy = {} step.save() with self.assertRaises(ReviewPolicyError): get_next_task_status(task, TaskAssignment.SnapshotType.SUBMIT) step.review_policy = { 'policy': 'sampled_review', 'rate': 1, 'max_reviews': 1 } step.save() self.assertEquals( get_next_task_status(task, TaskAssignment.SnapshotType.SUBMIT), Task.Status.PENDING_REVIEW) step.review_policy = { 'policy': 'sampled_review', 'rate': 0, 'max_reviews': 1 } step.save() self.assertEquals( get_next_task_status(task, TaskAssignment.SnapshotType.SUBMIT), Task.Status.COMPLETE) task.status = Task.Status.POST_REVIEW_PROCESSING self.assertEquals( get_next_task_status(task, TaskAssignment.SnapshotType.SUBMIT), Task.Status.REVIEWING) task = self.tasks['review_task'] task.status = Task.Status.REVIEWING step.review_policy = { 'policy': 'sampled_review', 'rate': 1, 'max_reviews': 0 } step.save() self.assertEquals( get_next_task_status(task, TaskAssignment.SnapshotType.ACCEPT), Task.Status.COMPLETE) step.review_policy = { 'policy': 'sampled_review', 'rate': 1, 'max_reviews': 2 } step.save() self.assertEquals( get_next_task_status(task, TaskAssignment.SnapshotType.ACCEPT), Task.Status.PENDING_REVIEW) # after max reviews done a task goes to state complete TaskAssignment.objects.create(worker=self.workers[1], task=task, status=TaskAssignment.Status.SUBMITTED, assignment_counter=1, in_progress_task_data={}, snapshots=empty_snapshots()) task.save() step.review_policy = { 'policy': 'sampled_review', 'rate': 1, 'max_reviews': 1 } step.save() self.assertEquals( get_next_task_status(task, TaskAssignment.SnapshotType.ACCEPT), Task.Status.COMPLETE)
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 project_management_information(project_id): project = Project.objects.get(id=project_id) df = work_time_df([project], human_only=False, complete_tasks_only=False) project_information = get_project_information(project.id) project_information['project']['status'] = dict( Project.STATUS_CHOICES).get(project.status, None) project_information['project']['admin_url'] = urljoin( settings.ORCHESTRA_URL, urlresolvers.reverse('admin:orchestra_project_change', args=(project_id, ))) for slug, task in project_information['tasks'].items(): task['admin_url'] = urljoin( settings.ORCHESTRA_URL, urlresolvers.reverse('admin:orchestra_task_change', args=(task['id'], ))) for assignment in task['assignments']: assignment['admin_url'] = urljoin( settings.ORCHESTRA_URL, urlresolvers.reverse('admin:orchestra_taskassignment_change', args=(assignment['id'], ))) iterations = df[(df.worker == assignment['worker']['username']) & (df.task_id == task['id'])] iterations = iterations[['start_datetime', 'end_datetime']] assignment['iterations'] = [] for idx, info in iterations.T.items(): iteration = info.to_dict() assignment['iterations'].append(iteration) if assignment['status'] == 'Processing': last_iteration_end = assignment['start_datetime'] last_assignment = last_snapshotted_assignment(task['id']) if last_assignment and len(assignment['iterations']) > 1: last_iteration_end = ( last_assignment.snapshots['snapshots'][-1]['datetime']) assignment['iterations'].append({ 'start_datetime': last_iteration_end, 'end_datetime': timezone.now() }) if task['status'] in ('Awaiting Processing', 'Pending Review'): last_assignment_end = task['start_datetime'] last_assignment = last_snapshotted_assignment(task['id']) if last_assignment: last_assignment_end = ( last_assignment.snapshots['snapshots'][-1]['datetime']) task['assignments'].append({ 'iterations': [{ 'start_datetime': last_assignment_end, 'end_datetime': timezone.now() }], 'snapshots': empty_snapshots(), 'start_datetime': last_assignment_end, 'status': 'Processing', 'task': task['id'], 'worker': { 'id': None, 'username': None }, }) return project_information