Skip to content

Commit

Permalink
Added todolist_templates_to_apply with automatic application (#476)
Browse files Browse the repository at this point in the history
* Added todolist_templates with automatic application

* Fixed linter errors

* Fixed more linter errors

* Fixed tests and comments

* Fixed linter errors

* Fixed linter errors

* Fixed based on comments

* Fixed tests

* Lint fix

* Fixed todo test

* Fixed load test

* Fixed lint

* Lint fix
  • Loading branch information
Noah Picard committed Aug 21, 2018
1 parent 8036232 commit e1693a3
Show file tree
Hide file tree
Showing 9 changed files with 122 additions and 26 deletions.
20 changes: 20 additions & 0 deletions orchestra/migrations/0081_step_todolist_templates_to_apply.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.12 on 2018-08-21 18:04
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('orchestra', '0080_completed_project_status'),
]

operations = [
migrations.AddField(
model_name='step',
name='todolist_templates_to_apply',
field=models.ManyToManyField(blank=True, to='orchestra.TodoListTemplate'),
),
]
5 changes: 5 additions & 0 deletions orchestra/models/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,9 @@ class Step(StepMixin, models.Model):
user_interface (str):
A JSON blob used to describe the files used in the user interface
for this step (only valid for human steps).
todolist_templates_to_apply ([orchestra.models.TodoListTemplate]):
TodoListTemplates to automatically apply to the todolist of this
step (only valid for human steps).
"""
# General fields
created_at = models.DateTimeField(default=timezone.now)
Expand Down Expand Up @@ -187,6 +190,8 @@ class Step(StepMixin, models.Model):
review_policy = JSONField(default={})
creation_policy = JSONField(default={})
user_interface = JSONField(default={})
todolist_templates_to_apply = models.ManyToManyField('TodoListTemplate',
blank=True)

class Meta:
app_label = 'orchestra'
Expand Down
38 changes: 38 additions & 0 deletions orchestra/tests/helpers/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from django.utils import timezone

from orchestra.communication.slack import create_project_slack_group
from orchestra.core.errors import WorkflowError
from orchestra.models import CommunicationPreference
from orchestra.models import Iteration
from orchestra.models import StaffBotRequest
Expand All @@ -17,6 +18,7 @@
from orchestra.models import Step
from orchestra.models import Task
from orchestra.models import TaskAssignment
from orchestra.models import TodoListTemplate
from orchestra.models import WorkerCertification
from orchestra.models import Workflow
from orchestra.models import WorkflowVersion
Expand Down Expand Up @@ -378,13 +380,34 @@ def setup_models(test_case):
},
}

todolist_template_slug = 'project-checklist'

todolist_templates = {
todolist_template_slug: {
'name': 'Project checklist',
'slug': todolist_template_slug
}
}

# Create the objects
_setup_todolist_templates(test_case, todolist_templates)
_setup_workflows(test_case, workflows)
_setup_workers(test_case, workers)
_setup_projects(test_case, projects)
_setup_tasks(test_case, tasks)


def _setup_todolist_templates(test_case, templates):
test_case.todolist_templates = {}
for template_idx, template_key in enumerate(templates):
template_details = templates[template_key]
template = TodoListTemplateFactory(
slug=template_details['slug'],
name=template_details['name'],
)
test_case.todolist_templates[template_details['slug']] = template


def _setup_workflows(test_case, workflows):
# Create workflows
test_case.workflows = {}
Expand Down Expand Up @@ -456,6 +479,9 @@ def _setup_workflows(test_case, workflows):
execution_function=step_details.get('execution_function',
{}),
)
_set_step_relations(step, step_details,
'todolist_templates_to_apply',
TodoListTemplate)
workflow_steps[step.slug] = step

# Add required certifications
Expand Down Expand Up @@ -497,6 +523,18 @@ def _add_step_dependencies(step_dict, existing_workflow_steps, attr):
return backrefs


def _set_step_relations(step, step_data, relation_attr, relation_model,
**model_filters):
relation_slugs = set(step_data.get(relation_attr, []))
relations = list(relation_model.objects.filter(
slug__in=relation_slugs, **model_filters))
if len(relations) != len(relation_slugs):
raise WorkflowError(
'{}.{} contains a non-existent slug.'
.format(step_data['slug'], relation_attr))
getattr(step, relation_attr).set(relations)


def _create_backrefs(workflow_steps, backrefs):
for backref in backrefs:
dependency = workflow_steps[backref['ref']]
Expand Down
6 changes: 6 additions & 0 deletions orchestra/tests/helpers/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,9 @@ def check_project(project):
'angular_module': 'step1',
'angular_directive': 'step1_directive',
},
'todolist_templates_to_apply': [
'project-checklist'
]
},
{
'slug': 'step_1',
Expand Down Expand Up @@ -227,6 +230,9 @@ def check_project(project):
'angular_module': 'step2',
'angular_directive': 'step2_directive',
},
'todolist_templates_to_apply': [
'project-checklist'
]
},
],
},
Expand Down
3 changes: 2 additions & 1 deletion orchestra/tests/test_todos.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,9 +380,10 @@ def _verify_todolist_template_content(self, todolist_template,
def _verify_todolist_template_list(self, expected_todolist_templates):
resp = self.request_client.get(self.todolist_template_list_url)
self.assertEqual(resp.status_code, 200)
length = len(expected_todolist_templates)
data = load_encoded_json(resp.content)
for todolist_template, expected_todolist_template in \
zip(data, expected_todolist_templates):
zip(data[-length:], expected_todolist_templates):
self._verify_todolist_template_content(
todolist_template, expected_todolist_template)

Expand Down
12 changes: 8 additions & 4 deletions orchestra/tests/workflows/test_load.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,10 @@ def test_load_workflow_version(self):

# Even with --force, can't overwrite a version with a new step
v1_data['steps'].append({'slug': 'invalid_new_step'})
step_change_err_msg = ('Even with --force, cannot change the steps of '
'a workflow.')
step_change_err_msg = ('Even with --force, you cannot change the '
'steps of a workflow. Drop and recreate the '
'database to reset, or create a new version '
'for your workflow.')
with self.assertRaisesMessage(WorkflowError, step_change_err_msg):
with transaction.atomic():
load_workflow_version(v1_data, workflow, force=True)
Expand All @@ -135,8 +137,10 @@ def test_load_workflow_version(self):
# Even with --force, can't change a step's creation dependencies.
step_2_create_dependencies = v1_data['steps'][1]['creation_depends_on']
step_2_create_dependencies.append('s3')
topology_change_err_msg = ('Even with --force, cannot change the '
'topology of a workflow.')
topology_change_err_msg = ('Even with --force, you cannot change the '
'topology of a workflow. Drop and recreate '
'the database to reset, or create a new '
'version for your workflow.')
with self.assertRaisesMessage(WorkflowError, topology_change_err_msg):
with transaction.atomic():
load_workflow_version(v1_data, workflow, force=True)
Expand Down
5 changes: 5 additions & 0 deletions orchestra/utils/task_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from orchestra.models import TaskAssignment
from orchestra.models import Worker
from orchestra.models import WorkerCertification
from orchestra.todos.api import add_todolist_template
from orchestra.utils.notifications import notify_status_change
from orchestra.utils.notifications import notify_project_status_change
from orchestra.utils.task_properties import assignment_history
Expand Down Expand Up @@ -1195,6 +1196,10 @@ def create_subsequent_tasks(project):
status=Task.Status.AWAITING_PROCESSING)
task.save()

# Apply todolist templates to Task
for template in task.step.todolist_templates_to_apply.all():
add_todolist_template(template.slug, task.id)

_preassign_workers(task, AssignmentPolicyType.ENTRY_LEVEL)

if not step.is_human:
Expand Down
19 changes: 16 additions & 3 deletions orchestra/utils/tests/test_task_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,12 @@ def test_worker_assigned_to_rejected_task(self):
task__status=Task.Status.POST_REVIEW_PROCESSING)
self.assertTrue(assignments.exists())
self.assertTrue(worker_assigned_to_rejected_task(self.workers[4]))
with self.assertRaises(TaskAssignmentError):
get_new_task_assignment(self.workers[4],
Task.Status.AWAITING_PROCESSING)
with patch('orchestra.utils.task_lifecycle.settings.'
+ 'ORCHESTRA_ENFORCE_NO_NEW_TASKS_DURING_REVIEW',
return_value=True):
with self.assertRaises(TaskAssignmentError):
get_new_task_assignment(self.workers[4],
Task.Status.AWAITING_PROCESSING)

def test_worker_has_reviewer_status(self):
self.assertFalse(worker_has_reviewer_status(self.workers[0]))
Expand Down Expand Up @@ -570,6 +573,16 @@ def test_preassign_workers(self):
self.assertTrue(
related_task.is_worker_assigned(self.workers[4]))

def test_todolist_templates_to_apply(self):
project = self.projects['assignment_policy']
mock = MagicMock(return_value=True)
with patch('orchestra.utils.task_lifecycle.add_todolist_template',
new=mock):
# Create first task in test project
create_subsequent_tasks(project)
assert mock.called_once
assert mock.call_args[0][0] == 'project-checklist'

def test_malformed_assignment_policy(self):
project = self.projects['assignment_policy']
workflow_version = project.workflow_version
Expand Down
40 changes: 22 additions & 18 deletions orchestra/workflow/load.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from orchestra.core.errors import WorkflowError
from orchestra.models import Certification
from orchestra.models import Step
from orchestra.models import TodoListTemplate
from orchestra.models import Workflow
from orchestra.models import WorkflowVersion
from orchestra.workflow.defaults import get_default_assignment_policy
Expand Down Expand Up @@ -122,8 +123,8 @@ def load_workflow_version(version_data, workflow, force=False):
.values_list('slug', flat=True)
)
if new_step_slugs != old_step_slugs:
raise WorkflowError('Even with --force, cannot change the steps '
'of a workflow. Drop and recreate the '
raise WorkflowError('Even with --force, you cannot change the '
'steps of a workflow. Drop and recreate the '
'database to reset, or create a new version '
'for your workflow.')

Expand Down Expand Up @@ -162,8 +163,8 @@ def load_workflow_version(version_data, workflow, force=False):

# Don't prevent updates to these, because we want to allow
# certifications to evolve over the lifetime of a workflow.
_set_step_dependencies(step, step_data, 'required_certifications',
Certification, workflow=workflow)
_set_step_relations(step, step_data, 'required_certifications',
Certification, workflow=workflow)

# Set up step dependencies once the steps objects are in the DB.
for step_data in version_data['steps']:
Expand All @@ -179,30 +180,33 @@ def load_workflow_version(version_data, workflow, force=False):
'creation_depends_on',
old_creation_dependencies.get(step_slug)
)
_set_step_dependencies(step, step_data, 'creation_depends_on', Step,
workflow_version=version)
_set_step_relations(step, step_data, 'creation_depends_on', Step,
workflow_version=version)

_set_step_dependencies(step, step_data, 'submission_depends_on', Step,
workflow_version=version)
_set_step_relations(step, step_data, 'submission_depends_on', Step,
workflow_version=version)

_set_step_relations(step, step_data, 'todolist_templates_to_apply',
TodoListTemplate)


def _verify_dependencies_not_updated(step_data, dependency_attr,
old_dependencies):
new_dependencies = set(step_data.get(dependency_attr, []))
if old_dependencies is not None and old_dependencies != new_dependencies:
raise WorkflowError(
'Even with --force, cannot change the topology of a workflow. '
'Even with --force, you cannot change the topology of a workflow. '
'Drop and recreate the database to reset, or create a new '
'version for your workflow.')


def _set_step_dependencies(step, step_data, dependency_attr, dependency_model,
**model_filters):
dependency_slugs = set(step_data.get(dependency_attr, []))
dependencies = list(dependency_model.objects.filter(
slug__in=dependency_slugs, **model_filters))
if len(dependencies) != len(dependency_slugs):
def _set_step_relations(step, step_data, relation_attr, relation_model,
**model_filters):
relation_slugs = set(step_data.get(relation_attr, []))
relations = list(relation_model.objects.filter(
slug__in=relation_slugs, **model_filters))
if len(relations) != len(relation_slugs):
raise WorkflowError(
'{}.{} contains a non-existent slug.'
.format(step_data['slug'], dependency_attr))
getattr(step, dependency_attr).set(dependencies)
'{}.{} contains a non-existent slug.'
.format(step_data['slug'], relation_attr))
getattr(step, relation_attr).set(relations)

0 comments on commit e1693a3

Please sign in to comment.