def test_is_worker_assigned_to_task(self):
        task = self.tasks['review_task']

        # worker is not related to any task
        self.assertFalse(is_worker_assigned_to_task(self.workers[2], task))

        # worker is assigned to a task.
        self.assertTrue(is_worker_assigned_to_task(self.workers[0], task))
    def test_is_worker_assigned_to_task(self):
        task = self.tasks['review_task']

        # worker is not related to any task
        self.assertFalse(is_worker_assigned_to_task(self.workers[2],
                                                    task))

        # worker is assigned to a task.
        self.assertTrue(is_worker_assigned_to_task(self.workers[0],
                                                   task))
Beispiel #3
0
def save_task(task_id, task_data, worker):
    """
    Save the latest data to the database for a task assignment,
    overwriting previously saved data.

    Args:
        task_id (int):
            The ID of the task to save.
        task_data (str):
            A JSON blob of task data to commit to the database.
        worker (orchestra.models.Worker):
            The worker saving the task.

    Returns:
        None

    Raises:
        orchestra.core.errors.TaskAssignmentError:
            The provided worker is not assigned to the given task or the
            assignment is in a non-processing state.
    """
    task = Task.objects.get(id=task_id)
    if not is_worker_assigned_to_task(worker, task):
        raise TaskAssignmentError('Worker is not associated with task')

    # Use select_for_update to prevent concurrency issues with submit_task.
    # See https://github.com/unlimitedlabs/orchestra/issues/2.
    assignment = (TaskAssignment.objects.select_for_update()
                                .get(task=task, worker=worker))

    if assignment.status != TaskAssignment.Status.PROCESSING:
        raise TaskAssignmentError('Worker is not allowed to save')

    assignment.in_progress_task_data = task_data
    assignment.save()
Beispiel #4
0
def save_task(task_id, task_data, worker):
    """
    Save the latest data to the database for a task assignment,
    overwriting previously saved data.

    Args:
        task_id (int):
            The ID of the task to save.
        task_data (str):
            A JSON blob of task data to commit to the database.
        worker (orchestra.models.Worker):
            The worker saving the task.

    Returns:
        None

    Raises:
        orchestra.core.errors.TaskAssignmentError:
            The provided worker is not assigned to the given task or the
            assignment is in a non-processing state.
    """
    task = Task.objects.get(id=task_id)
    if not is_worker_assigned_to_task(worker, task):
        raise TaskAssignmentError('Worker is not associated with task')

    # Use select_for_update to prevent concurrency issues with submit_task.
    # See https://github.com/unlimitedlabs/orchestra/issues/2.
    assignment = (TaskAssignment.objects.select_for_update().get(
        task=task, worker=worker))

    if assignment.status != TaskAssignment.Status.PROCESSING:
        raise TaskAssignmentError('Worker is not allowed to save')

    assignment.in_progress_task_data = task_data
    assignment.save()
    def test_assign_task(self):
        entry_task = TaskFactory(project=self.projects['base_test_project'],
                                 status=Task.Status.AWAITING_PROCESSING,
                                 step=self.test_step)

        # Assign entry-level task to entry-level worker
        entry_task = assign_task(self.workers[0].id, entry_task.id)
        self.assertTrue(is_worker_assigned_to_task(self.workers[0],
                                                   entry_task))
        self.assertEquals(entry_task.status, Task.Status.PROCESSING)
        self.assertEquals(entry_task.assignments.count(), 1)

        # Attempt to assign task which isn't awaiting a new assignment
        invalid = (Task.Status.PROCESSING, Task.Status.ABORTED,
                   Task.Status.REVIEWING, Task.Status.COMPLETE,
                   Task.Status.POST_REVIEW_PROCESSING)
        for status in invalid:
            invalid_status_task = Task.objects.create(
                project=self.projects['base_test_project'],
                status=status,
                step=self.test_step)

            with self.assertRaises(TaskAssignmentError):
                invalid_status_task = assign_task(self.workers[0].id,
                                                  invalid_status_task.id)

        # Attempt to assign review task to worker already in review hierarchy
        review_task = Task.objects.create(
            project=self.projects['base_test_project'],
            status=Task.Status.PENDING_REVIEW,
            step=self.test_step)
        test_data = {'test_assign': True}
        TaskAssignmentFactory(worker=self.workers[1],
                              task=review_task,
                              status=TaskAssignment.Status.SUBMITTED,
                              in_progress_task_data=test_data,
                              snapshots=empty_snapshots())

        with self.assertRaises(TaskAssignmentError):
            assign_task(self.workers[1].id, review_task.id)
        self.assertEqual(
            current_assignment(review_task).in_progress_task_data, test_data)

        # Attempt to assign review task to worker not certified for task
        with self.assertRaises(WorkerCertificationError):
            assign_task(self.workers[2].id, review_task.id)
        self.assertEqual(
            current_assignment(review_task).in_progress_task_data, test_data)

        # Assign review task to review worker
        self.assertEquals(review_task.assignments.count(), 1)
        review_task = assign_task(self.workers[3].id, review_task.id)
        self.assertEquals(review_task.assignments.count(), 2)
        self.assertEqual(
            current_assignment(review_task).worker, self.workers[3])
        self.assertEqual(
            current_assignment(review_task).in_progress_task_data, test_data)
        self.assertEquals(review_task.status, Task.Status.REVIEWING)
    def test_preassign_workers(self):
        project = self.projects['assignment_policy']

        # Create first task in test project
        create_subsequent_tasks(project)
        self.assertEquals(project.tasks.count(), 1)
        # Assign initial task to worker 0
        initial_task = assign_task(self.workers[0].id,
                                   project.tasks.first().id)
        # Submit task; next task should be created
        with patch('orchestra.utils.task_lifecycle._is_review_needed',
                   return_value=False):
            initial_task = submit_task(initial_task.id, {},
                                       TaskAssignment.SnapshotType.SUBMIT,
                                       self.workers[0], 0)
        self.assertEquals(project.tasks.count(), 2)
        related_task = project.tasks.exclude(id=initial_task.id).first()
        # Worker 0 not certified for related tasks, so should not have been
        # auto-assigned
        self.assertEquals(related_task.assignments.count(), 0)
        self.assertEquals(related_task.status, Task.Status.AWAITING_PROCESSING)

        # Reset project
        project.tasks.all().delete()

        # Create first task in test project
        create_subsequent_tasks(project)
        self.assertEquals(project.tasks.count(), 1)
        # Assign initial task to worker 4
        initial_task = assign_task(self.workers[4].id,
                                   project.tasks.first().id)
        # Submit task; next task should be created
        with patch('orchestra.utils.task_lifecycle._is_review_needed',
                   return_value=False):
            initial_task = submit_task(initial_task.id, {},
                                       TaskAssignment.SnapshotType.SUBMIT,
                                       self.workers[4], 0)
        self.assertEquals(project.tasks.count(), 2)
        related_task = project.tasks.exclude(id=initial_task.id).first()
        # Worker 4 is certified for related task and should have been assigned
        self.assertEquals(related_task.assignments.count(), 1)
        self.assertEquals(related_task.status, Task.Status.PROCESSING)
        self.assertTrue(is_worker_assigned_to_task(self.workers[4],
                                                   related_task))

        # Reset project
        project.tasks.all().delete()
    def test_preassign_workers(self):
        project = self.projects['assignment_policy']

        # Create first task in test project
        create_subsequent_tasks(project)
        self.assertEquals(project.tasks.count(), 1)
        # Assign initial task to worker 0
        initial_task = assign_task(self.workers[0].id,
                                   project.tasks.first().id)
        # Submit task; next task should be created
        with patch('orchestra.utils.task_lifecycle._is_review_needed',
                   return_value=False):
            initial_task = submit_task(initial_task.id, {},
                                       TaskAssignment.SnapshotType.SUBMIT,
                                       self.workers[0], 0)
        self.assertEquals(project.tasks.count(), 2)
        related_task = project.tasks.exclude(id=initial_task.id).first()
        # Worker 0 not certified for related tasks, so should not have been
        # auto-assigned
        self.assertEquals(related_task.assignments.count(), 0)
        self.assertEquals(related_task.status, Task.Status.AWAITING_PROCESSING)

        # Reset project
        project.tasks.all().delete()

        # Create first task in test project
        create_subsequent_tasks(project)
        self.assertEquals(project.tasks.count(), 1)
        # Assign initial task to worker 4
        initial_task = assign_task(self.workers[4].id,
                                   project.tasks.first().id)
        # Submit task; next task should be created
        with patch('orchestra.utils.task_lifecycle._is_review_needed',
                   return_value=False):
            initial_task = submit_task(initial_task.id, {},
                                       TaskAssignment.SnapshotType.SUBMIT,
                                       self.workers[4], 0)
        self.assertEquals(project.tasks.count(), 2)
        related_task = project.tasks.exclude(id=initial_task.id).first()
        # Worker 4 is certified for related task and should have been assigned
        self.assertEquals(related_task.assignments.count(), 1)
        self.assertEquals(related_task.status, Task.Status.PROCESSING)
        self.assertTrue(
            is_worker_assigned_to_task(self.workers[4], related_task))

        # Reset project
        project.tasks.all().delete()
Beispiel #8
0
def reassign_assignment(worker_id, assignment_id):
    """
    Return a given assignment after reassigning it to the specified
    worker.

    Args:
        worker_id (int):
            The ID of the worker to be assigned.
        assignment_id (int):
            The ID of the assignment to be assigned.

    Returns:
        assignment (orchestra.models.TaskAssignment):
            The newly assigned assignment.

    Raises:
        orchestra.core.errors.TaskAssignmentError:
            The specified worker is already assigned to the given task.
        orchestra.core.errors.WorkerCertificationError:
            The specified worker is not certified for the given assignment.
    """
    worker = Worker.objects.get(id=worker_id)
    assignment = TaskAssignment.objects.get(id=assignment_id)
    if assignment.assignment_counter > 0:
        role = WorkerCertification.Role.REVIEWER
    else:
        role = WorkerCertification.Role.ENTRY_LEVEL

    if not _worker_certified_for_task(worker, assignment.task, role):
        raise WorkerCertificationError(
            'Worker not certified for this assignment.')
    if is_worker_assigned_to_task(worker, assignment.task):
        raise TaskAssignmentError('Worker already assigned to this task.')

    assignment.worker = worker
    assignment.save()
    add_worker_to_project_team(worker, assignment.task.project)

    return assignment
Beispiel #9
0
def reassign_assignment(worker_id, assignment_id):
    """
    Return a given assignment after reassigning it to the specified
    worker.

    Args:
        worker_id (int):
            The ID of the worker to be assigned.
        assignment_id (int):
            The ID of the assignment to be assigned.

    Returns:
        assignment (orchestra.models.TaskAssignment):
            The newly assigned assignment.

    Raises:
        orchestra.core.errors.TaskAssignmentError:
            The specified worker is already assigned to the given task.
        orchestra.core.errors.WorkerCertificationError:
            The specified worker is not certified for the given assignment.
    """
    worker = Worker.objects.get(id=worker_id)
    assignment = TaskAssignment.objects.get(id=assignment_id)
    if assignment.assignment_counter > 0:
        role = WorkerCertification.Role.REVIEWER
    else:
        role = WorkerCertification.Role.ENTRY_LEVEL

    if not _worker_certified_for_task(worker, assignment.task, role):
        raise WorkerCertificationError(
            'Worker not certified for this assignment.')
    if is_worker_assigned_to_task(worker, assignment.task):
        raise TaskAssignmentError('Worker already assigned to this task.')

    assignment.worker = worker
    assignment.save()
    add_worker_to_project_team(worker, assignment.task.project)

    return assignment
Beispiel #10
0
def get_task_overview_for_worker(task_id, worker):
    """
    Get information about `task` and its assignment for `worker`.

    Args:
        task_id (int):
            The ID of the desired task object.
        worker (orchestra.models.Worker):
            The specified worker object.

    Returns:
        task_assignment_details (dict):
            Information about `task` and its assignment for `worker`.
    """
    task = Task.objects.get(id=task_id)
    if not is_worker_assigned_to_task(worker, task):
        raise TaskAssignmentError('Worker is not associated with task')
    task_details = get_task_details(task_id)

    task_assignment = TaskAssignment.objects.get(worker=worker, task=task)
    task_assignment_details = get_task_assignment_details(task_assignment)
    task_assignment_details.update(task_details)
    return task_assignment_details
Beispiel #11
0
def get_task_overview_for_worker(task_id, worker):
    """
    Get information about `task` and its assignment for `worker`.

    Args:
        task_id (int):
            The ID of the desired task object.
        worker (orchestra.models.Worker):
            The specified worker object.

    Returns:
        task_assignment_details (dict):
            Information about `task` and its assignment for `worker`.
    """
    task = Task.objects.get(id=task_id)
    if not is_worker_assigned_to_task(worker, task):
        raise TaskAssignmentError('Worker is not associated with task')
    task_details = get_task_details(task_id)

    task_assignment = TaskAssignment.objects.get(worker=worker,
                                                 task=task)
    task_assignment_details = get_task_assignment_details(task_assignment)
    task_assignment_details.update(task_details)
    return task_assignment_details
Beispiel #12
0
    def test_notify_status_change(self):
        project = self.projects['empty_project']
        internal_name = settings.SLACK_INTERNAL_NOTIFICATION_CHANNEL.strip('#')
        internal_groups = [
            group for group in self.slack.groups.list().body['groups']
            if group['name'] == internal_name]
        internal_group_id = internal_groups[0]['id']
        internal_slack_messages = self.slack.get_messages(internal_group_id)
        experts_slack_messages = self.slack.get_messages(
            project.slack_group_id)

        def _validate_slack_messages(message_stub):
            """
            Check that correct slack message was sent if API key present.
            """
            self.assertIn(message_stub, internal_slack_messages.pop())
            self.assertIn(message_stub, experts_slack_messages.pop())

        task = TaskFactory(project=project,
                           step_slug=self.test_step_slug,
                           status=Task.Status.AWAITING_PROCESSING)

        # Entry-level worker picks up task
        self.assertEquals(task.status, Task.Status.AWAITING_PROCESSING)
        task = assign_task(self.workers[0].id, task.id)
        self.assertTrue(is_worker_assigned_to_task(self.workers[0], task))

        # Notification should be sent to entry-level worker
        self.assertEquals(len(self.mail.inbox), 1)
        notification = self.mail.inbox.pop()
        self.assertEquals(notification['recipient'],
                          self.workers[0].user.email)
        self.assertEquals(notification['subject'],
                          "You've been assigned to a new task!")

        _validate_slack_messages('Task has been picked up by a worker.')
        self.assertEquals(len(internal_slack_messages), 0)
        self.assertEquals(len(experts_slack_messages), 0)

        with patch('orchestra.utils.task_lifecycle._is_review_needed',
                   return_value=True):
            # Entry-level worker submits task
            task = submit_task(task.id, {}, TaskAssignment.SnapshotType.SUBMIT,
                               self.workers[0], 0)

        self.assertEquals(task.status, Task.Status.PENDING_REVIEW)
        # Notification should be sent to entry-level worker
        self.assertEquals(len(self.mail.inbox), 1)
        notification = self.mail.inbox.pop()
        self.assertEquals(notification['recipient'],
                          self.workers[0].user.email)
        self.assertEquals(notification['subject'],
                          'Your task is under review!')

        _validate_slack_messages('Task is awaiting review.')
        self.assertEquals(len(internal_slack_messages), 0)
        self.assertEquals(len(experts_slack_messages), 0)

        # Reviewer picks up task
        task = assign_task(self.workers[1].id, task.id)
        self.assertEquals(task.status, Task.Status.REVIEWING)
        # No notification should be sent
        self.assertEquals(len(self.mail.inbox), 0)

        _validate_slack_messages('Task is under review.')
        self.assertEquals(len(internal_slack_messages), 0)
        self.assertEquals(len(experts_slack_messages), 0)

        # Reviewer rejects task
        task = submit_task(task.id, {}, TaskAssignment.SnapshotType.REJECT,
                           self.workers[1], 0)
        self.assertEquals(task.status, Task.Status.POST_REVIEW_PROCESSING)
        # Notification should be sent to original worker
        self.assertEquals(len(self.mail.inbox), 1)
        notification = self.mail.inbox.pop()
        self.assertEquals(notification['recipient'],
                          self.workers[0].user.email)
        self.assertEquals(notification['subject'],
                          'Your task has been returned')

        _validate_slack_messages('Task was returned by reviewer.')
        self.assertEquals(len(internal_slack_messages), 0)
        self.assertEquals(len(experts_slack_messages), 0)

        # Entry-level worker resubmits task
        task = submit_task(task.id, {}, TaskAssignment.SnapshotType.SUBMIT,
                           self.workers[0], 0)
        self.assertEquals(task.status, Task.Status.REVIEWING)
        # Notification should be sent to reviewer
        self.assertEquals(len(self.mail.inbox), 1)
        notification = self.mail.inbox.pop()
        self.assertEquals(notification['recipient'],
                          self.workers[1].user.email)
        self.assertEquals(notification['subject'],
                          'A task is ready for re-review!')

        _validate_slack_messages('Task is under review.')
        self.assertEquals(len(internal_slack_messages), 0)
        self.assertEquals(len(experts_slack_messages), 0)

        # First reviewer accepts task
        with patch('orchestra.utils.task_lifecycle._is_review_needed',
                   return_value=True):
            task = submit_task(task.id, {}, TaskAssignment.SnapshotType.ACCEPT,
                               self.workers[1], 0)
        self.assertEquals(task.status, Task.Status.PENDING_REVIEW)
        # Notification should be sent to first reviewer
        self.assertEquals(len(self.mail.inbox), 1)
        notification = self.mail.inbox.pop()
        self.assertEquals(notification['recipient'],
                          self.workers[1].user.email)
        self.assertEquals(notification['subject'],
                          'Your task is under review!')

        _validate_slack_messages('Task is awaiting review.')
        self.assertEquals(len(internal_slack_messages), 0)
        self.assertEquals(len(experts_slack_messages), 0)

        # Second reviewer picks up task
        task = assign_task(self.workers[3].id, task.id)
        self.assertEquals(task.status, Task.Status.REVIEWING)
        # No notification should be sent
        self.assertEquals(len(self.mail.inbox), 0)

        _validate_slack_messages('Task is under review.')
        self.assertEquals(len(internal_slack_messages), 0)
        self.assertEquals(len(experts_slack_messages), 0)

        # Second reviewer rejects task
        task = submit_task(task.id, {}, TaskAssignment.SnapshotType.REJECT,
                           self.workers[3], 0)
        self.assertEquals(task.status, Task.Status.POST_REVIEW_PROCESSING)
        # Notification should be sent to first reviewer
        self.assertEquals(len(self.mail.inbox), 1)
        notification = self.mail.inbox.pop()
        self.assertEquals(notification['recipient'],
                          self.workers[1].user.email)
        self.assertEquals(notification['subject'],
                          'Your task has been returned')

        _validate_slack_messages('Task was returned by reviewer.')
        self.assertEquals(len(internal_slack_messages), 0)
        self.assertEquals(len(experts_slack_messages), 0)

        # First reviewer resubmits task
        task = submit_task(task.id, {}, TaskAssignment.SnapshotType.SUBMIT,
                           self.workers[1], 0)
        self.assertEquals(task.status, Task.Status.REVIEWING)
        # Notification should be sent to second reviewer
        self.assertEquals(len(self.mail.inbox), 1)
        notification = self.mail.inbox.pop()
        self.assertEquals(notification['recipient'],
                          self.workers[3].user.email)
        self.assertEquals(notification['subject'],
                          'A task is ready for re-review!')

        _validate_slack_messages('Task is under review.')
        self.assertEquals(len(internal_slack_messages), 0)
        self.assertEquals(len(experts_slack_messages), 0)

        # Second reviewer accepts task; task is complete
        with patch('orchestra.utils.task_lifecycle._is_review_needed',
                   return_value=False):
            task = submit_task(task.id, {}, TaskAssignment.SnapshotType.ACCEPT,
                               self.workers[3], 0)
        self.assertEquals(task.status, Task.Status.COMPLETE)

        # Notification should be sent to all workers on task
        self.assertEquals(len(self.mail.inbox), 3)
        recipients = {mail['recipient'] for mail in self.mail.inbox}
        subjects = {mail['subject'] for mail in self.mail.inbox}
        self.assertEquals(recipients,
                          {self.workers[uid].user.email for uid in (0, 1, 3)})
        self.assertEquals(subjects, {'Task complete!'})
        self.mail.clear()

        _validate_slack_messages('Task has been completed.')
        self.assertEquals(len(internal_slack_messages), 0)
        self.assertEquals(len(experts_slack_messages), 0)

        # End project
        end_project(task.project.id)
        task = Task.objects.get(id=task.id)
        self.assertEquals(task.status, Task.Status.ABORTED)

        # Notification should be sent to all workers on task
        self.assertEquals(len(self.mail.inbox), 3)
        recipients = {mail['recipient'] for mail in self.mail.inbox}
        subjects = {mail['subject'] for mail in self.mail.inbox}
        self.assertEquals(recipients,
                          {self.workers[uid].user.email for uid in (0, 1, 3)})
        self.assertEquals(subjects,
                          {'A task you were working on has been ended'})
        self.mail.clear()

        for task in project.tasks.all():
            _validate_slack_messages('Task has been aborted.')
        self.assertEquals(len(internal_slack_messages), 0)
        self.assertEquals(len(experts_slack_messages), 0)
    def test_notify_status_change(self):
        project = self.projects['empty_project']
        internal_name = settings.SLACK_INTERNAL_NOTIFICATION_CHANNEL.strip('#')
        internal_groups = [
            group for group in self.slack.groups.list().body['groups']
            if group['name'] == internal_name
        ]
        internal_group_id = internal_groups[0]['id']
        internal_slack_messages = self.slack.get_messages(internal_group_id)
        experts_slack_messages = self.slack.get_messages(
            project.slack_group_id)

        def _validate_slack_messages(message_stub):
            """
            Check that correct slack message was sent if API key present.
            """
            self.assertIn(message_stub, internal_slack_messages.pop())
            self.assertIn(message_stub, experts_slack_messages.pop())

        task = TaskFactory(project=project,
                           step=self.test_step,
                           status=Task.Status.AWAITING_PROCESSING)

        # Entry-level worker picks up task
        self.assertEquals(task.status, Task.Status.AWAITING_PROCESSING)
        task = assign_task(self.workers[0].id, task.id)
        self.assertTrue(is_worker_assigned_to_task(self.workers[0], task))

        # Notification should be sent to entry-level worker
        self.assertEquals(len(self.mail.inbox), 1)
        notification = self.mail.inbox.pop()
        self.assertEquals(notification['recipient'],
                          self.workers[0].user.email)
        self.assertEquals(notification['subject'],
                          "You've been assigned to a new task!")

        _validate_slack_messages('Task has been picked up by a worker.')
        self.assertEquals(len(internal_slack_messages), 0)
        self.assertEquals(len(experts_slack_messages), 0)

        with patch('orchestra.utils.task_lifecycle._is_review_needed',
                   return_value=True):
            # Entry-level worker submits task
            task = submit_task(task.id, {}, TaskAssignment.SnapshotType.SUBMIT,
                               self.workers[0], 0)

        self.assertEquals(task.status, Task.Status.PENDING_REVIEW)
        # Notification should be sent to entry-level worker
        self.assertEquals(len(self.mail.inbox), 1)
        notification = self.mail.inbox.pop()
        self.assertEquals(notification['recipient'],
                          self.workers[0].user.email)
        self.assertEquals(notification['subject'],
                          'Your task is under review!')

        _validate_slack_messages('Task is awaiting review.')
        self.assertEquals(len(internal_slack_messages), 0)
        self.assertEquals(len(experts_slack_messages), 0)

        # Reviewer picks up task
        task = assign_task(self.workers[1].id, task.id)
        self.assertEquals(task.status, Task.Status.REVIEWING)
        # No notification should be sent
        self.assertEquals(len(self.mail.inbox), 0)

        _validate_slack_messages('Task is under review.')
        self.assertEquals(len(internal_slack_messages), 0)
        self.assertEquals(len(experts_slack_messages), 0)

        # Reviewer rejects task
        task = submit_task(task.id, {}, TaskAssignment.SnapshotType.REJECT,
                           self.workers[1], 0)
        self.assertEquals(task.status, Task.Status.POST_REVIEW_PROCESSING)
        # Notification should be sent to original worker
        self.assertEquals(len(self.mail.inbox), 1)
        notification = self.mail.inbox.pop()
        self.assertEquals(notification['recipient'],
                          self.workers[0].user.email)
        self.assertEquals(notification['subject'],
                          'Your task has been returned')

        _validate_slack_messages('Task was returned by reviewer.')
        self.assertEquals(len(internal_slack_messages), 0)
        self.assertEquals(len(experts_slack_messages), 0)

        # Entry-level worker resubmits task
        task = submit_task(task.id, {}, TaskAssignment.SnapshotType.SUBMIT,
                           self.workers[0], 0)
        self.assertEquals(task.status, Task.Status.REVIEWING)
        # Notification should be sent to reviewer
        self.assertEquals(len(self.mail.inbox), 1)
        notification = self.mail.inbox.pop()
        self.assertEquals(notification['recipient'],
                          self.workers[1].user.email)
        self.assertEquals(notification['subject'],
                          'A task is ready for re-review!')

        _validate_slack_messages('Task is under review.')
        self.assertEquals(len(internal_slack_messages), 0)
        self.assertEquals(len(experts_slack_messages), 0)

        # First reviewer accepts task
        with patch('orchestra.utils.task_lifecycle._is_review_needed',
                   return_value=True):
            task = submit_task(task.id, {}, TaskAssignment.SnapshotType.ACCEPT,
                               self.workers[1], 0)
        self.assertEquals(task.status, Task.Status.PENDING_REVIEW)
        # Notification should be sent to first reviewer
        self.assertEquals(len(self.mail.inbox), 1)
        notification = self.mail.inbox.pop()
        self.assertEquals(notification['recipient'],
                          self.workers[1].user.email)
        self.assertEquals(notification['subject'],
                          'Your task is under review!')

        _validate_slack_messages('Task is awaiting review.')
        self.assertEquals(len(internal_slack_messages), 0)
        self.assertEquals(len(experts_slack_messages), 0)

        # Second reviewer picks up task
        task = assign_task(self.workers[3].id, task.id)
        self.assertEquals(task.status, Task.Status.REVIEWING)
        # No notification should be sent
        self.assertEquals(len(self.mail.inbox), 0)

        _validate_slack_messages('Task is under review.')
        self.assertEquals(len(internal_slack_messages), 0)
        self.assertEquals(len(experts_slack_messages), 0)

        # Second reviewer rejects task
        task = submit_task(task.id, {}, TaskAssignment.SnapshotType.REJECT,
                           self.workers[3], 0)
        self.assertEquals(task.status, Task.Status.POST_REVIEW_PROCESSING)
        # Notification should be sent to first reviewer
        self.assertEquals(len(self.mail.inbox), 1)
        notification = self.mail.inbox.pop()
        self.assertEquals(notification['recipient'],
                          self.workers[1].user.email)
        self.assertEquals(notification['subject'],
                          'Your task has been returned')

        _validate_slack_messages('Task was returned by reviewer.')
        self.assertEquals(len(internal_slack_messages), 0)
        self.assertEquals(len(experts_slack_messages), 0)

        # First reviewer resubmits task
        task = submit_task(task.id, {}, TaskAssignment.SnapshotType.SUBMIT,
                           self.workers[1], 0)
        self.assertEquals(task.status, Task.Status.REVIEWING)
        # Notification should be sent to second reviewer
        self.assertEquals(len(self.mail.inbox), 1)
        notification = self.mail.inbox.pop()
        self.assertEquals(notification['recipient'],
                          self.workers[3].user.email)
        self.assertEquals(notification['subject'],
                          'A task is ready for re-review!')

        _validate_slack_messages('Task is under review.')
        self.assertEquals(len(internal_slack_messages), 0)
        self.assertEquals(len(experts_slack_messages), 0)

        # Second reviewer accepts task; task is complete
        with patch('orchestra.utils.task_lifecycle._is_review_needed',
                   return_value=False):
            task = submit_task(task.id, {}, TaskAssignment.SnapshotType.ACCEPT,
                               self.workers[3], 0)
        self.assertEquals(task.status, Task.Status.COMPLETE)

        # Notification should be sent to all workers on task
        self.assertEquals(len(self.mail.inbox), 3)
        recipients = {mail['recipient'] for mail in self.mail.inbox}
        subjects = {mail['subject'] for mail in self.mail.inbox}
        self.assertEquals(recipients,
                          {self.workers[uid].user.email
                           for uid in (0, 1, 3)})
        self.assertEquals(subjects, {'Task complete!'})
        self.mail.clear()

        _validate_slack_messages('Task has been completed.')
        self.assertEquals(len(internal_slack_messages), 0)
        self.assertEquals(len(experts_slack_messages), 0)

        # End project
        end_project(task.project.id)
        task = Task.objects.get(id=task.id)
        self.assertEquals(task.status, Task.Status.ABORTED)

        # Notification should be sent to all workers on task
        self.assertEquals(len(self.mail.inbox), 3)
        recipients = {mail['recipient'] for mail in self.mail.inbox}
        subjects = {mail['subject'] for mail in self.mail.inbox}
        self.assertEquals(recipients,
                          {self.workers[uid].user.email
                           for uid in (0, 1, 3)})
        self.assertEquals(subjects,
                          {'A task you were working on has been ended'})
        self.mail.clear()

        for task in project.tasks.all():
            _validate_slack_messages('Task has been aborted.')
        self.assertEquals(len(internal_slack_messages), 0)
        self.assertEquals(len(experts_slack_messages), 0)
    def test_assign_task(self):
        entry_task = TaskFactory(
            project=self.projects['base_test_project'],
            status=Task.Status.AWAITING_PROCESSING,
            step=self.test_step)

        # Assign entry-level task to entry-level worker
        entry_task = assign_task(self.workers[0].id, entry_task.id)
        self.assertTrue(is_worker_assigned_to_task(self.workers[0],
                                                   entry_task))
        self.assertEquals(entry_task.status, Task.Status.PROCESSING)
        self.assertEquals(entry_task.assignments.count(), 1)

        # Attempt to assign task which isn't awaiting a new assignment
        invalid = (Task.Status.PROCESSING, Task.Status.ABORTED,
                   Task.Status.REVIEWING, Task.Status.COMPLETE,
                   Task.Status.POST_REVIEW_PROCESSING)
        for status in invalid:
            invalid_status_task = Task.objects.create(
                project=self.projects['base_test_project'],
                status=status,
                step=self.test_step)

            with self.assertRaises(TaskAssignmentError):
                invalid_status_task = assign_task(
                    self.workers[0].id, invalid_status_task.id)

        # Attempt to assign review task to worker already in review hierarchy
        review_task = Task.objects.create(
            project=self.projects['base_test_project'],
            status=Task.Status.PENDING_REVIEW,
            step=self.test_step)
        test_data = {'test_assign': True}
        TaskAssignmentFactory(
            worker=self.workers[1],
            task=review_task,
            status=TaskAssignment.Status.SUBMITTED,
            in_progress_task_data=test_data,
            snapshots=empty_snapshots())

        with self.assertRaises(TaskAssignmentError):
            assign_task(self.workers[1].id, review_task.id)
        self.assertEqual(
            current_assignment(review_task).in_progress_task_data, test_data)

        # Attempt to assign review task to worker not certified for task
        with self.assertRaises(WorkerCertificationError):
            assign_task(self.workers[2].id, review_task.id)
        self.assertEqual(
            current_assignment(review_task).in_progress_task_data, test_data)

        # Assign review task to review worker
        self.assertEquals(review_task.assignments.count(), 1)
        review_task = assign_task(self.workers[3].id, review_task.id)
        self.assertEquals(review_task.assignments.count(), 2)
        self.assertEqual(
            current_assignment(review_task).worker, self.workers[3])
        self.assertEqual(
            current_assignment(review_task).in_progress_task_data, test_data)
        self.assertEquals(
            review_task.status, Task.Status.REVIEWING)
Beispiel #15
0
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
Beispiel #16
0
    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()
Beispiel #17
0
    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])
Beispiel #18
0
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
Beispiel #19
0
    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()