Add / Edit studies, show in a sensible form, allow modifying list of requirements.

This commit is contained in:
Dan Funk 2020-02-17 11:43:26 -05:00
parent a0965bc0fe
commit 51c9b8be86
7 changed files with 275 additions and 110 deletions

View File

@ -13,6 +13,7 @@ flask-wtf = "*"
sqlalchemy = "*"
flask-sqlalchemy = "*"
flask-table = "*"
flask-migrate = "*"
[requires]
python_version = "3.7"

37
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "72fff5c0cf2514d4307a4fc4eb60d57cc5bd0bf19603dc77587835bf01e43bd7"
"sha256": "a5291bb0360424bf22621f3a5aed3906df624e7237a9c8f4c80a34249014a6fa"
},
"pipfile-spec": 6,
"requires": {
@ -16,6 +16,12 @@
]
},
"default": {
"alembic": {
"hashes": [
"sha256:2df2519a5b002f881517693b95626905a39c5faf4b5a1f94de4f1441095d1d26"
],
"version": "==1.4.0"
},
"attrs": {
"hashes": [
"sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
@ -84,6 +90,14 @@
],
"version": "==1.0.0"
},
"flask-migrate": {
"hashes": [
"sha256:6fb038be63d4c60727d5dfa5f581a6189af5b4e2925bc378697b4f0a40cfb4e1",
"sha256:a96ff1875a49a40bd3e8ac04fce73fdb0870b9211e6168608cbafa4eb839d502"
],
"index": "pypi",
"version": "==2.5.2"
},
"flask-sqlalchemy": {
"hashes": [
"sha256:0078d8663330dc05a74bc72b3b6ddc441b9a744e2f56fe60af1a5bfc81334327",
@ -206,6 +220,12 @@
],
"version": "==3.2.0"
},
"mako": {
"hashes": [
"sha256:2984a6733e1d472796ceef37ad48c26f4a984bb18119bb2dbc37a44d8f6e75a4"
],
"version": "==1.1.1"
},
"markupsafe": {
"hashes": [
"sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
@ -258,6 +278,21 @@
],
"version": "==0.15.7"
},
"python-dateutil": {
"hashes": [
"sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
"sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
],
"version": "==2.8.1"
},
"python-editor": {
"hashes": [
"sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d",
"sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b",
"sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8"
],
"version": "==1.0.4"
},
"pytz": {
"hashes": [
"sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d",

84
app.py
View File

@ -1,19 +1,22 @@
import datetime
from datetime import date
import connexion
from flask import url_for, json, redirect, render_template, request, flash
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy
PROTOCOLS = {}
def get_user_studies(user_id):
return {"protocols": [p for p in PROTOCOLS.values() if p['user_id'] == user_id][:limit]}
def required_docs(id):
return {
id: 21,
requirements: []
'id': 21,
'requirements': []
}
@ -32,8 +35,9 @@ conn.add_api('api.yml')
app = conn.app
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'
db = SQLAlchemy(app)
migrate = Migrate(app, db)
def has_no_empty_params(rule):
defaults = rule.defaults if rule.defaults is not None else ()
@ -55,52 +59,60 @@ def site_map():
app.config['SECRET_KEY'] = 'a really really really really long secret key'
from forms import Study, StudyForm, StudySearchForm, StudyTable, RequiredDocument
from forms import StudyForm, StudySearchForm, StudyTable
from models import Study, RequiredDocument
@app.route('/', methods=['GET', 'POST'])
def index():
search = StudySearchForm(request.form)
if request.method == 'POST':
return search_results(search)
return render_template('index.html', form=search)
@app.route('/results')
def search_results(search):
results = []
search_string = search.data['search']
if search.data['search'] == '':
qry = db.session.query(Study)
results = qry.all()
if not results:
flash('No results found!')
return redirect('/')
else:
# display results
studies = db.session.query("Study").all()
table = StudyTable(studies)
return render_template('results.html', table=table)
# display results
studies = db.session.query(Study).order_by(Study.last_updated.desc()).all()
table = StudyTable(studies)
return render_template('index.html', table=table)
@app.route('/new_study', methods=['GET', 'POST'])
def new_study():
form = StudyForm(request.form)
action = "/new_study"
if request.method == 'POST':
# save the study
study = Study()
study.id = form.id
study.title = form.title
# for r in form.requirements:
# requirement = RequiredDocument(id = r.id,
# study.requirements = form.requirements
db.session.add(study)
db.session.commit()
flash('Album created successfully!')
_update_study(study, form)
flash('Study created successfully!')
return redirect('/')
form = StudyForm(request.form)
return render_template('study_form.html', form=form)
@app.route('/study/<study_id>}', methods=['GET', 'POST'])
def edit_study(study_id):
study = db.session.query(Study).filter(Study.study_id == study_id).first()
form = StudyForm(request.form, obj=study)
if request.method == 'GET':
action = "/study/" + study_id
if study.requirements:
form.requirements.data = list(map(lambda r: r.code, list(study.requirements)))
if request.method == 'POST':
_update_study(study, form)
flash('Study updated successfully!')
return redirect('/')
return render_template('study_form.html', form=form)
def _update_study(study, form):
if study.study_id:
db.session.query(RequiredDocument).filter(RequiredDocument.study_id == study.study_id).delete()
for r in form.requirements:
if r.checked:
requirement = RequiredDocument(code=r.data, name=r.label.text, study=study)
db.session.add(requirement)
study.title = form.title.data
study.netbadge_id = form.netbadge_id.data
study.last_updated = datetime.datetime.now()
study.q_complete = form.q_complete.data
db.session.add(study)
db.session.commit()
if __name__ == '__main__':
# run our standalone gevent server

View File

@ -1,58 +1,8 @@
from flask_table import Table, Col
from flask_table import Table, Col, DateCol, LinkCol, BoolCol, DatetimeCol, NestedTableCol
from flask_wtf import FlaskForm
from wtforms import SelectMultipleField, SubmitField, StringField, IntegerField
from wtforms import SelectMultipleField, SubmitField, StringField, IntegerField, BooleanField, DateField, widgets
from app import db
class Study(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(80), nullable=False)
requirements = db.relationship("RequiredDocument", backref="study", lazy='dynamic')
class RequiredDocument(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), nullable=False)
active = db.Column(db.Boolean, default=False)
study_id = db.Column(db.Integer, db.ForeignKey('study.id'))
@staticmethod
def all():
docs = [RequiredDocument(id=1, name="Investigators Brochure"),
RequiredDocument(id=6, name="Cancer Center's PRC Approval Form"),
RequiredDocument(id=8, name="SOM CTO IND/IDE Review Letter"),
RequiredDocument(id=9, name="HIRE Approval"),
RequiredDocument(id=10, name="Cancer Center's PRC Approval Waiver"),
RequiredDocument(id=12, name="Certificate of Confidentiality Application"),
RequiredDocument(id=14, name="Institutional Biosafety Committee Approval"),
RequiredDocument(id=18, name="SOM CTO Approval Letter - UVA PI Multisite Trial"),
RequiredDocument(id=20,
name="IRB Approval or Letter of Approval from Administration: Study Conducted at non- UVA Facilities "),
RequiredDocument(id=21, name="New Medical Device Form"),
RequiredDocument(id=22, name="SOM CTO Review regarding need for IDE"),
RequiredDocument(id=23, name="SOM CTO Review regarding need for IND"),
RequiredDocument(id=24, name="InfoSec Approval"),
RequiredDocument(id=25, name="Scientific Pre-review Documentation"),
RequiredDocument(id=26, name="IBC Number"),
RequiredDocument(id=32, name="IDS - Investigational Drug Service Approval"),
RequiredDocument(id=36, name="RDRC Approval "),
RequiredDocument(id=40, name="SBS/IRB Approval-FERPA"),
RequiredDocument(id=41, name="HIRE Standard Radiation Language"),
RequiredDocument(id=42, name="COI Management Plan "),
RequiredDocument(id=43, name="SOM CTO Approval Letter-Non UVA, Non Industry PI MultiSite Study"),
RequiredDocument(id=44, name="GRIME Approval"),
RequiredDocument(id=45, name="GMEC Approval"),
RequiredDocument(id=46, name="IRB Reliance Agreement Request Form- IRB-HSR is IRB of Record"),
RequiredDocument(id=47, name="Non UVA IRB Approval - Initial and Last Continuation"),
RequiredDocument(id=48, name="MR Physicist Approval- Use of Gadolinium"),
RequiredDocument(id=49, name="SOM CTO Approval- Non- UVA Academia PI of IDE"),
RequiredDocument(id=51, name="IDS Waiver"),
RequiredDocument(id=52, name="Package Inserts"),
RequiredDocument(id=53, name="IRB Reliance Agreement Request Form- IRB-HSR Not IRB of Record"),
RequiredDocument(id=54, name="ESCRO Approval"),
RequiredDocument(id=57, name="Laser Safety Officer Approval")]
return docs
from models import RequiredDocument
class StudySearchForm(FlaskForm):
@ -60,11 +10,26 @@ class StudySearchForm(FlaskForm):
class StudyForm(FlaskForm):
id = IntegerField('Study Id')
title = StringField('Title')
requirements = SelectMultipleField("Requirements", choices=[(rd.id, rd.name) for rd in RequiredDocument.all()])
netbadge_id = StringField('UVA Id for Primary Investigator')
requirements = SelectMultipleField("Requirements", choices=[(rd.code, rd.name) for rd in RequiredDocument.all()])
hsr_number = StringField('HSR Number')
q_complete = BooleanField('Complete in Protocol Builder?')
# last_updated = DateField('Last Updated')
class RequirementsTable(Table):
code = Col('Code')
name = Col('Name')
class StudyTable(Table):
id = Col('Id')
title = Col('Artist')
requirements = Col('Title')
def sort_url(self, col_id, reverse=False):
pass
edit = LinkCol('Edit', 'edit_study', url_kwargs=dict(study_id='study_id'))
study_id = Col('Study Id')
title = Col('Title')
netbadge_id = Col('Primary Investigator')
last_updated = DatetimeCol('Last Update', "medium")
q_complete = BoolCol('Complete?')
requirements = NestedTableCol('Requirements', RequirementsTable)

80
models.py Normal file
View File

@ -0,0 +1,80 @@
from sqlalchemy import func
from app import db
class Study(db.Model):
study_id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(80), nullable=False)
netbadge_id = db.Column(db.String(), nullable=False)
requirements = db.relationship("RequiredDocument", backref="study", lazy='dynamic')
investigators = db.relationship("Investigator", backref="study", lazy='dynamic')
last_updated = db.Column(db.DateTime(timezone=True), default=func.now())
hsr_number = db.Column(db.String())
q_complete = db.Column(db.Integer, nullable=True)
class Investigator(db.Model):
id = db.Column(db.Integer, primary_key=True)
study_id = db.Column(db.Integer, db.ForeignKey('study.study_id'))
netbadge_id = db.Column(db.String(), nullable=False)
type = db.Column(db.String(), nullable=False)
description = db.Column(db.String(), nullable=False)
@staticmethod
def all_types(self):
types = [
Investigator(type="PI", description="Primary Investigator"),
Investigator(type="SI", description="Sub Investigator"),
Investigator(type="DC", description="Department Contact"),
Investigator(type="SC_I", description="Study Coordinator 1"),
Investigator(type="SC_II", description="Study Coordinator 2"),
Investigator(type="AS_C", description="Additional Study Coordinators"),
Investigator(type="DEPT_CH", description="Department Chair"),
Investigator(type="IRBC", description="IRB Coordinator"),
Investigator(type="SCI", description="Scientific Contact"),
]
return types
class RequiredDocument(db.Model):
id = db.Column(db.Integer, primary_key=True)
code = db.Column(db.String(), nullable=False, default="")
name = db.Column(db.String(), nullable=False, default="")
study_id = db.Column(db.Integer, db.ForeignKey('study.study_id'))
@staticmethod
def all():
docs = [RequiredDocument(code=1, name="Investigators Brochure"),
RequiredDocument(code=6, name="Cancer Center's PRC Approval Form"),
RequiredDocument(code=8, name="SOM CTO IND/IDE Review Letter"),
RequiredDocument(code=9, name="HIRE Approval"),
RequiredDocument(code=10, name="Cancer Center's PRC Approval Waiver"),
RequiredDocument(code=12, name="Certificate of Confidentiality Application"),
RequiredDocument(code=14, name="Institutional Biosafety Committee Approval"),
RequiredDocument(code=18, name="SOM CTO Approval Letter - UVA PI Multisite Trial"),
RequiredDocument(code=20,
name="IRB Approval or Letter of Approval from Administration: Study Conducted at non- UVA Facilities "),
RequiredDocument(code=21, name="New Medical Device Form"),
RequiredDocument(code=22, name="SOM CTO Review regarding need for IDE"),
RequiredDocument(code=23, name="SOM CTO Review regarding need for IND"),
RequiredDocument(code=24, name="InfoSec Approval"),
RequiredDocument(code=25, name="Scientific Pre-review Documentation"),
RequiredDocument(code=26, name="IBC Number"),
RequiredDocument(code=32, name="IDS - Investigational Drug Service Approval"),
RequiredDocument(code=36, name="RDRC Approval "),
RequiredDocument(code=40, name="SBS/IRB Approval-FERPA"),
RequiredDocument(code=41, name="HIRE Standard Radiation Language"),
RequiredDocument(code=42, name="COI Management Plan "),
RequiredDocument(code=43, name="SOM CTO Approval Letter-Non UVA, Non Industry PI MultiSite Study"),
RequiredDocument(code=44, name="GRIME Approval"),
RequiredDocument(code=45, name="GMEC Approval"),
RequiredDocument(code=46, name="IRB Reliance Agreement Request Form- IRB-HSR is IRB of Record"),
RequiredDocument(code=47, name="Non UVA IRB Approval - Initial and Last Continuation"),
RequiredDocument(code=48, name="MR Physicist Approval- Use of Gadolinium"),
RequiredDocument(code=49, name="SOM CTO Approval- Non- UVA Academia PI of IDE"),
RequiredDocument(code=51, name="IDS Waiver"),
RequiredDocument(code=52, name="Package Inserts"),
RequiredDocument(code=53, name="IRB Reliance Agreement Request Form- IRB-HSR Not IRB of Record"),
RequiredDocument(code=54, name="ESCRO Approval"),
RequiredDocument(code=57, name="Laser Safety Officer Approval")]
return docs

View File

@ -1,24 +1,93 @@
<doctype html>
<head>
<head>
<title>Protocol Builder Mock</title>
</head>
</head>
<style>
table {
border: 1px solid #1C6EA4;
background-color: #EEEEEE;
width: 100%;
text-align: left;
border-collapse: collapse;
}
<h2>Protocol Builder Mock</h2>
table td, table.blueTable th {
border: 1px solid #AAAAAA;
padding: 3px 2px;
}
<p><p>
<a href="{{ url_for('.new_study') }}"> New Study </a>
table tbody td {
font-size: 13px;
}
{% with messages = get_flashed_messages() %}
{% if messages %}
table tr:nth-child(even) {
background: #D0E4F5;
}
table thead {
background: #1C6EA4;
background: -moz-linear-gradient(top, #5592bb 0%, #327cad 66%, #1C6EA4 100%);
background: -webkit-linear-gradient(top, #5592bb 0%, #327cad 66%, #1C6EA4 100%);
background: linear-gradient(to bottom, #5592bb 0%, #327cad 66%, #1C6EA4 100%);
border-bottom: 2px solid #444444;
}
table thead th {
font-size: 15px;
font-weight: bold;
color: #FFFFFF;
border-left: 2px solid #D0E4F5;
}
table thead th:first-child {
border-left: none;
}
table tfoot {
font-size: 14px;
font-weight: bold;
color: #FFFFFF;
background: #D0E4F5;
background: -moz-linear-gradient(top, #dcebf7 0%, #d4e6f6 66%, #D0E4F5 100%);
background: -webkit-linear-gradient(top, #dcebf7 0%, #d4e6f6 66%, #D0E4F5 100%);
background: linear-gradient(to bottom, #dcebf7 0%, #d4e6f6 66%, #D0E4F5 100%);
border-top: 2px solid #444444;
}
table tfoot td {
font-size: 14px;
}
table tfoot .links {
text-align: right;
}
table tfoot .links a {
display: inline-block;
background: #1C6EA4;
color: #FFFFFF;
padding: 2px 8px;
border-radius: 5px;
}
</style>
<h2>Protocol Builder Mock</h2>
<p>
<p>
<a href="{{ url_for('.new_study') }}"> New Study </a>
{% with messages = get_flashed_messages() %}
{% if messages %}
<ul class=flashes>
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
{% endif %}
{% endwith %}
{{ table }}
<h3>Current Studies</h3>
{{ table }}
</doctype>

View File

@ -8,11 +8,14 @@
select {
height: 600px;
}
input {
width: 500px;
}
</style>
<body>
<h2>New Study</h2>
<form action="/new_study" method="post">
<form action="{{action}}" method="post">
{{ form.csrf_token() }}