コード例 #1
0
ファイル: task_lifecycle.py プロジェクト: cove9988/orchestra
def get_task_overview_for_worker(task_id, worker):
    """
    Get information about `task` and its assignment for `worker`.

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

    Returns:
        task_assignment_details (dict):
            Information about `task` and its assignment for `worker`.
    """
    task = Task.objects.get(id=task_id)
    task_details = get_task_details(task_id)
    task_details['is_project_admin'] = worker.is_project_admin()

    if task.is_worker_assigned(worker):
        task_assignment = TaskAssignment.objects.get(worker=worker, task=task)
    elif worker.is_project_admin():
        task_assignment = assignment_history(task).last()
        task_details['is_read_only'] = True
    else:
        raise TaskAssignmentError('Worker is not associated with task')

    task_assignment_details = get_task_assignment_details(task_assignment)
    task_assignment_details.update(task_details)
    return task_assignment_details
コード例 #2
0
ファイル: task_lifecycle.py プロジェクト: cove9988/orchestra
def get_previously_completed_task_data(step, project):
    """
    Returns a dict mapping task prerequisites onto their
    latest task assignment information.  The dict is of the form:
    {'previous-slug': {latest_assignment_data}, ...}

    Args:
        step (orchestra.models.Step): The specified step object.
        project (orchestra.models.Project): The specified project object.
    Returns:
        prerequisites (dict):
            A dict mapping task prerequisites onto their latest task
            assignment information.
    """
    # Find all prerequisite steps in graph
    to_visit = list(step.creation_depends_on.all())
    prerequisite_steps = set(to_visit)
    while to_visit:
        current_step = to_visit.pop()
        for step in current_step.creation_depends_on.all():
            if step not in prerequisite_steps:
                to_visit.append(step)
                prerequisite_steps.add(step)

    prerequisite_data = {}
    for step in prerequisite_steps:
        required_task = Task.objects.get(step=step, project=project)

        if required_task.status != Task.Status.COMPLETE:
            raise TaskDependencyError('Task depenency is not satisfied')
        assignment = assignment_history(required_task).last()
        prerequisite_data[step.slug] = assignment.in_progress_task_data
    return prerequisite_data
コード例 #3
0
def previously_completed_task_data(task):
    """
    Returns a dict mapping task prerequisites onto their
    latest task assignment information.  The dict is of the form:
    {'previous-slug': {task_assignment_data}, ...}

    Args:
        task (orchestra.models.Task): The specified task object.

    Returns:
        prerequisites (dict):
            A dict mapping task prerequisites onto their latest task
            assignment information.
    """
    step = task.step
    prerequisites = {}

    for required_step in step.creation_depends_on.all():
        required_task = Task.objects.get(step=required_step,
                                         project=task.project)
        if required_task.status != Task.Status.COMPLETE:
            raise TaskDependencyError('Task depenency is not satisfied')

        task_details = get_task_details(required_task.id)

        assignment = assignment_history(required_task).last()
        task_assignment_details = {}
        if assignment:
            # Task assignment should be present unless task was skipped.
            task_assignment_details = get_task_assignment_details(assignment)
        task_assignment_details.update(task_details)

        # TODO(kkamalov): check for circular prerequisites
        prerequisites[required_step.slug] = task_assignment_details
    return prerequisites
コード例 #4
0
ファイル: task_lifecycle.py プロジェクト: skamille/orchestra
def previously_completed_task_data(task):
    """
    Returns a dict mapping task prerequisites onto their
    latest task assignment information.  The dict is of the form:
    {'previous-slug': {task_assignment_data}, ...}

    Args:
        task (orchestra.models.Task): The specified task object.

    Returns:
        prerequisites (dict):
            A dict mapping task prerequisites onto their latest task
            assignment information.
    """
    step = task.step
    prerequisites = {}

    for required_step in step.creation_depends_on.all():
        required_task = Task.objects.get(step=required_step,
                                         project=task.project)
        if required_task.status != Task.Status.COMPLETE:
            raise TaskDependencyError('Task depenency is not satisfied')

        task_details = get_task_details(required_task.id)

        assignment = assignment_history(required_task).last()
        task_assignment_details = {}
        if assignment:
            # Task assignment should be present unless task was skipped.
            task_assignment_details = get_task_assignment_details(assignment)
        task_assignment_details.update(task_details)

        # TODO(kkamalov): check for circular prerequisites
        prerequisites[required_step.slug] = task_assignment_details
    return prerequisites
コード例 #5
0
ファイル: task_lifecycle.py プロジェクト: vhf/orchestra
def previously_completed_task_data(task):
    """
    Returns a dict mapping task prerequisites onto their
    latest task assignment information.  The dict is of the form:
    {'previous-slug': {latest_assignment_data}, ...}

    Args:
        task (orchestra.models.Task): The specified task object.

    Returns:
        prerequisites (dict):
            A dict mapping task prerequisites onto their latest task
            assignment information.
    """
    # Find all prerequisite steps in graph
    to_visit = list(task.step.creation_depends_on.all())
    prerequisite_steps = set(to_visit)
    while to_visit:
        current_step = to_visit.pop()
        for step in current_step.creation_depends_on.all():
            if step not in prerequisite_steps:
                to_visit.append(step)
                prerequisite_steps.add(step)

    prerequisite_data = {}
    for step in prerequisite_steps:
        required_task = Task.objects.get(step=step, project=task.project)

        if required_task.status != Task.Status.COMPLETE:
            raise TaskDependencyError('Task depenency is not satisfied')
        assignment = assignment_history(required_task).last()
        prerequisite_data[step.slug] = assignment.in_progress_task_data
    return prerequisite_data
コード例 #6
0
 def _expected_audit(self,
                     complete_task,
                     reverted_status=None,
                     assignment_changes=None,
                     iteration_changes=None):
     assignments = assignment_history(complete_task)
     iterations = get_iteration_history(complete_task)
     audit = {
         'task':
         TaskSerializer(complete_task).data,
         'assignments': [{
             'assignment':
             (TaskAssignmentSerializer(assignments.first()).data),
             'change':
             0,
             'iterations': [{
                 'change':
                 0,
                 'iteration':
                 (IterationSerializer(iterations.all()[0]).data)
             }, {
                 'change':
                 0,
                 'iteration':
                 (IterationSerializer(iterations.all()[2]).data)
             }]
         }, {
             'assignment':
             (TaskAssignmentSerializer(assignments.last()).data),
             'change':
             0,
             'iterations': [{
                 'change':
                 0,
                 'iteration':
                 (IterationSerializer(iterations.all()[1]).data)
             }, {
                 'change':
                 0,
                 'iteration':
                 (IterationSerializer(iterations.all()[3]).data)
             }]
         }],
     }
     if reverted_status is not None:
         audit['reverted_status'] = reverted_status
     if assignment_changes is not None:
         for i, assignment_change in enumerate(assignment_changes):
             audit['assignments'][i]['change'] = assignment_change
     if iteration_changes is not None:
         for i, changes_per_assignment in enumerate(iteration_changes):
             for j, iteration_change in enumerate(changes_per_assignment):
                 audit['assignments'][i]['iterations'][j][
                     'change'] = iteration_change
     return audit
コード例 #7
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))
コード例 #8
0
 def _expected_audit(self, complete_task, reverted_status=None,
                     assignment_changes=None, iteration_changes=None):
     assignments = assignment_history(complete_task)
     iterations = get_iteration_history(complete_task)
     audit = {
         'task': TaskSerializer(complete_task).data,
         'assignments': [
             {
                 'assignment': (
                     TaskAssignmentSerializer(assignments.first()).data),
                 'change': 0,
                 'iterations': [
                     {
                         'change': 0,
                         'iteration': (
                             IterationSerializer(iterations.all()[0]).data)
                     },
                     {
                         'change': 0,
                         'iteration': (
                             IterationSerializer(iterations.all()[2]).data)
                     }
                 ]
             },
             {
                 'assignment': (
                     TaskAssignmentSerializer(assignments.last()).data),
                 'change': 0,
                 'iterations': [
                     {
                         'change': 0,
                         'iteration': (
                             IterationSerializer(iterations.all()[1]).data)
                     },
                     {
                         'change': 0,
                         'iteration': (
                             IterationSerializer(iterations.all()[3]).data)
                     }
                 ]
             }
         ],
     }
     if reverted_status is not None:
         audit['reverted_status'] = reverted_status
     if assignment_changes is not None:
         for i, assignment_change in enumerate(assignment_changes):
             audit['assignments'][i]['change'] = assignment_change
     if iteration_changes is not None:
         for i, changes_per_assignment in enumerate(iteration_changes):
             for j, iteration_change in enumerate(changes_per_assignment):
                 audit['assignments'][i][
                     'iterations'][j]['change'] = iteration_change
     return audit
コード例 #9
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()
コード例 #10
0
ファイル: fixtures.py プロジェクト: GJdan/orchestra
def setup_task_history(test):
    task = test.tasks['rejected_review']

    test._submit_assignment(
        test.clients[6], task.id, seconds=35)
    test._submit_assignment(
        test.clients[7], task.id, command='reject', seconds=36)
    test._submit_assignment(
        test.clients[6], task.id, seconds=37)
    test._submit_assignment(
        test.clients[7], task.id, command='accept', seconds=38)

    # Fill out the snapshots for all assignments
    assignments = assignment_history(task)
    first_assignment = assignments[0]
    second_assignment = assignments[1]
    third_assignment = assignments[2]

    first_assignment.snapshots['snapshots'] = deepcopy(
        second_assignment.snapshots['snapshots'])
    second_assignment.snapshots['snapshots'] = (
        deepcopy(third_assignment.snapshots['snapshots']) +
        second_assignment.snapshots['snapshots'])[:-1]

    def fix_datetimes(snapshots, new_datetimes):
        for snapshot, new_datetime in zip(snapshots, new_datetimes):
            snapshot['datetime'] = new_datetime

    # Explicitly set the iteration datetimes.  If we didn't, the timestamps
    # would be `datetime.now`, which we can't test against.  The explicitly set
    # times are predictable distance apart, so we can test the
    # resulting latency reports.
    fix_datetimes(
        first_assignment.snapshots['snapshots'],
        ['2015-10-12T02:02:00+00:00', '2015-10-12T03:05:00+00:00'])
    fix_datetimes(
        second_assignment.snapshots['snapshots'],
        ['2015-10-12T03:01:00+00:00', '2015-10-12T03:07:00+00:00',
         '2015-10-12T04:03:00+00:00', '2015-10-12T04:10:00+00:00'])
    fix_datetimes(
        third_assignment.snapshots['snapshots'],
        ['2015-10-12T04:02:00+00:00', '2015-10-12T04:13:00+00:00'])

    first_assignment.save()
    second_assignment.save()
    third_assignment.save()

    return task
コード例 #11
0
ファイル: fixtures.py プロジェクト: skamille/orchestra
def setup_task_history(test):
    task = test.tasks['rejected_review']

    test._submit_assignment(test.clients[6], task.id, seconds=35)
    test._submit_assignment(test.clients[7],
                            task.id,
                            command='reject',
                            seconds=36)
    test._submit_assignment(test.clients[6], task.id, seconds=37)
    test._submit_assignment(test.clients[7],
                            task.id,
                            command='accept',
                            seconds=38)

    # Fill out the snapshots for all assignments
    assignments = assignment_history(task)
    first_assignment = assignments[0]
    second_assignment = assignments[1]
    third_assignment = assignments[2]

    first_assignment.snapshots['snapshots'] = deepcopy(
        second_assignment.snapshots['snapshots'])
    second_assignment.snapshots['snapshots'] = (
        deepcopy(third_assignment.snapshots['snapshots']) +
        second_assignment.snapshots['snapshots'])[:-1]

    def fix_datetimes(snapshots, new_datetimes):
        for snapshot, new_datetime in zip(snapshots, new_datetimes):
            snapshot['datetime'] = new_datetime

    # Explicitly set the iteration datetimes.  If we didn't, the timestamps
    # would be `datetime.now`, which we can't test against.  The explicitly set
    # times are predictable distance apart, so we can test the
    # resulting latency reports.
    fix_datetimes(first_assignment.snapshots['snapshots'],
                  ['2015-10-12T02:02:00+00:00', '2015-10-12T03:05:00+00:00'])
    fix_datetimes(second_assignment.snapshots['snapshots'], [
        '2015-10-12T03:01:00+00:00', '2015-10-12T03:07:00+00:00',
        '2015-10-12T04:03:00+00:00', '2015-10-12T04:10:00+00:00'
    ])
    fix_datetimes(third_assignment.snapshots['snapshots'],
                  ['2015-10-12T04:02:00+00:00', '2015-10-12T04:13:00+00:00'])

    first_assignment.save()
    second_assignment.save()
    third_assignment.save()

    return task
コード例 #12
0
def previously_completed_steps(task, related_steps, **kwargs):
    """
    Assign a new task to the entry-level worker of the specified tasks.
    If no worker can be assigned, return the unmodified task.

    Args:
        task (orchestra.models.Task):
            The newly created task to assign.
        related_steps ([str]):
            List of step slugs from which to attempt to assign a worker.

    Returns:
        task (orchestra.models.Task): The modified task object.

    Raises:
        orchestra.core.errors.AssignmentPolicyError:
            Machine steps cannot be included in an assignment policy.
    """
    if related_steps is None:
        raise AssignmentPolicyError('No related steps given')

    workflow_version = task.step.workflow_version
    for step_slug in related_steps:
        step = workflow_version.steps.get(slug=step_slug)
        if not step.is_human:
            raise AssignmentPolicyError('Machine step should not be '
                                        'member of assignment policy')
    related_tasks = (Task.objects.filter(
        step__slug__in=related_steps,
        project=task.project).select_related('step'))
    for related_task in related_tasks:
        entry_level_assignment = assignment_history(related_task).first()
        if entry_level_assignment and entry_level_assignment.worker:
            try:
                return assign_task(entry_level_assignment.worker.id, task.id)
            except WorkerCertificationError:
                # Task could not be assigned to related worker, try with
                # another related worker
                logger.warning(
                    'Tried to assign worker %s to step %s, for '
                    'which they are not certified',
                    entry_level_assignment.worker.id,
                    task.step.slug,
                    exc_info=True)
            except Exception:
                logger.warning('Unable to assign task.', exc_info=True)
    return task
コード例 #13
0
    def test_invalid_revert_before(self):
        task = setup_complete_task(self)
        task.status = Task.Status.ABORTED
        task.save()

        # Ensure reviewer assignment has more than one iteration
        reviewer_assignment = assignment_history(task).last()
        self.assertGreater(reviewer_assignment.iterations.count(), 1)

        # Attempt to revert before an iteration that isn't the assignment's
        # first
        with self.assertRaises(InvalidRevertError):
            self._revert_task(
                task, get_latest_iteration(reviewer_assignment),
                revert_before=True, commit=True)

        task.delete()
コード例 #14
0
ファイル: revert.py プロジェクト: jesseflb/orchestra
def _build_revert_audit(task, revert_iteration, revert_before):
    if task.status == Task.Status.ABORTED:
        raise InvalidRevertError('Cannot revert aborted task.')

    task_audit = {
        'task': TaskSerializer(task).data,
        'assignments': [],
    }

    for assignment in assignment_history(task).all():
        assignment_audit = (_build_assignment_revert_audit(
            assignment, revert_iteration, revert_before))
        task_audit['assignments'].append(assignment_audit)

    task_audit['reverted_status'] = _reverted_task_status(
        task_audit, revert_before)
    return task_audit
コード例 #15
0
    def test_invalid_revert_before(self):
        task = setup_complete_task(self)
        task.status = Task.Status.ABORTED
        task.save()

        # Ensure reviewer assignment has more than one iteration
        reviewer_assignment = assignment_history(task).last()
        self.assertGreater(reviewer_assignment.iterations.count(), 1)

        # Attempt to revert before an iteration that isn't the assignment's
        # first
        with self.assertRaises(InvalidRevertError):
            self._revert_task(
                task, get_latest_iteration(reviewer_assignment),
                revert_before=True, commit=True)

        task.delete()
コード例 #16
0
ファイル: task_lifecycle.py プロジェクト: vhf/orchestra
def end_project(project_id):
    """
    Mark the specified project and its component tasks as aborted.

    Args:
        project_id (int): The ID of the project to abort.

    Returns:
        None
    """
    project = Project.objects.get(id=project_id)
    project.status = Project.Status.ABORTED
    project.save()
    for task in project.tasks.all():
        task.status = Task.Status.ABORTED
        task.save()
        notify_status_change(task, assignment_history(task))
コード例 #17
0
def previously_completed_steps(task, related_steps, **kwargs):
    """
    Assign a new task to the entry-level worker of the specified tasks.
    If no worker can be assigned, return the unmodified task.

    Args:
        task (orchestra.models.Task):
            The newly created task to assign.
        related_steps ([str]):
            List of step slugs from which to attempt to assign a worker.

    Returns:
        task (orchestra.models.Task): The modified task object.

    Raises:
        orchestra.core.errors.AssignmentPolicyError:
            Machine steps cannot be included in an assignment policy.
    """
    if related_steps is None:
        raise AssignmentPolicyError('No related steps given')

    workflow_version = task.step.workflow_version
    for step_slug in related_steps:
        step = workflow_version.steps.get(slug=step_slug)
        if not step.is_human:
            raise AssignmentPolicyError('Machine step should not be '
                                        'member of assignment policy')
    related_tasks = (
        Task.objects
        .filter(step__slug__in=related_steps, project=task.project)
        .select_related('step'))
    for related_task in related_tasks:
        entry_level_assignment = assignment_history(related_task).first()
        if entry_level_assignment and entry_level_assignment.worker:
            try:
                return assign_task(entry_level_assignment.worker.id, task.id)
            except WorkerCertificationError:
                # Task could not be assigned to related worker, try with
                # another related worker
                logger.warning('Tried to assign worker %s to step %s, for '
                               'which they are not certified',
                               entry_level_assignment.worker.id,
                               task.step.slug, exc_info=True)
            except Exception:
                logger.warning('Unable to assign task.', exc_info=True)
    return task
コード例 #18
0
def end_project(project_id):
    """
    Mark the specified project and its component tasks as aborted.

    Args:
        project_id (int): The ID of the project to abort.

    Returns:
        None
    """
    project = Project.objects.get(id=project_id)
    project.status = Project.Status.ABORTED
    project.save()
    for task in project.tasks.all():
        task.status = Task.Status.ABORTED
        task.save()
        notify_status_change(task, assignment_history(task))
コード例 #19
0
ファイル: revert.py プロジェクト: EM6Holdings/orchestra
def _build_revert_audit(task, revert_iteration, revert_before):
    if task.status == Task.Status.ABORTED:
        raise InvalidRevertError('Cannot revert aborted task.')

    task_audit = {
        'task': TaskSerializer(task).data,
        'assignments': [],
    }

    for assignment in assignment_history(task).all():
        assignment_audit = (
            _build_assignment_revert_audit(
                assignment, revert_iteration, revert_before))
        task_audit['assignments'].append(assignment_audit)

    task_audit['reverted_status'] = _reverted_task_status(
        task_audit, revert_before)
    return task_audit
コード例 #20
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
コード例 #21
0
ファイル: task_lifecycle.py プロジェクト: nbanta/orchestra
def _assign_worker_from_previously_completed_steps(task, related_steps):
    """
    Assign a new task to the entry-level worker of the specified tasks.
    If no worker can be assigned, return the unmodified task.

    Args:
        task (orchestra.models.Task):
            The newly created task to assign.
        related_steps ([orchestra.workflow.steps]):
            List of steps from which to attempt to assign a worker.

    Returns:
        task (orchestra.models.Task): The modified task object.

    Raises:
        orchestra.core.errors.AssignmentPolicyError:
            Machine steps cannot be included in an assignment policy.
    """
    workflow = get_workflow_by_slug(task.project.workflow_slug)
    for slug in related_steps:
        if workflow.get_step(slug).worker_type == Step.WorkerType.MACHINE:
            raise AssignmentPolicyError('Machine step should not be '
                                        'member of assignment policy')
    related_tasks = Task.objects.filter(step_slug__in=related_steps,
                                        project=task.project)
    for related_task in related_tasks:
        entry_level_assignment = assignment_history(related_task).first()
        if entry_level_assignment and entry_level_assignment.worker:
            try:
                return assign_task(entry_level_assignment.worker.id, task.id)
            except:
                # Task could not be assigned to related worker, try with
                # another related worker
                logger.warning('Tried to assign worker %s to step %s, for '
                               'which they are not certified',
                               entry_level_assignment.worker.id,
                               task.step_slug, exc_info=True)
    return task
コード例 #22
0
def setup_complete_task(test_case):
    # Microseconds are truncated when manually saving models
    test_start = timezone.now().replace(microsecond=0)
    times = {
        'awaiting_pickup': test_start,
        'entry_pickup': test_start + timedelta(hours=1),
        'entry_submit': test_start + timedelta(hours=2),
        'reviewer_pickup': test_start + timedelta(hours=3),
        'reviewer_reject': test_start + timedelta(hours=4),
        'entry_resubmit': test_start + timedelta(hours=5),
        'reviewer_accept': test_start + timedelta(hours=6),
    }

    task = TaskFactory(
        project=test_case.projects['empty_project'],
        status=Task.Status.AWAITING_PROCESSING,
        step=test_case.test_step,
        start_datetime=times['awaiting_pickup'])

    workers = {
        'entry': test_case.workers[0],
        'reviewer': test_case.workers[1]
    }

    assign_task(workers['entry'].id, task.id)

    task.refresh_from_db()
    test_case.assertEqual(task.status, Task.Status.PROCESSING)

    submit_task(
        task.id, {'test': 'entry_submit'},
        Iteration.Status.REQUESTED_REVIEW,
        workers['entry'])

    task.refresh_from_db()
    test_case.assertEqual(task.status, Task.Status.PENDING_REVIEW)

    assign_task(workers['reviewer'].id, task.id)
    reviewer_assignment = task.assignments.get(
        worker=workers['reviewer'])

    # Modify assignment with correct datetime
    reviewer_assignment.start_datetime = times['reviewer_pickup']
    reviewer_assignment.save()

    task.refresh_from_db()
    test_case.assertEqual(task.status, Task.Status.REVIEWING)

    submit_task(
        task.id, {'test': 'reviewer_reject'},
        Iteration.Status.PROVIDED_REVIEW,
        workers['reviewer'])

    task.refresh_from_db()
    test_case.assertEqual(task.status, Task.Status.POST_REVIEW_PROCESSING)

    submit_task(
        task.id, {'test': 'entry_resubmit'},
        Iteration.Status.REQUESTED_REVIEW,
        workers['entry'])

    task.refresh_from_db()
    test_case.assertEqual(task.status, Task.Status.REVIEWING)

    with patch('orchestra.utils.task_lifecycle._is_review_needed',
               return_value=False):
        submit_task(
            task.id, {'test': 'reviewer_accept'},
            Iteration.Status.REQUESTED_REVIEW,
            workers['reviewer'])

    task.refresh_from_db()
    test_case.assertEqual(task.status, Task.Status.COMPLETE)
    test_case.assertEqual(task.assignments.count(), 2)
    for assignment in task.assignments.all():
        test_case.assertEqual(
            assignment.status, TaskAssignment.Status.SUBMITTED)
        test_case.assertEqual(assignment.iterations.count(), 2)

    # Modify assignments with correct datetime
    new_datetime_labels = ('entry_pickup', 'reviewer_pickup')
    for i, assignment in enumerate(assignment_history(task).all()):
        assignment.start_datetime = times[new_datetime_labels[i]]
        assignment.save()

    # Modify iterations with correct datetime
    new_datetime_labels = (
        ('entry_pickup', 'entry_submit'),
        ('reviewer_pickup', 'reviewer_reject'),
        ('reviewer_reject', 'entry_resubmit'),
        ('entry_resubmit', 'reviewer_accept')
    )
    new_datetimes = [
        (times[start_label], times[end_label])
        for start_label, end_label in new_datetime_labels]

    for i, iteration in enumerate(get_iteration_history(task)):
        iteration.start_datetime, iteration.end_datetime = new_datetimes[i]
        iteration.save()

    verify_iterations(task.id)

    return task
コード例 #23
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)
コード例 #24
0
ファイル: task_lifecycle.py プロジェクト: skamille/orchestra
def _revert_task_status(task):
    """
    Reverts the status of an otherwise-reverted task.

    Args:
        task (orchestra.models.Task):
            The task with status to revert.

    Returns:
        task (orchestra.models.Task):
            The task with reverted status.
    """
    assignments = assignment_history(task)
    num_assignments = assignments.count()

    reverted_status = None
    current_assignment_counter = None
    previous_assignment_counter = None
    if num_assignments == 0:
        # No assignment is present
        reverted_status = Task.Status.AWAITING_PROCESSING
    else:
        assignment = last_snapshotted_assignment(task.id)
        if not assignment:
            # Task has an assignment but no snapshots
            reverted_status = Task.Status.PROCESSING
            current_assignment_counter = 0
        else:
            latest_counter = assignment.assignment_counter
            snapshot = assignment.snapshots['snapshots'][-1]
            if snapshot['type'] == TaskAssignment.SnapshotType.REJECT:
                # Task was last rejected back to previous worker
                reverted_status = Task.Status.POST_REVIEW_PROCESSING
                current_assignment_counter = latest_counter - 1
                previous_assignment_counter = latest_counter

            elif (snapshot['type'] in (TaskAssignment.SnapshotType.SUBMIT,
                                       TaskAssignment.SnapshotType.ACCEPT)):
                if latest_counter == num_assignments - 1:
                    # Task was last submitted and no higher-level
                    # assignments are present (reverted tasks will never end
                    # in a completed state)
                    reverted_status = Task.Status.PENDING_REVIEW
                else:
                    reverted_status = Task.Status.REVIEWING
                    previous_assignment_counter = latest_counter
                    current_assignment_counter = latest_counter + 1

    if current_assignment_counter is not None:
        current_assignment = task.assignments.get(
            assignment_counter=current_assignment_counter)
        current_assignment.status = TaskAssignment.Status.PROCESSING
        current_assignment.save()

    if previous_assignment_counter is not None:
        previous_assignment = task.assignments.get(
            assignment_counter=previous_assignment_counter)
        previous_assignment.status = TaskAssignment.Status.SUBMITTED
        previous_assignment.save()

    # TODO(jrbotros): The revert methos should "peel off" snapshots,
    # rather than deleting them in bulk and recalculating the assignment
    # and task statuses; this logic needs to be cleaned up.
    for assignment in task.assignments.all():
        if assignment.status == TaskAssignment.Status.SUBMITTED:
            latest_snapshot = assignment.snapshots['snapshots'][-1]
            assignment.in_progress_task_data = latest_snapshot['data']
            assignment.save()

    task.status = reverted_status
    task.save()
    return task
コード例 #25
0
ファイル: test_admin.py プロジェクト: dretelny/orchestra
    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()
コード例 #26
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)
コード例 #27
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)
コード例 #28
0
ファイル: test_admin.py プロジェクト: nbanta/orchestra
    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()
コード例 #29
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)
コード例 #30
0
def revert_task_to_datetime(task_id, revert_datetime, fake=False):
    """
    Reverts a task to its state immediately before the specified
    datetime.

    Args:
        task_id (int):
            The ID of the task to be reverted.
        revert_datetime (datetime.datetime):
            The datetime before which to revert the task.
        fake (bool):
            Determines whether the revert is actually carried out;
            otherwise, an audit trail is passively generated.

    Returns:
        audit (dict):
            An audit trail for the revert, e.g.,

            {
                'task': {...},
                'change': 'reverted'
                'assignments': [
                    {
                        # worker_0 assignment
                        'assignment': {...},
                        'change': 'reverted',
                        'snapshots': [
                            {
                                'snapshot': {...},
                                'change': 'unchanged'
                            }
                            {
                                'snapshot': {...},
                                'change': 'deleted'
                            },
                        ]
                    },
                    {
                        # worker_1 assignment
                        'assignment': {...},
                        'change': 'deleted',
                        'snapshots': [
                            {
                                'snapshot': {...},
                                'change': 'deleted'
                            },
                            {
                                'snapshot': {...},
                                'change': 'deleted'
                            }
                        ]
                    }
                ],
            }
    """
    task = Task.objects.get(id=task_id)
    audit = {
        'task': TaskSerializer(task).data,
        'change': 'unchanged',
        'assignments': []
    }
    reverted = False

    for assignment in assignment_history(task).all():
        assignment_audit = _revert_assignment_to_datetime(
            assignment, revert_datetime, fake)
        audit['assignments'].append(assignment_audit)
        if assignment_audit['change'] != 'unchanged':
            reverted = True

    # Delete task if it starts after revert_datetime
    if task.start_datetime >= revert_datetime:
        audit['change'] = 'deleted'
        if not fake:
            task.delete()
    elif reverted:
        audit['change'] = 'reverted'
        if not fake:
            _revert_task_status(task)

    return audit
コード例 #31
0
def _revert_task_status(task):
    """
    Reverts the status of an otherwise-reverted task.

    Args:
        task (orchestra.models.Task):
            The task with status to revert.

    Returns:
        task (orchestra.models.Task):
            The task with reverted status.
    """
    assignments = assignment_history(task)
    num_assignments = assignments.count()

    reverted_status = None
    current_assignment_counter = None
    previous_assignment_counter = None
    if num_assignments == 0:
        # No assignment is present
        reverted_status = Task.Status.AWAITING_PROCESSING
    else:
        assignment = last_snapshotted_assignment(task.id)
        if not assignment:
            # Task has an assignment but no snapshots
            reverted_status = Task.Status.PROCESSING
            current_assignment_counter = 0
        else:
            latest_counter = assignment.assignment_counter
            snapshot = assignment.snapshots['snapshots'][-1]
            if snapshot['type'] == TaskAssignment.SnapshotType.REJECT:
                # Task was last rejected back to previous worker
                reverted_status = Task.Status.POST_REVIEW_PROCESSING
                current_assignment_counter = latest_counter - 1
                previous_assignment_counter = latest_counter

            elif (snapshot['type'] in (TaskAssignment.SnapshotType.SUBMIT,
                                       TaskAssignment.SnapshotType.ACCEPT)):
                if latest_counter == num_assignments - 1:
                    # Task was last submitted and no higher-level
                    # assignments are present (reverted tasks will never end
                    # in a completed state)
                    reverted_status = Task.Status.PENDING_REVIEW
                else:
                    reverted_status = Task.Status.REVIEWING
                    previous_assignment_counter = latest_counter
                    current_assignment_counter = latest_counter + 1

    if current_assignment_counter is not None:
        current_assignment = task.assignments.get(
            assignment_counter=current_assignment_counter)
        current_assignment.status = TaskAssignment.Status.PROCESSING
        current_assignment.save()

    if previous_assignment_counter is not None:
        previous_assignment = task.assignments.get(
            assignment_counter=previous_assignment_counter)
        previous_assignment.status = TaskAssignment.Status.SUBMITTED
        previous_assignment.save()

    # TODO(jrbotros): The revert methos should "peel off" snapshots,
    # rather than deleting them in bulk and recalculating the assignment
    # and task statuses; this logic needs to be cleaned up.
    for assignment in task.assignments.all():
        if assignment.status == TaskAssignment.Status.SUBMITTED:
            latest_snapshot = assignment.snapshots['snapshots'][-1]
            assignment.in_progress_task_data = latest_snapshot['data']
            assignment.save()

    task.status = reverted_status
    task.save()
    return task
コード例 #32
0
    def test_project_information_api(self):
        project = self.projects['project_management_project']
        response = self.api_client.post(
            reverse(
                'orchestra:orchestra:project_management:'
                'project_information'),
            json.dumps({
                'project_id': project.id,
            }),
            content_type='application/json')
        self.assertEquals(response.status_code, 200)
        returned = load_encoded_json(response.content)

        # Skipping the `tasks` key/value pair for this test
        expected_project = {
            'task_class': project.task_class,
            'start_datetime': '2015-10-12T00:00:00Z',
            'team_messages_url': None,
            'admin_url': settings.ORCHESTRA_URL + reverse(
                'admin:orchestra_project_change',
                args=(project.id,)),
            'priority': project.priority,
            'project_data': project.project_data,
            'short_description': project.short_description,
            'id': project.id,
            'workflow_slug': project.workflow_version.workflow.slug
        }
        self.assertEquals(
            expected_project,
            {k: returned['project'][k] for k in expected_project.keys()})

        sample_task = returned['tasks']['step1']
        task = project.tasks.get(step__slug='step1')
        expected_task = {
            'status': 'Post-review Processing',
            'start_datetime': '2015-10-12T01:00:00Z',
            'admin_url': settings.ORCHESTRA_URL + reverse(
                'admin:orchestra_task_change',
                args=(task.id,)),
            'latest_data': {
                'test_key': 'test_value'
            },
            'project': task.project.id,
            'step_slug': 'step1',
            'id': task.id
        }

        self.assertEquals(
            expected_task,
            {k: sample_task[k] for k in expected_task.keys()})

        sample_assignment = sample_task['assignments'][0]
        assignment = assignment_history(task)[0]
        expected_assignment = {
            'status': 'Submitted',
            'start_datetime': '2015-10-12T02:00:00Z',
            'task': assignment.task.id,
            'admin_url': settings.ORCHESTRA_URL + reverse(
                'admin:orchestra_taskassignment_change',
                args=(assignment.id,)),
            'worker': {
                'username': assignment.worker.user.username,
                'first_name': assignment.worker.user.first_name,
                'last_name': assignment.worker.user.last_name,
                'id': assignment.worker.id,
            },
            'iterations': [
                {
                    'id': assignment.iterations.first().id,
                    'admin_url': settings.ORCHESTRA_URL + reverse(
                        'admin:orchestra_iteration_change',
                        args=(assignment.iterations.first().id,)),
                    'assignment': assignment.id,
                    'start_datetime': '2015-10-12T02:00:00Z',
                    'end_datetime': '2015-10-12T03:00:00Z',
                    'status': 'Requested Review',
                    'submitted_data': {}
                }
            ],
            'in_progress_task_data': {},
            'id': assignment.id
        }

        self.assertEquals(
            expected_assignment,
            {k: sample_assignment[k] for k in expected_assignment.keys()})
コード例 #33
0
    def test_project_information_api(self):
        project = self.projects['project_management_project']
        response = self.api_client.post(reverse(
            'orchestra:orchestra:project_management:'
            'project_information'),
                                        json.dumps({
                                            'project_id': project.id,
                                        }),
                                        content_type='application/json')
        self.assertEquals(response.status_code, 200)
        returned = load_encoded_json(response.content)

        # Skipping the `tasks` key/value pair for this test
        expected_project = {
            'task_class':
            project.task_class,
            'start_datetime':
            '2015-10-12T00:00:00Z',
            'team_messages_url':
            None,
            'admin_url':
            settings.ORCHESTRA_URL +
            reverse('admin:orchestra_project_change', args=(project.id, )),
            'priority':
            project.priority,
            'project_data':
            project.project_data,
            'short_description':
            project.short_description,
            'id':
            project.id,
            'workflow_slug':
            project.workflow_version.workflow.slug
        }
        self.assertEquals(
            expected_project,
            {k: returned['project'][k]
             for k in expected_project.keys()})

        sample_task = returned['tasks']['step1']
        task = project.tasks.get(step__slug='step1')
        expected_task = {
            'status':
            'Post-review Processing',
            'start_datetime':
            '2015-10-12T01:00:00Z',
            'admin_url':
            settings.ORCHESTRA_URL +
            reverse('admin:orchestra_task_change', args=(task.id, )),
            'latest_data': {
                'test_key': 'test_value'
            },
            'project':
            task.project.id,
            'step_slug':
            'step1',
            'id':
            task.id
        }

        self.assertEquals(expected_task,
                          {k: sample_task[k]
                           for k in expected_task.keys()})

        sample_assignment = sample_task['assignments'][0]
        assignment = assignment_history(task)[0]
        expected_assignment = {
            'status':
            'Submitted',
            'start_datetime':
            '2015-10-12T02:00:00Z',
            'task':
            assignment.task.id,
            'admin_url':
            settings.ORCHESTRA_URL +
            reverse('admin:orchestra_taskassignment_change',
                    args=(assignment.id, )),
            'worker': {
                'username': assignment.worker.user.username,
                'first_name': assignment.worker.user.first_name,
                'last_name': assignment.worker.user.last_name,
                'id': assignment.worker.id,
            },
            'iterations': [{
                'id':
                assignment.iterations.first().id,
                'admin_url':
                settings.ORCHESTRA_URL +
                reverse('admin:orchestra_iteration_change',
                        args=(assignment.iterations.first().id, )),
                'assignment':
                assignment.id,
                'start_datetime':
                '2015-10-12T02:00:00Z',
                'end_datetime':
                '2015-10-12T03:00:00Z',
                'status':
                'Requested Review',
                'submitted_data': {}
            }],
            'in_progress_task_data': {},
            'id':
            assignment.id
        }

        self.assertEquals(
            expected_assignment,
            {k: sample_assignment[k]
             for k in expected_assignment.keys()})
コード例 #34
0
ファイル: task_lifecycle.py プロジェクト: skamille/orchestra
def revert_task_to_datetime(task_id, revert_datetime, fake=False):
    """
    Reverts a task to its state immediately before the specified
    datetime.

    Args:
        task_id (int):
            The ID of the task to be reverted.
        revert_datetime (datetime.datetime):
            The datetime before which to revert the task.
        fake (bool):
            Determines whether the revert is actually carried out;
            otherwise, an audit trail is passively generated.

    Returns:
        audit (dict):
            An audit trail for the revert, e.g.,

            {
                'task': {...},
                'change': 'reverted'
                'assignments': [
                    {
                        # worker_0 assignment
                        'assignment': {...},
                        'change': 'reverted',
                        'snapshots': [
                            {
                                'snapshot': {...},
                                'change': 'unchanged'
                            }
                            {
                                'snapshot': {...},
                                'change': 'deleted'
                            },
                        ]
                    },
                    {
                        # worker_1 assignment
                        'assignment': {...},
                        'change': 'deleted',
                        'snapshots': [
                            {
                                'snapshot': {...},
                                'change': 'deleted'
                            },
                            {
                                'snapshot': {...},
                                'change': 'deleted'
                            }
                        ]
                    }
                ],
            }
    """
    task = Task.objects.get(id=task_id)
    audit = {
        'task': TaskSerializer(task).data,
        'change': 'unchanged',
        'assignments': []
    }
    reverted = False

    for assignment in assignment_history(task).all():
        assignment_audit = _revert_assignment_to_datetime(
            assignment, revert_datetime, fake)
        audit['assignments'].append(assignment_audit)
        if assignment_audit['change'] != 'unchanged':
            reverted = True

    # Delete task if it starts after revert_datetime
    if task.start_datetime >= revert_datetime:
        audit['change'] = 'deleted'
        if not fake:
            task.delete()
    elif reverted:
        audit['change'] = 'reverted'
        if not fake:
            _revert_task_status(task)

    return audit
コード例 #35
0
ファイル: fixtures.py プロジェクト: ehdr/orchestra
def setup_complete_task(test_case):
    # Microseconds are truncated when manually saving models
    test_start = timezone.now().replace(microsecond=0)
    times = {
        'awaiting_pickup': test_start,
        'entry_pickup': test_start + timedelta(hours=1),
        'entry_submit': test_start + timedelta(hours=2),
        'reviewer_pickup': test_start + timedelta(hours=3),
        'reviewer_reject': test_start + timedelta(hours=4),
        'entry_resubmit': test_start + timedelta(hours=5),
        'reviewer_accept': test_start + timedelta(hours=6),
    }

    task = TaskFactory(
        project=test_case.projects['empty_project'],
        status=Task.Status.AWAITING_PROCESSING,
        step=test_case.test_step,
        start_datetime=times['awaiting_pickup'])

    workers = {
        'entry': test_case.workers[0],
        'reviewer': test_case.workers[1]
    }

    assign_task(workers['entry'].id, task.id)

    task.refresh_from_db()
    test_case.assertEquals(task.status, Task.Status.PROCESSING)

    submit_task(
        task.id, {'test': 'entry_submit'},
        Iteration.Status.REQUESTED_REVIEW,
        workers['entry'])

    task.refresh_from_db()
    test_case.assertEquals(task.status, Task.Status.PENDING_REVIEW)

    assign_task(workers['reviewer'].id, task.id)
    reviewer_assignment = task.assignments.get(
        worker=workers['reviewer'])

    # Modify assignment with correct datetime
    reviewer_assignment.start_datetime = times['reviewer_pickup']
    reviewer_assignment.save()

    task.refresh_from_db()
    test_case.assertEquals(task.status, Task.Status.REVIEWING)

    submit_task(
        task.id, {'test': 'reviewer_reject'},
        Iteration.Status.PROVIDED_REVIEW,
        workers['reviewer'])

    task.refresh_from_db()
    test_case.assertEquals(task.status, Task.Status.POST_REVIEW_PROCESSING)

    submit_task(
        task.id, {'test': 'entry_resubmit'},
        Iteration.Status.REQUESTED_REVIEW,
        workers['entry'])

    task.refresh_from_db()
    test_case.assertEquals(task.status, Task.Status.REVIEWING)

    with patch('orchestra.utils.task_lifecycle._is_review_needed',
               return_value=False):
        submit_task(
            task.id, {'test': 'reviewer_accept'},
            Iteration.Status.REQUESTED_REVIEW,
            workers['reviewer'])

    task.refresh_from_db()
    test_case.assertEquals(task.status, Task.Status.COMPLETE)
    test_case.assertEquals(task.assignments.count(), 2)
    for assignment in task.assignments.all():
        test_case.assertEquals(
            assignment.status, TaskAssignment.Status.SUBMITTED)
        test_case.assertEquals(assignment.iterations.count(), 2)

    # Modify assignments with correct datetime
    new_datetime_labels = ('entry_pickup', 'reviewer_pickup')
    for i, assignment in enumerate(assignment_history(task).all()):
        assignment.start_datetime = times[new_datetime_labels[i]]
        assignment.save()

    # Modify iterations with correct datetime
    new_datetime_labels = (
        ('entry_pickup', 'entry_submit'),
        ('reviewer_pickup', 'reviewer_reject'),
        ('reviewer_reject', 'entry_resubmit'),
        ('entry_resubmit', 'reviewer_accept')
    )
    new_datetimes = [
        (times[start_label], times[end_label])
        for start_label, end_label in new_datetime_labels]

    for i, iteration in enumerate(get_iteration_history(task)):
        iteration.start_datetime, iteration.end_datetime = new_datetimes[i]
        iteration.save()

    verify_iterations(task.id)

    return task
コード例 #36
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)