コード例 #1
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)
コード例 #2
0
    def _test_reverted_task(self, task, iteration, num_iterations,
                            task_status, latest_data, expected_audit,
                            revert_before=False):
        response = self._revert_task(
            task, iteration, revert_before=revert_before, commit=False)
        self.assertEqual(response.status_code, 200)
        task.refresh_from_db()
        fake_audit = load_encoded_json(response.content)
        self.assertEqual(fake_audit, expected_audit)

        self.assertEqual(task.status, Task.Status.COMPLETE)
        self.assertEqual(task.assignments.count(), 2)
        for assignment in task.assignments.all():
            self.assertEqual(assignment.iterations.count(), 2)

        response = self._revert_task(
            task, iteration, revert_before=revert_before, commit=True)
        self.assertEqual(response.status_code, 200)
        task.refresh_from_db()
        audit = load_encoded_json(response.content)
        self.assertEqual(audit, fake_audit)
        task.refresh_from_db()
        self.assertEqual(task.status, task_status)
        self.assertEqual(
            get_iteration_history(task).count(), num_iterations)

        verify_iterations(task.id)

        if num_iterations:
            self.assertEqual(
                current_assignment(task).in_progress_task_data,
                latest_data)

        verify_iterations(task.id)
コード例 #3
0
ファイル: iterations.py プロジェクト: jesseflb/orchestra
def _verify_final_iteration(iteration):
    # Last iteration should belong to current assignment
    assignment = iteration.assignment
    assert assignment == current_assignment(assignment.task)

    # Map final iteration statuses onto task statuses
    task_statuses = {
        Iteration.Status.PROCESSING: [
            Task.Status.PROCESSING, Task.Status.REVIEWING,
            Task.Status.POST_REVIEW_PROCESSING
        ],
        Iteration.Status.REQUESTED_REVIEW:
        [Task.Status.PENDING_REVIEW, Task.Status.COMPLETE],
        Iteration.Status.PROVIDED_REVIEW: [Task.Status.POST_REVIEW_PROCESSING]
    }

    # A task awaiting processing should not have iterations
    assignment.task.status != Task.Status.AWAITING_PROCESSING

    for k, v in task_statuses.items():
        # An aborted task could have any iteration configuration
        task_statuses[k].append(Task.Status.ABORTED)

    if iteration.status == Iteration.Status.PROCESSING:
        expected_assignment_status = TaskAssignment.Status.PROCESSING
    else:
        expected_assignment_status = TaskAssignment.Status.SUBMITTED

    # Check that task and assignment statuses are correctly set
    assert assignment.status == expected_assignment_status
    assert assignment.task.status in task_statuses[iteration.status]
コード例 #4
0
ファイル: iterations.py プロジェクト: EM6Holdings/orchestra
def _verify_final_iteration(iteration):
    # Last iteration should belong to current assignment
    assignment = iteration.assignment
    assert assignment == current_assignment(assignment.task)

    # Map final iteration statuses onto task statuses
    task_statuses = {
        Iteration.Status.PROCESSING: [
            Task.Status.PROCESSING, Task.Status.REVIEWING,
            Task.Status.POST_REVIEW_PROCESSING
        ],
        Iteration.Status.REQUESTED_REVIEW: [
            Task.Status.PENDING_REVIEW, Task.Status.COMPLETE
        ],
        Iteration.Status.PROVIDED_REVIEW: [
            Task.Status.POST_REVIEW_PROCESSING
        ]
    }

    # A task awaiting processing should not have iterations
    assignment.task.status != Task.Status.AWAITING_PROCESSING

    for k, v in task_statuses.items():
        # An aborted task could have any iteration configuration
        task_statuses[k].append(Task.Status.ABORTED)

    if iteration.status == Iteration.Status.PROCESSING:
        expected_assignment_status = TaskAssignment.Status.PROCESSING
    else:
        expected_assignment_status = TaskAssignment.Status.SUBMITTED

    # Check that task and assignment statuses are correctly set
    assert assignment.status == expected_assignment_status
    assert assignment.task.status in task_statuses[iteration.status]
コード例 #5
0
ファイル: task_lifecycle.py プロジェクト: cove9988/orchestra
def complete_and_skip_task(task_id):
    """
    Submits a task on behalf of the worker working on it. If the task
    isn't assigned to a worker, marks it and its assignments as
    complete and creates subsequent tasks.

    Args:
        task_id (int):
            The ID of the task to be marked as complete and skipped.

    Returns:
        task (orchestra.models.Task):
            The completed and skipped task.

    """
    task = Task.objects.get(id=task_id)
    assignment = current_assignment(task)
    if assignment and assignment.worker:
        if assignment.status != TaskAssignment.Status.PROCESSING:
            return task
        task_data = assignment.in_progress_task_data or {}
        task_data.update(_orchestra_internal={'complete_and_skip_task': True})
        submit_task(task_id, task_data, Iteration.Status.REQUESTED_REVIEW,
                    assignment.worker)
    else:
        task.status = Task.Status.COMPLETE
        task.save()
        for assignment in task.assignments.all():
            assignment.status = TaskAssignment.Status.SUBMITTED
            assignment.save()
    return task
コード例 #6
0
    def _test_reverted_task(self, task, iteration, num_iterations,
                            task_status, latest_data, expected_audit,
                            revert_before=False):
        response = self._revert_task(
            task, iteration, revert_before=revert_before, commit=False)
        self.assertEquals(response.status_code, 200)
        task.refresh_from_db()
        fake_audit = load_encoded_json(response.content)
        self.assertEqual(fake_audit, expected_audit)

        self.assertEquals(task.status, Task.Status.COMPLETE)
        self.assertEquals(task.assignments.count(), 2)
        for assignment in task.assignments.all():
            self.assertEquals(assignment.iterations.count(), 2)

        response = self._revert_task(
            task, iteration, revert_before=revert_before, commit=True)
        self.assertEqual(response.status_code, 200)
        task.refresh_from_db()
        audit = load_encoded_json(response.content)
        self.assertEqual(audit, fake_audit)
        task.refresh_from_db()
        self.assertEqual(task.status, task_status)
        self.assertEqual(
            get_iteration_history(task).count(), num_iterations)

        verify_iterations(task.id)

        if num_iterations:
            self.assertEquals(
                current_assignment(task).in_progress_task_data,
                latest_data)

        verify_iterations(task.id)
コード例 #7
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 = role_required_for_new_task(task)

    assignment = current_assignment(task)
    if not is_worker_certified_for_task(worker, task, required_role):
        raise WorkerCertificationError('Worker not certified for this task.')
    if task.is_worker_assigned(worker):
        raise TaskAssignmentError('Worker already assigned to this task.')

    assignment_counter = task.assignments.count()
    in_progress_task_data = {}

    if assignment:
        # In-progress task data is the latest
        # submission by a previous worker
        in_progress_task_data = assignment.in_progress_task_data

    previous_status = task.status
    if previous_status == Task.Status.AWAITING_PROCESSING:
        task.status = Task.Status.PROCESSING
    elif previous_status == Task.Status.PENDING_REVIEW:
        task.status = Task.Status.REVIEWING
    task.save()
    assignment = (TaskAssignment.objects.create(
        worker=worker,
        task=task,
        status=TaskAssignment.Status.PROCESSING,
        assignment_counter=assignment_counter,
        in_progress_task_data=in_progress_task_data))

    Iteration.objects.create(assignment=assignment,
                             start_datetime=assignment.start_datetime)

    add_worker_to_project_team(worker, task.project)
    notify_status_change(task, previous_status)
    return task
コード例 #8
0
ファイル: forms.py プロジェクト: dretelny/orchestra
    def __init__(self, *args, **kwargs):
        super(TaskForm, self).__init__(*args, **kwargs)
        workers = Worker.objects.all()
        choices = [(None, None)] + [(w.id, w.user.username) for w in workers]
        self.fields["currently_assigned_to"].choices = choices

        if self._instance_created() and self.instance.assignments.exists():
            assignment = current_assignment(self.instance)
            if assignment.worker:
                # If human task, select human worker
                (self.fields["currently_assigned_to"].initial) = assignment.worker.id
コード例 #9
0
 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))
コード例 #10
0
    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()
コード例 #11
0
ファイル: task_lifecycle.py プロジェクト: vhf/orchestra
def get_new_task_assignment(worker, task_status):
    """
    Check if new task assignment is available for the provided worker
    and task status; if so, assign the task to the worker and return the
    assignment.

    Args:
        worker (orchestra.models.Worker):
            The worker submitting the task.
        task_status (orchestra.models.Task.Status):
            The status of the desired new task assignment.

    Returns:
        assignment (orchestra.models.TaskAssignment):
            The newly created task assignment.

    Raises:
        orchestra.core.errors.WorkerCertificationError:
            No human tasks are available for the given task status
            except those for which the worker is not certified.
        orchestra.core.errors.NoTaskAvailable:
            No human tasks are available for the given task status.
    """
    assert_new_task_status_valid(task_status)
    check_worker_allowed_new_assignment(worker)

    tasks = (Task.objects
             .filter(status=task_status)
             .exclude(assignments__worker=worker)
             .order_by('-project__priority')
             .order_by('project__start_datetime'))

    certification_error = False
    for task in tasks.iterator():
        try:
            task = assign_task(worker.id, task.id)
            return current_assignment(task)
        except WorkerCertificationError:
            certification_error = True
        except ModelSaveError:
            # Machine task cannot have human worker; treat machine tasks as if
            # they do not exist
            pass

    if certification_error:
        raise WorkerCertificationError
    else:
        raise NoTaskAvailable('No task available for {}'.format(worker))
コード例 #12
0
ファイル: task_lifecycle.py プロジェクト: fsnlarson/orchestra
def get_new_task_assignment(worker, task_status):
    """
    Check if new task assignment is available for the provided worker
    and task status; if so, assign the task to the worker and return the
    assignment.

    Args:
        worker (orchestra.models.Worker):
            The worker submitting the task.
        task_status (orchestra.models.Task.Status):
            The status of the desired new task assignment.

    Returns:
        assignment (orchestra.models.TaskAssignment):
            The newly created task assignment.

    Raises:
        orchestra.core.errors.WorkerCertificationError:
            No human tasks are available for the given task status
            except those for which the worker is not certified.
        orchestra.core.errors.NoTaskAvailable:
            No human tasks are available for the given task status.
    """
    assert_new_task_status_valid(task_status)
    check_worker_allowed_new_assignment(worker)

    tasks = (Task.objects
             .filter(status=task_status)
             .exclude(assignments__worker=worker)
             .order_by('-project__priority')
             .order_by('project__start_datetime'))

    certification_error = False
    for task in tasks:
        try:
            task = assign_task(worker.id, task.id)
            return current_assignment(task)
        except WorkerCertificationError:
            certification_error = True
        except ModelSaveError:
            # Machine task cannot have human worker; treat machine tasks as if
            # they do not exist
            pass

    if certification_error:
        raise WorkerCertificationError
    else:
        raise NoTaskAvailable('No task available for {}'.format(worker))
コード例 #13
0
def staff_task(request):
    data = load_encoded_json(request.body)
    errors = {}
    try:
        task = Task.objects.get(id=data.get('task_id'))
        request_cause = StaffBotRequest.RequestCause.USER.value
        bot = StaffBot()
        assignment = current_assignment(task)
        is_restaff = assignment is not None
        if is_restaff:
            username = assignment.worker.user.username
            bot.restaff(task.id, username, request_cause=request_cause)
        else:
            bot.staff(task.id, request_cause=request_cause)
    except Exception as e:
        raise BadRequest(e)
    success = len(errors) == 0
    return {'success': success, 'is_restaff': is_restaff}
コード例 #14
0
ファイル: task_lifecycle.py プロジェクト: nbanta/orchestra
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
コード例 #15
0
ファイル: task_lifecycle.py プロジェクト: vhf/orchestra
def assign_task(worker_id, task_id):
    """
    Return a given task after assigning or reassigning it to the specified
    worker.

    Args:
        worker_id (int):
            The ID of the worker to be assigned.
        task_id (int):
            The ID of the task to be assigned.

    Returns:
        task (orchestra.models.Task):
            The newly assigned task.

    Raises:
        orchestra.core.errors.TaskAssignmentError:
            The specified worker is already assigned to the given task
            or the task status is not compatible with new assignment.
        orchestra.core.errors.WorkerCertificationError:
            The specified worker is not certified for the given task.
    """
    worker = Worker.objects.get(id=worker_id)
    task = Task.objects.get(id=task_id)
    required_role = role_required_for_new_task(task)

    assignment = current_assignment(task)
    if not is_worker_certified_for_task(worker, task, required_role):
        raise WorkerCertificationError('Worker not certified for this task.')
    if task.is_worker_assigned(worker):
        raise TaskAssignmentError('Worker already assigned to this task.')

    assignment_counter = task.assignments.count()
    in_progress_task_data = {}

    if assignment:
        # In-progress task data is the latest
        # submission by a previous worker
        in_progress_task_data = assignment.in_progress_task_data

    previous_status = task.status
    if previous_status == Task.Status.AWAITING_PROCESSING:
        task.status = Task.Status.PROCESSING
    elif previous_status == Task.Status.PENDING_REVIEW:
        task.status = Task.Status.REVIEWING
    task.save()
    assignment = (
        TaskAssignment.objects
        .create(worker=worker,
                task=task,
                status=TaskAssignment.Status.PROCESSING,
                assignment_counter=assignment_counter,
                in_progress_task_data=in_progress_task_data))

    Iteration.objects.create(
        assignment=assignment,
        start_datetime=assignment.start_datetime)

    add_worker_to_project_team(worker, task.project)
    notify_status_change(task, previous_status)
    return task
コード例 #16
0
ファイル: notifications.py プロジェクト: charpty/orchestra
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)
コード例 #17
0
    def test_assign_task(self):
        entry_task = TaskFactory(
            project=self.projects['base_test_project'],
            status=Task.Status.AWAITING_PROCESSING,
            step=self.test_step)

        # No iterations should be present for task
        self.assertEqual(
            Iteration.objects.filter(assignment__task=entry_task).count(), 0)

        # Assign entry-level task to entry-level worker
        entry_task = assign_task(self.workers[0].id, entry_task.id)
        self.assertTrue(entry_task.is_worker_assigned(self.workers[0]))
        self.assertEqual(entry_task.status, Task.Status.PROCESSING)

        self.assertEqual(entry_task.assignments.count(), 1)
        entry_assignment = entry_task.assignments.first()

        # A single iteration was created for the assignment
        self.assertEqual(entry_assignment.iterations.count(), 1)
        self.assertEqual(
            Iteration.objects.filter(assignment__task=entry_task).count(), 1)
        self.assertEqual(
            entry_assignment.iterations.first().start_datetime,
            entry_assignment.start_datetime)

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

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

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

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

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

        # Assign review task to review worker
        self.assertEquals(review_task.assignments.count(), 1)
        review_task = assign_task(self.workers[3].id, review_task.id)
        self.assertEquals(review_task.assignments.count(), 2)

        reviewer_assignment = current_assignment(review_task)
        self.assertEqual(
            reviewer_assignment.worker, self.workers[3])
        self.assertEqual(
            reviewer_assignment.in_progress_task_data, test_data)
        self.assertEquals(
            reviewer_assignment.iterations.count(), 1)
        self.assertEqual(
            reviewer_assignment.iterations.first().start_datetime,
            reviewer_assignment.start_datetime)

        self.assertEquals(
            review_task.status, Task.Status.REVIEWING)
コード例 #18
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])
コード例 #19
0
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)
コード例 #20
0
    def test_assign_task(self):
        entry_task = TaskFactory(project=self.projects['base_test_project'],
                                 status=Task.Status.AWAITING_PROCESSING,
                                 step=self.test_step)

        # No iterations should be present for task
        self.assertEqual(
            Iteration.objects.filter(assignment__task=entry_task).count(), 0)

        # Assign entry-level task to entry-level worker
        entry_task = assign_task(self.workers[0].id, entry_task.id)
        self.assertTrue(entry_task.is_worker_assigned(self.workers[0]))
        self.assertEqual(entry_task.status, Task.Status.PROCESSING)

        self.assertEqual(entry_task.assignments.count(), 1)
        entry_assignment = entry_task.assignments.first()

        # A single iteration was created for the assignment
        self.assertEqual(entry_assignment.iterations.count(), 1)
        self.assertEqual(
            Iteration.objects.filter(assignment__task=entry_task).count(), 1)
        self.assertEqual(entry_assignment.iterations.first().start_datetime,
                         entry_assignment.start_datetime)

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

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

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

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

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

        # Assign review task to review worker
        self.assertEquals(review_task.assignments.count(), 1)
        review_task = assign_task(self.workers[3].id, review_task.id)
        self.assertEquals(review_task.assignments.count(), 2)

        reviewer_assignment = current_assignment(review_task)
        self.assertEqual(reviewer_assignment.worker, self.workers[3])
        self.assertEqual(reviewer_assignment.in_progress_task_data, test_data)
        self.assertEquals(reviewer_assignment.iterations.count(), 1)
        self.assertEqual(reviewer_assignment.iterations.first().start_datetime,
                         reviewer_assignment.start_datetime)

        self.assertEquals(review_task.status, Task.Status.REVIEWING)
コード例 #21
0
ファイル: task_lifecycle.py プロジェクト: nbanta/orchestra
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
コード例 #22
0
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)
コード例 #23
0
ファイル: fixtures.py プロジェクト: ehdr/orchestra
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)
コード例 #24
0
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)
コード例 #25
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)