Merge remote-tracking branch 'origin/master' into feature/delete_study

This commit is contained in:
Aaron Louie 2020-03-16 08:49:44 -04:00
commit 3885bc7624
11 changed files with 274 additions and 22 deletions

View File

@ -500,6 +500,27 @@ paths:
responses: responses:
'204': '204':
description: The workflow was removed description: The workflow was removed
/workflow/{workflow_id}/stats:
parameters:
- name: workflow_id
in: path
required: true
description: The id of the workflow
schema:
type: integer
format: int32
get:
operationId: crc.api.stats.get_workflow_stats
summary: Stats about a given workflow
tags:
- Workflows and Tasks
responses:
'200':
description: Returns counts of complete, incomplete, and total number of tasks
content:
application/json:
schema:
$ref: "#/components/schemas/Workflow"
# /v1.0/workflow/0/task/0 # /v1.0/workflow/0/task/0
/workflow/{workflow_id}/task/{task_id}: /workflow/{workflow_id}/task/{task_id}:
parameters: parameters:
@ -749,6 +770,12 @@ components:
type: string type: string
is_latest_spec: is_latest_spec:
type: boolean type: boolean
num_tasks_total:
type: integer
num_tasks_complete:
type: integer
num_tasks_incomplete:
type: integer
example: example:
id: 291234 id: 291234

59
crc/api/stats.py Normal file
View File

@ -0,0 +1,59 @@
from datetime import datetime
from flask import g
from crc import session, auth
from crc.models.stats import WorkflowStatsModel, WorkflowStatsModelSchema, TaskEventModel, TaskEventModelSchema
@auth.login_required
def get_workflow_stats(workflow_id):
workflow_model = session.query(WorkflowStatsModel).filter_by(workflow_id=workflow_id).first()
return WorkflowStatsModelSchema().dump(workflow_model)
@auth.login_required
def update_workflow_stats(workflow_model, workflow_api_model):
stats = session.query(WorkflowStatsModel) \
.filter_by(study_id=workflow_model.study_id) \
.filter_by(workflow_id=workflow_model.id) \
.filter_by(workflow_spec_id=workflow_model.workflow_spec_id) \
.filter_by(spec_version=workflow_model.spec_version) \
.first()
if stats is None:
stats = WorkflowStatsModel(
study_id=workflow_model.study_id,
workflow_id=workflow_model.id,
workflow_spec_id=workflow_model.workflow_spec_id,
spec_version=workflow_model.spec_version,
)
complete_states = ['CANCELLED', 'COMPLETED']
incomplete_states = ['MAYBE', 'LIKELY', 'FUTURE', 'WAITING', 'READY']
tasks = list(workflow_api_model.user_tasks)
stats.num_tasks_total = len(tasks)
stats.num_tasks_complete = sum(1 for t in tasks if t.state in complete_states)
stats.num_tasks_incomplete = sum(1 for t in tasks if t.state in incomplete_states)
stats.last_updated = datetime.now()
session.add(stats)
session.commit()
return WorkflowStatsModelSchema().dump(stats)
@auth.login_required
def log_task_complete(workflow_model, task_id):
task_event = TaskEventModel(
study_id=workflow_model.study_id,
user_uid=g.user.uid,
workflow_id=workflow_model.id,
workflow_spec_id=workflow_model.workflow_spec_id,
spec_version=workflow_model.spec_version,
task_id=task_id,
task_state='COMPLETE',
date=datetime.now(),
)
session.add(task_event)
session.commit()
return TaskEventModelSchema().dump(task_event)

View File

@ -33,7 +33,6 @@ def add_study(body):
return StudyModelSchema().dump(study) return StudyModelSchema().dump(study)
@auth.login_required @auth.login_required
def update_study(study_id, body): def update_study(study_id, body):
if study_id is None: if study_id is None:

View File

@ -1,12 +1,13 @@
import uuid import uuid
from crc.api.file import delete_file from crc.api.stats import update_workflow_stats, log_task_complete
from crc import session from crc import session, auth
from crc.api.common import ApiError, ApiErrorSchema from crc.api.common import ApiError, ApiErrorSchema
from crc.api.file import delete_file
from crc.models.api_models import Task, WorkflowApi, WorkflowApiSchema from crc.models.api_models import Task, WorkflowApi, WorkflowApiSchema
from crc.models.file import FileModel
from crc.models.workflow import WorkflowModel, WorkflowSpecModelSchema, WorkflowSpecModel from crc.models.workflow import WorkflowModel, WorkflowSpecModelSchema, WorkflowSpecModel
from crc.services.workflow_processor import WorkflowProcessor from crc.services.workflow_processor import WorkflowProcessor
from crc.models.file import FileModel
def all_specifications(): def all_specifications():
@ -14,6 +15,7 @@ def all_specifications():
return schema.dump(session.query(WorkflowSpecModel).all()) return schema.dump(session.query(WorkflowSpecModel).all())
@auth.login_required
def add_workflow_specification(body): def add_workflow_specification(body):
new_spec = WorkflowSpecModelSchema().load(body, session=session) new_spec = WorkflowSpecModelSchema().load(body, session=session)
session.add(new_spec) session.add(new_spec)
@ -21,6 +23,7 @@ def add_workflow_specification(body):
return WorkflowSpecModelSchema().dump(new_spec) return WorkflowSpecModelSchema().dump(new_spec)
@auth.login_required
def get_workflow_specification(spec_id): def get_workflow_specification(spec_id):
if spec_id is None: if spec_id is None:
error = ApiError('unknown_spec', 'Please provide a valid Workflow Specification ID.') error = ApiError('unknown_spec', 'Please provide a valid Workflow Specification ID.')
@ -35,6 +38,7 @@ def get_workflow_specification(spec_id):
return WorkflowSpecModelSchema().dump(spec) return WorkflowSpecModelSchema().dump(spec)
@auth.login_required
def update_workflow_specification(spec_id, body): def update_workflow_specification(spec_id, body):
spec = WorkflowSpecModelSchema().load(body, session=session) spec = WorkflowSpecModelSchema().load(body, session=session)
spec.id = spec_id spec.id = spec_id
@ -43,6 +47,7 @@ def update_workflow_specification(spec_id, body):
return WorkflowSpecModelSchema().dump(spec) return WorkflowSpecModelSchema().dump(spec)
@auth.login_required
def delete_workflow_specification(spec_id): def delete_workflow_specification(spec_id):
if spec_id is None: if spec_id is None:
error = ApiError('unknown_spec', 'Please provide a valid Workflow Specification ID.') error = ApiError('unknown_spec', 'Please provide a valid Workflow Specification ID.')
@ -61,13 +66,12 @@ def delete_workflow_specification(spec_id):
session.query(WorkflowModel).filter_by(workflow_spec_id=spec_id).delete() session.query(WorkflowModel).filter_by(workflow_spec_id=spec_id).delete()
session.query(WorkflowSpecModel).filter_by(id=spec_id).delete() session.query(WorkflowSpecModel).filter_by(id=spec_id).delete()
session.commit() session.commit()
def __get_workflow_api_model(processor: WorkflowProcessor): def __get_workflow_api_model(processor: WorkflowProcessor):
spiff_tasks = processor.get_all_user_tasks() spiff_tasks = processor.get_all_user_tasks()
user_tasks = map(Task.from_spiff, spiff_tasks) user_tasks = list(map(Task.from_spiff, spiff_tasks))
workflow_api = WorkflowApi( workflow_api = WorkflowApi(
id=processor.get_workflow_id(), id=processor.get_workflow_id(),
status=processor.get_status(), status=processor.get_status(),
@ -83,22 +87,29 @@ def __get_workflow_api_model(processor: WorkflowProcessor):
return workflow_api return workflow_api
@auth.login_required
def get_workflow(workflow_id, soft_reset=False, hard_reset=False): def get_workflow(workflow_id, soft_reset=False, hard_reset=False):
schema = WorkflowApiSchema()
workflow_model = session.query(WorkflowModel).filter_by(id=workflow_id).first() workflow_model = session.query(WorkflowModel).filter_by(id=workflow_id).first()
processor = WorkflowProcessor(workflow_model, soft_reset=soft_reset, hard_reset=hard_reset) processor = WorkflowProcessor(workflow_model, soft_reset=soft_reset, hard_reset=hard_reset)
return schema.dump(__get_workflow_api_model(processor))
workflow_api_model = __get_workflow_api_model(processor)
update_workflow_stats(workflow_model, workflow_api_model)
return WorkflowApiSchema().dump(workflow_api_model)
@auth.login_required
def delete(workflow_id): def delete(workflow_id):
session.query(WorkflowModel).filter_by(id=workflow_id).delete() session.query(WorkflowModel).filter_by(id=workflow_id).delete()
session.commit() session.commit()
@auth.login_required
def get_task(workflow_id, task_id): def get_task(workflow_id, task_id):
workflow = session.query(WorkflowModel).filter_by(id=workflow_id).first() workflow = session.query(WorkflowModel).filter_by(id=workflow_id).first()
return workflow.bpmn_workflow().get_task(task_id) return workflow.bpmn_workflow().get_task(task_id)
@auth.login_required
def update_task(workflow_id, task_id, body): def update_task(workflow_id, task_id, body):
workflow_model = session.query(WorkflowModel).filter_by(id=workflow_id).first() workflow_model = session.query(WorkflowModel).filter_by(id=workflow_id).first()
processor = WorkflowProcessor(workflow_model) processor = WorkflowProcessor(workflow_model)
@ -111,5 +122,8 @@ def update_task(workflow_id, task_id, body):
workflow_model.bpmn_workflow_json = processor.serialize() workflow_model.bpmn_workflow_json = processor.serialize()
session.add(workflow_model) session.add(workflow_model)
session.commit() session.commit()
return WorkflowApiSchema().dump(__get_workflow_api_model(processor)
) workflow_api_model = __get_workflow_api_model(processor)
update_workflow_stats(workflow_model, workflow_api_model)
log_task_complete(workflow_model, task_id)
return WorkflowApiSchema().dump(workflow_api_model)

View File

@ -108,10 +108,9 @@ class WorkflowApi(object):
class WorkflowApiSchema(ma.Schema): class WorkflowApiSchema(ma.Schema):
class Meta: class Meta:
model = WorkflowApi model = WorkflowApi
fields = ["id", "status", fields = ["id", "status", "user_tasks", "last_task", "next_task",
"user_tasks", "last_task", "next_task", "workflow_spec_id", "spec_version", "is_latest_spec",
"workflow_spec_id", "num_tasks_total", "num_tasks_complete", "num_tasks_incomplete"]
"spec_version", "is_latest_spec"]
unknown = INCLUDE unknown = INCLUDE
status = EnumField(WorkflowStatus) status = EnumField(WorkflowStatus)
@ -121,4 +120,15 @@ class WorkflowApiSchema(ma.Schema):
@marshmallow.post_load @marshmallow.post_load
def make_workflow(self, data, **kwargs): def make_workflow(self, data, **kwargs):
return WorkflowApi(**data) keys = [
'id',
'status',
'user_tasks',
'last_task',
'next_task',
'workflow_spec_id',
'spec_version',
'is_latest_spec'
]
filtered_fields = {key: data[key] for key in keys}
return WorkflowApi(**filtered_fields)

41
crc/models/stats.py Normal file
View File

@ -0,0 +1,41 @@
from marshmallow_sqlalchemy import ModelSchema
from crc import db
class WorkflowStatsModel(db.Model):
__tablename__ = 'workflow_stats'
id = db.Column(db.Integer, primary_key=True)
study_id = db.Column(db.Integer, db.ForeignKey('study.id'), nullable=False)
workflow_id = db.Column(db.Integer, db.ForeignKey('workflow.id'), nullable=False)
workflow_spec_id = db.Column(db.String, db.ForeignKey('workflow_spec.id'))
spec_version = db.Column(db.String)
num_tasks_total = db.Column(db.Integer)
num_tasks_complete = db.Column(db.Integer)
num_tasks_incomplete = db.Column(db.Integer)
last_updated = db.Column(db.DateTime)
class WorkflowStatsModelSchema(ModelSchema):
class Meta:
model = WorkflowStatsModel
include_fk = True # Includes foreign keys
class TaskEventModel(db.Model):
__tablename__ = 'task_event'
id = db.Column(db.Integer, primary_key=True)
study_id = db.Column(db.Integer, db.ForeignKey('study.id'), nullable=False)
user_uid = db.Column(db.String, db.ForeignKey('user.uid'), nullable=False)
workflow_id = db.Column(db.Integer, db.ForeignKey('workflow.id'), nullable=False)
workflow_spec_id = db.Column(db.String, db.ForeignKey('workflow_spec.id'))
spec_version = db.Column(db.String)
task_id = db.Column(db.String)
task_state = db.Column(db.String)
date = db.Column(db.DateTime)
class TaskEventModelSchema(ModelSchema):
class Meta:
model = TaskEventModel
include_fk = True # Includes foreign keys

View File

@ -28,5 +28,3 @@ class StudyModelSchema(ModelSchema):
include_fk = True # Includes foreign keys include_fk = True # Includes foreign keys
protocol_builder_status = EnumField(ProtocolBuilderStatus) protocol_builder_status = EnumField(ProtocolBuilderStatus)

View File

@ -0,0 +1,59 @@
"""empty message
Revision ID: 90dd63672e0a
Revises: 8856126b6658
Create Date: 2020-03-10 21:16:38.827156
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '90dd63672e0a'
down_revision = '8856126b6658'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('task_event',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('study_id', sa.Integer(), nullable=False),
sa.Column('user_uid', sa.String(), nullable=False),
sa.Column('workflow_id', sa.Integer(), nullable=False),
sa.Column('workflow_spec_id', sa.String(), nullable=True),
sa.Column('spec_version', sa.String(), nullable=True),
sa.Column('task_id', sa.String(), nullable=True),
sa.Column('task_state', sa.String(), nullable=True),
sa.Column('date', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['study_id'], ['study.id'], ),
sa.ForeignKeyConstraint(['user_uid'], ['user.uid'], ),
sa.ForeignKeyConstraint(['workflow_id'], ['workflow.id'], ),
sa.ForeignKeyConstraint(['workflow_spec_id'], ['workflow_spec.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('workflow_stats',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('study_id', sa.Integer(), nullable=False),
sa.Column('workflow_id', sa.Integer(), nullable=False),
sa.Column('workflow_spec_id', sa.String(), nullable=True),
sa.Column('spec_version', sa.String(), nullable=True),
sa.Column('num_tasks_total', sa.Integer(), nullable=True),
sa.Column('num_tasks_complete', sa.Integer(), nullable=True),
sa.Column('num_tasks_incomplete', sa.Integer(), nullable=True),
sa.Column('last_updated', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['study_id'], ['study.id'], ),
sa.ForeignKeyConstraint(['workflow_id'], ['workflow.id'], ),
sa.ForeignKeyConstraint(['workflow_spec_id'], ['workflow_spec.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('workflow_stats')
op.drop_table('task_event')
# ### end Alembic commands ###

View File

@ -174,7 +174,7 @@ class TestStudyApi(BaseTest):
self.assertEqual(1, session.query(WorkflowModel).count()) self.assertEqual(1, session.query(WorkflowModel).count())
json_data = json.loads(rv.get_data(as_text=True)) json_data = json.loads(rv.get_data(as_text=True))
workflow = WorkflowApiSchema().load(json_data) workflow = WorkflowApiSchema().load(json_data)
rv = self.app.delete('/v1.0/workflow/%i' % workflow.id) rv = self.app.delete('/v1.0/workflow/%i' % workflow.id, headers=self.logged_in_headers())
self.assert_success(rv) self.assert_success(rv)
self.assertEqual(0, session.query(WorkflowModel).count()) self.assertEqual(0, session.query(WorkflowModel).count())

View File

@ -4,6 +4,7 @@ import os
from crc import session, app from crc import session, app
from crc.models.api_models import WorkflowApiSchema, Task from crc.models.api_models import WorkflowApiSchema, Task
from crc.models.file import FileModelSchema from crc.models.file import FileModelSchema
from crc.models.stats import WorkflowStatsModel, TaskEventModel
from crc.models.study import StudyModel from crc.models.study import StudyModel
from crc.models.workflow import WorkflowSpecModelSchema, WorkflowModel, WorkflowStatus from crc.models.workflow import WorkflowSpecModelSchema, WorkflowModel, WorkflowStatus
from crc.services.workflow_processor import WorkflowProcessor from crc.services.workflow_processor import WorkflowProcessor
@ -28,6 +29,7 @@ class TestTasksApi(BaseTest):
def get_workflow_api(self, workflow, soft_reset=False, hard_reset=False): def get_workflow_api(self, workflow, soft_reset=False, hard_reset=False):
rv = self.app.get('/v1.0/workflow/%i?soft_reset=%s&hard_reset=%s' % rv = self.app.get('/v1.0/workflow/%i?soft_reset=%s&hard_reset=%s' %
(workflow.id, str(soft_reset), str(hard_reset)), (workflow.id, str(soft_reset), str(hard_reset)),
headers=self.logged_in_headers(),
content_type="application/json") content_type="application/json")
self.assert_success(rv) self.assert_success(rv)
json_data = json.loads(rv.get_data(as_text=True)) json_data = json.loads(rv.get_data(as_text=True))
@ -37,10 +39,24 @@ class TestTasksApi(BaseTest):
def complete_form(self, workflow, task, dict_data): def complete_form(self, workflow, task, dict_data):
rv = self.app.put('/v1.0/workflow/%i/task/%s/data' % (workflow.id, task.id), rv = self.app.put('/v1.0/workflow/%i/task/%s/data' % (workflow.id, task.id),
headers=self.logged_in_headers(),
content_type="application/json", content_type="application/json",
data=json.dumps(dict_data)) data=json.dumps(dict_data))
self.assert_success(rv) self.assert_success(rv)
json_data = json.loads(rv.get_data(as_text=True)) json_data = json.loads(rv.get_data(as_text=True))
num_stats = session.query(WorkflowStatsModel) \
.filter_by(workflow_id=workflow.id) \
.filter_by(workflow_spec_id=workflow.workflow_spec_id) \
.count()
self.assertGreater(num_stats, 0)
num_task_events = session.query(TaskEventModel) \
.filter_by(workflow_id=workflow.id) \
.filter_by(task_id=task.id) \
.count()
self.assertGreater(num_task_events, 0)
workflow = WorkflowApiSchema().load(json_data) workflow = WorkflowApiSchema().load(json_data)
return workflow return workflow
@ -145,7 +161,7 @@ class TestTasksApi(BaseTest):
self.assertIsNotNone(workflow_api.next_task) self.assertIsNotNone(workflow_api.next_task)
self.assertEquals("EndEvent_0evb22x", workflow_api.next_task['name']) self.assertEquals("EndEvent_0evb22x", workflow_api.next_task['name'])
self.assertTrue(workflow_api.status == WorkflowStatus.complete) self.assertTrue(workflow_api.status == WorkflowStatus.complete)
rv = self.app.get('/v1.0/file?workflow_id=%i' % workflow.id) rv = self.app.get('/v1.0/file?workflow_id=%i' % workflow.id, headers=self.logged_in_headers())
self.assert_success(rv) self.assert_success(rv)
json_data = json.loads(rv.get_data(as_text=True)) json_data = json.loads(rv.get_data(as_text=True))
files = FileModelSchema(many=True).load(json_data, session=session) files = FileModelSchema(many=True).load(json_data, session=session)
@ -252,3 +268,30 @@ class TestTasksApi(BaseTest):
workflow_api = self.get_workflow_api(workflow) workflow_api = self.get_workflow_api(workflow)
self.assertTrue(workflow_api.spec_version.startswith("v1 ")) self.assertTrue(workflow_api.spec_version.startswith("v1 "))
self.assertFalse(workflow_api.is_latest_spec) self.assertFalse(workflow_api.is_latest_spec)
def test_get_workflow_stats(self):
self.load_example_data()
workflow = self.create_workflow('exclusive_gateway')
response_before = self.app.get('/v1.0/workflow/%i/stats' % workflow.id,
content_type="application/json",
headers=self.logged_in_headers())
self.assert_success(response_before)
db_stats_before = session.query(WorkflowStatsModel).filter_by(workflow_id=workflow.id).first()
self.assertIsNone(db_stats_before)
# Start the workflow.
tasks = self.get_workflow_api(workflow).user_tasks
self.complete_form(workflow, tasks[0], {"has_bananas": True})
response_after = self.app.get('/v1.0/workflow/%i/stats' % workflow.id,
content_type="application/json",
headers=self.logged_in_headers())
self.assert_success(response_after)
db_stats_after = session.query(WorkflowStatsModel).filter_by(workflow_id=workflow.id).first()
self.assertIsNotNone(db_stats_after)
self.assertGreater(db_stats_after.num_tasks_complete, 0)
self.assertGreater(db_stats_after.num_tasks_incomplete, 0)
self.assertGreater(db_stats_after.num_tasks_total, 0)
self.assertEqual(db_stats_after.num_tasks_total,
db_stats_after.num_tasks_complete + db_stats_after.num_tasks_incomplete)

View File

@ -27,7 +27,9 @@ class TestWorkflowSpec(BaseTest):
num_before = session.query(WorkflowSpecModel).count() num_before = session.query(WorkflowSpecModel).count()
spec = WorkflowSpecModel(id='make_cookies', display_name='Cooooookies', spec = WorkflowSpecModel(id='make_cookies', display_name='Cooooookies',
description='Om nom nom delicious cookies') description='Om nom nom delicious cookies')
rv = self.app.post('/v1.0/workflow-specification', content_type="application/json", rv = self.app.post('/v1.0/workflow-specification',
headers=self.logged_in_headers(),
content_type="application/json",
data=json.dumps(WorkflowSpecModelSchema().dump(spec))) data=json.dumps(WorkflowSpecModelSchema().dump(spec)))
self.assert_success(rv) self.assert_success(rv)
db_spec = session.query(WorkflowSpecModel).filter_by(id='make_cookies').first() db_spec = session.query(WorkflowSpecModel).filter_by(id='make_cookies').first()
@ -38,7 +40,7 @@ class TestWorkflowSpec(BaseTest):
def test_get_workflow_specification(self): def test_get_workflow_specification(self):
self.load_example_data() self.load_example_data()
db_spec = session.query(WorkflowSpecModel).first() db_spec = session.query(WorkflowSpecModel).first()
rv = self.app.get('/v1.0/workflow-specification/%s' % db_spec.id) rv = self.app.get('/v1.0/workflow-specification/%s' % db_spec.id, headers=self.logged_in_headers())
self.assert_success(rv) self.assert_success(rv)
json_data = json.loads(rv.get_data(as_text=True)) json_data = json.loads(rv.get_data(as_text=True))
api_spec = WorkflowSpecModelSchema().load(json_data, session=session) api_spec = WorkflowSpecModelSchema().load(json_data, session=session)
@ -55,7 +57,7 @@ class TestWorkflowSpec(BaseTest):
num_workflows_before = session.query(WorkflowModel).filter_by(workflow_spec_id=spec_id).count() num_workflows_before = session.query(WorkflowModel).filter_by(workflow_spec_id=spec_id).count()
self.assertGreater(num_files_before + num_workflows_before, 0) self.assertGreater(num_files_before + num_workflows_before, 0)
rv = self.app.delete('/v1.0/workflow-specification/' + spec_id) rv = self.app.delete('/v1.0/workflow-specification/' + spec_id, headers=self.logged_in_headers())
self.assert_success(rv) self.assert_success(rv)
num_specs_after = session.query(WorkflowSpecModel).filter_by(id=spec_id).count() num_specs_after = session.query(WorkflowSpecModel).filter_by(id=spec_id).count()