diff --git a/Pipfile b/Pipfile index 59200d4..2033d53 100644 --- a/Pipfile +++ b/Pipfile @@ -13,6 +13,7 @@ flask-wtf = "*" sqlalchemy = "*" flask-sqlalchemy = "*" flask-table = "*" +flask-migrate = "*" [requires] python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock index 5ce8cea..6d5a5c4 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -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", diff --git a/app.py b/app.py index 82ebf80..ff740bb 100644 --- a/app.py +++ b/app.py @@ -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/}', 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 diff --git a/forms.py b/forms.py index a1a9a8e..3e92b59 100644 --- a/forms.py +++ b/forms.py @@ -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) + diff --git a/models.py b/models.py new file mode 100644 index 0000000..53e4b66 --- /dev/null +++ b/models.py @@ -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 diff --git a/templates/index.html b/templates/index.html index 05b242d..254dc0d 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,24 +1,93 @@ - - + Protocol Builder Mock - + + + +

Protocol Builder Mock

+ +

+

+ New Study + + {% with messages = get_flashed_messages() %} + {% if messages %}

- {% endif %} -{% endwith %} + {% endif %} + {% endwith %} -{{ table }} +

Current Studies

+ {{ table }}
\ No newline at end of file diff --git a/templates/study_form.html b/templates/study_form.html index 3da688f..2dbe8a2 100644 --- a/templates/study_form.html +++ b/templates/study_form.html @@ -8,11 +8,14 @@ select { height: 600px; } + input { + width: 500px; + }

New Study

-
+ {{ form.csrf_token() }}