Merge pull request #84 from sartography/chore/return-to-pi-api-759
Chore/return to pi api #759
This commit is contained in:
commit
65e9f9f752
|
@ -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()
|
||||
|
||||
|
|
|
@ -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')
|
11
pb/api.py
11
pb/api.py
|
@ -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)
|
||||
|
|
84
pb/api.yml
84
pb/api.yml
|
@ -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
|
||||
|
|
20
pb/forms.py
20
pb/forms.py
|
@ -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.'])
|
||||
|
|
35
pb/models.py
35
pb/models.py
|
@ -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"]
|
||||
|
|
63
pb/routes.py
63
pb/routes.py
|
@ -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)
|
||||
|
|
|
@ -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> 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() }}
|
||||
|
|
|
@ -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')
|
||||
|
|
Loading…
Reference in New Issue