Merge pull request #84 from sartography/chore/return-to-pi-api-759

Chore/return to pi api #759
This commit is contained in:
Dan Funk 2022-06-20 16:36:51 -04:00 committed by GitHub
commit 65e9f9f752
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 334 additions and 7 deletions

View File

@ -7,8 +7,11 @@ class ExampleDataLoader:
@staticmethod
def clean_db():
session.flush() # Clear out any transactions before deleting it all to avoid spurious errors.
engine = session.bind.engine
connection = engine.connect()
for table in reversed(db.metadata.sorted_tables):
session.execute(table.delete())
if engine.dialect.has_table(connection, table):
session.execute(table.delete())
session.commit()
session.flush()

View File

@ -0,0 +1,40 @@
"""add pre_review table
Revision ID: 0659a655b5be
Revises: fcc193c49110
Create Date: 2022-06-16 10:03:33.853014
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '0659a655b5be'
down_revision = 'fcc193c49110'
branch_labels = None
depends_on = None
def upgrade():
op.create_table(
'pre_review',
sa.Column('PROT_EVENT_ID', sa.Integer()),
sa.Column('SS_STUDY_ID', sa.Integer(), nullable=False),
sa.Column('DATEENTERED', sa.DateTime(timezone=True)),
sa.Column('REVIEW_TYPE', sa.Integer()),
sa.Column('COMMENTS', sa.String(), nullable=False, default=''),
sa.Column('IRBREVIEWERADMIN', sa.String()),
sa.Column('FNAME', sa.String()),
sa.Column('LNAME', sa.String()),
sa.Column('LOGIN', sa.String(), nullable=False, default=''),
sa.Column('EVENT_TYPE', sa.Integer()),
sa.Column('STATUS', sa.String()),
sa.Column('DETAIL', sa.String()),
sa.ForeignKeyConstraint(['SS_STUDY_ID'], ['study.STUDYID'], ),
sa.PrimaryKeyConstraint('PROT_EVENT_ID')
)
def downgrade():
op.drop_table('pre_review')

View File

@ -2,7 +2,8 @@ from pb import session
from pb.models import Investigator, InvestigatorSchema, IRBInfo, IRBInfoSchema, IRBInfoErrorSchema,\
IRBStatus, IRBStatusSchema, RequiredDocument, RequiredDocumentSchema, \
Study, StudySchema, StudyDetails, StudyDetailsSchema, \
StudySponsor, StudySponsorSchema, CreatorStudySchema
StudySponsor, StudySponsorSchema, CreatorStudySchema, \
PreReview, PreReviewSchema, PreReviewErrorSchema
def get_user_studies(uva_id):
@ -46,3 +47,11 @@ def current_irb_info(studyid):
else:
# IRB Online returns a list with 1 dictionary in this case
return IRBInfoSchema(many=True).dump([irb_info])
def pre_reviews(study_id):
results = session.query(PreReview).filter(PreReview.SS_STUDY_ID == study_id).all()
if results:
return PreReviewSchema(many=True).dump(results)
pre_review = PreReview(STATUS='Error', DETAIL='No records found.')
return PreReviewErrorSchema().dump(pre_review)

View File

@ -175,6 +175,27 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/IRBInfo"
/pre_reviews/{study_id}:
parameters:
- name: study_id
in: path
required: true
description: The id of the study
schema:
type: integer
format: int32
get:
tags:
- CR-Connect
operationId: pb.api.pre_reviews
summary: Info when study is returned to PI
responses:
200:
description: Pre Review info about the study
content:
application/json:
schema:
$ref: "#/components/schemas/PreReview"
components:
schemas:
Study:
@ -642,3 +663,66 @@ components:
type: string
example: Non-UVA IRB Full Board
description: Human readable version of Review Type
PreReview:
type: object
properties:
SS_STUDY_ID:
type: number
example: 1
description: The unique id of the study in Protocol Builder.
PROT_EVENT_ID:
type: number
example: 2
description: The unique id of the Pre Review event
DATEENTERED:
type: string
format: date_time
example: "2022-07-03 00:00:00+00:00"
description: The date this Pre Review event occurred
REVIEW_TYPE:
type: number
example: 3
description: The ID of the review type
UVA_STUDY_TRACKING:
type: number
example: 4
description: An identifier for the study. Should be the same as SS_STUDY_ID
COMMENTS:
type: string
format: string
example: Returned because reasons
description: A comment about the Pre Review
IRBREVIEWERADMIN:
type: string
format: string
example: abc13
description: The UVA user uid of the Reviewer Admin
FNAME:
type: string
format: string
example: Joanne
description: The first name of the Reviewer
LNAME:
type: string
format: string
example: Smith
description: The last name of the Reviewer
LOGIN:
type: string
format: string
example: xyz3a
description: The UVA user uid of the Reviewer
EVENT_TYPE:
type: number
example: 299
description: The ID for the event type (should be 299)
STATUS:
type: string
format: string
example: Error
description: Used when study has *not* been returned to the PI
DETAIL:
type: string
format: string
example: No records found.
description: Used when study has *not* been returned to the PI

View File

@ -5,7 +5,7 @@ from wtforms.widgets import DateInput
from wtforms_alchemy import ModelForm
from wtforms.validators import Optional
from pb.models import RequiredDocument, Investigator, StudyDetails, IRBStatus, IRBInfo, IRBInfoEvent, IRBInfoStatus
from pb.models import RequiredDocument, Investigator, IRBStatus, IRBInfoEvent, IRBInfoStatus
class StudyForm(FlaskForm):
@ -209,6 +209,11 @@ class StudyTable(Table):
anchor_attrs={'class': 'btn btn-icon btn-accent', 'title': 'Edit Info'},
th_html_attrs={'class': 'mat-icon text-center', 'title': 'Edit Info'}
)
pre_review = LinkCol(
'check', 'edit_pre_review', url_kwargs=dict(study_id='STUDYID'),
anchor_attrs={'class': 'btn btn-icon btn-accent', 'title': 'Pre Review'},
th_html_attrs={'class': 'mat-icon text-center', 'title': 'Pre Review'}
)
STUDYID = Col('Study Id')
TITLE = Col('Title')
NETBADGEID = Col('User')
@ -223,3 +228,16 @@ class StudyTable(Table):
th_html_attrs={'class': 'mat-icon text-center', 'title': 'Delete Study'}
)
class PreReviewForm(FlaskForm):
SS_STUDY_ID = HiddenField()
UVA_STUDY_TRACKING = StringField('UVA_STUDY_TRACKING', render_kw={'readonly': True})
DATEENTERED = DateField('DATEENTERED', widget=DateInput())
REVIEW_TYPE = IntegerField('REVIEW_TYPE')
COMMENTS = StringField('COMMENTS')
IRBREVIEWERADMIN = StringField('IRBREVIEWERADMIN')
FNAME = StringField('FNAME')
LNAME = StringField('LNAME')
LOGIN = StringField('LOGIN')
EVENT_TYPE = IntegerField('EVENT_TYPE', render_kw={'readonly': True})
STATUS_DETAIL = SelectField("STATUS/DETAIL", choices=['', 'Error: No Records Found', 'Record: Study returned to PI.'])

View File

@ -458,3 +458,38 @@ class SelectedUserSchema(ma.Schema):
class Meta:
fields = ("user_id", "selected_user")
class PreReview(db.Model):
__tablename__ = 'pre_review'
PROT_EVENT_ID = db.Column(db.Integer, primary_key=True)
SS_STUDY_ID = db.Column(db.Integer, db.ForeignKey('study.STUDYID'))
DATEENTERED = db.Column(db.DateTime(timezone=True), default=func.now())
REVIEW_TYPE = db.Column(db.Integer)
COMMENTS = db.Column(db.String)
IRBREVIEWERADMIN = db.Column(db.String)
FNAME = db.Column(db.String)
LNAME = db.Column(db.String)
LOGIN = db.Column(db.String)
EVENT_TYPE = db.Column(db.Integer)
STATUS = db.Column(db.String)
DETAIL = db.Column(db.String)
class PreReviewSchema(ma.Schema):
class Meta:
model = PreReview
fields = ["SS_STUDY_ID", "PROT_EVENT_ID", "DATEENTERED", "REVIEW_TYPE", "UVA_STUDY_TRACKING",
"COMMENTS", "IRBREVIEWERADMIN", "FNAME", "LNAME", "LOGIN", "EVENT_TYPE", "STATUS", "DETAIL"]
UVA_STUDY_TRACKING = fields.Method('get_uva_study_tracking', dump_only=True)
@staticmethod
def get_uva_study_tracking(obj):
return obj.SS_STUDY_ID
class PreReviewErrorSchema(ma.Schema):
class Meta:
model = PreReview
fields = ["STATUS", "DETAIL"]

View File

@ -4,8 +4,8 @@ from pb.ldap.ldap_service import LdapService
from pb.pb_mock import get_current_user, get_selected_user, update_selected_user, \
render_study_template, _update_study, redirect_home, _update_irb_info, _allowed_file, \
process_csv_study_details, has_no_empty_params, verify_required_document_list, verify_study_details_list
from pb.forms import StudyForm, IRBInfoForm, InvestigatorForm, ConfirmDeleteForm, StudySponsorForm, StudyDetailsForm
from pb.models import Study, StudyDetails, IRBInfo, IRBInfoEvent, IRBInfoStatus, IRBStatus, Investigator, Sponsor, StudySponsor, RequiredDocument
from pb.forms import StudyForm, IRBInfoForm, InvestigatorForm, ConfirmDeleteForm, StudySponsorForm, StudyDetailsForm, PreReviewForm
from pb.models import Study, StudyDetails, IRBInfo, IRBInfoEvent, IRBInfoStatus, IRBStatus, Investigator, Sponsor, StudySponsor, RequiredDocument, PreReview
import json
@ -441,3 +441,62 @@ def verify_study_details():
else:
flash('Study details are not up to date.', 'failure')
return redirect_home()
@app.route('/edit_pre_review/<study_id>', methods=['GET', 'POST'])
def edit_pre_review(study_id):
study = db.session.query(Study).filter(Study.STUDYID == study_id).first()
pre_review_models = db.session.query(PreReview).filter(PreReview.SS_STUDY_ID == study_id).all()
pre_reviews = []
for model in pre_review_models:
pre_reviews.append({
'DATEENTERED': model.DATEENTERED,
'COMMENTS': model.COMMENTS,
'PROT_EVENT_ID': model.PROT_EVENT_ID,
'form_action': BASE_HREF + "/delete_pre_review/" + str(model.PROT_EVENT_ID)
})
form = PreReviewForm(request.form, obj=study)
if request.method == 'GET':
form.UVA_STUDY_TRACKING.data = study_id
form.EVENT_TYPE.data = 299
if request.method == 'POST':
if form.validate():
pre_review = PreReview(
SS_STUDY_ID=study_id,
DATEENTERED=form.DATEENTERED.data,
REVIEW_TYPE=form.REVIEW_TYPE.data,
COMMENTS=form.COMMENTS.data,
IRBREVIEWERADMIN=form.IRBREVIEWERADMIN.data,
FNAME=form.FNAME.data,
LNAME=form.LNAME.data,
LOGIN=form.LOGIN.data,
EVENT_TYPE=299,
STATUS='Record',
DETAIL='Study returned to PI.'
)
session.add(pre_review)
session.commit()
flash('Pre-Review updated successfully!', 'success')
return redirect_home()
action = BASE_HREF + "/edit_pre_review/" + study_id
title = "Edit Pre Review"
return render_template(
'form.html',
form=form,
action=action,
title=title,
description_map={'PROT_EVENT_ID': 'Assigned by DB'},
base_href=BASE_HREF,
pre_reviews=pre_reviews
)
@app.route('/delete_pre_review/<pre_review_id>', methods=['POST'])
def delete_pre_review(pre_review_id):
model = session.query(PreReview).filter(PreReview.PROT_EVENT_ID == pre_review_id).first()
study_id = model.SS_STUDY_ID
session.delete(model)
session.commit()
redirect_url = url_for("edit_pre_review", study_id=study_id)
return redirect(redirect_url)

View File

@ -24,6 +24,23 @@
</form>
{% endif %}
{% if 'Edit Pre Review' in title %}
<div>
<div>Pre Reviews</div>
<div>
{% for review in pre_reviews %}
<div>
<p>Date: {{ review.DATEENTERED }}</p>
<p>&nbsp; &nbsp;Comments: {{ review.COMMENTS }}</p>
<form action="{{ review.form_action }}" method="post">
<input type=submit value=Delete label="Delete">
</form>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<form action="{{ action }}" method="post">
{{ form.csrf_token() }}

View File

@ -12,21 +12,23 @@ from pb import app, db, session
from pb.api import current_irb_info
from pb.forms import StudyForm, StudySponsorForm
from pb.ldap.ldap_service import LdapService
from pb.models import Study, RequiredDocument, Sponsor, StudySponsor, IRBStatus, Investigator, IRBInfo, StudyDetails, IRBInfoEvent, IRBInfoStatus
from pb.models import Study, RequiredDocument, Sponsor, StudySponsor, IRBStatus, Investigator, IRBInfo, StudyDetails, IRBInfoEvent, IRBInfoStatus, PreReview
from example_data import ExampleDataLoader
class Sanity_Check_Test(unittest.TestCase):
class TestSanity(unittest.TestCase):
auths = {}
@classmethod
def setUpClass(cls):
ExampleDataLoader().clean_db()
cls.ctx = app.test_request_context()
cls.app = app.test_client()
db.create_all()
@classmethod
def tearDownClass(cls):
ExampleDataLoader().clean_db()
db.drop_all()
def setUp(self):
@ -72,6 +74,25 @@ class Sanity_Check_Test(unittest.TestCase):
return added_study
@staticmethod
def add_pre_review(study_id, i):
pre_review = PreReview(
SS_STUDY_ID=study_id,
DATEENTERED=None,
REVIEW_TYPE=2,
COMMENTS=f'This is my comment {i}',
IRBREVIEWERADMIN=f'abc-{i}',
FNAME=f'Firstname_{i}',
LNAME=f'Lastname_{i}',
LOGIN=f'login_{i}',
EVENT_TYPE=299,
STATUS='Record',
DETAIL='Study returned to PI.'
)
session.add(pre_review)
session.commit()
return pre_review
def test_add_and_edit_study(self):
"""Add and edit a study"""
added_study: Study = self.add_study()
@ -272,3 +293,44 @@ class Sanity_Check_Test(unittest.TestCase):
self.assertIsNone(api_irb_info[0]['IRBEVENT_ID'])
self.assertIsNone(api_irb_info[0]['STATUS'])
self.assertIsNone(api_irb_info[0]['DETAIL'])
def test_pre_review(self):
study = self.add_study()
for i in range(5):
self.add_pre_review(study.STUDYID, i)
result = self.app.get(f'/v2.0/pre_reviews/{study.STUDYID}', follow_redirects=False)
reviews = json.loads(result.get_data(as_text=True))
self.assertEqual(len(reviews), 5)
for i in range(5):
self.assertEqual(reviews[i]['COMMENTS'], f'This is my comment {i}')
self.assertEqual(reviews[i]['PROT_EVENT_ID'], i + 1) # python starts at 0, postgres starts at 1
self.assertEqual(reviews[i]['STATUS'], 'Record')
def test_pre_review_no_review(self):
study = self.add_study()
result = self.app.get(f'/v2.0/pre_reviews/{study.STUDYID}', follow_redirects=False)
reviews = json.loads(result.get_data(as_text=True))
self.assertEqual(len(reviews), 2)
self.assertEqual(reviews['STATUS'], 'Error')
self.assertEqual(reviews['DETAIL'], 'No records found.')
def test_pre_review_delete(self):
study = self.add_study()
for i in range(2):
self.add_pre_review(study.STUDYID, i)
result = self.app.get(f'/v2.0/pre_reviews/{study.STUDYID}', follow_redirects=False)
reviews = json.loads(result.get_data(as_text=True))
self.assertEqual(len(reviews), 2)
review_0_id = reviews[0]['PROT_EVENT_ID']
review_1_id = reviews[1]['PROT_EVENT_ID']
self.assertEqual(reviews[0]['COMMENTS'], 'This is my comment 0')
self.assertEqual(reviews[1]['COMMENTS'], 'This is my comment 1')
self.app.post(f'/delete_pre_review/{review_0_id}')
result = self.app.get(f'/v2.0/pre_reviews/{study.STUDYID}', follow_redirects=False)
reviews = json.loads(result.get_data(as_text=True))
self.assertEqual(len(reviews), 1)
self.assertEqual(reviews[0]['PROT_EVENT_ID'], review_1_id)
self.assertEqual(reviews[0]['COMMENTS'], 'This is my comment 1')