mirror of
https://github.com/sartography/cr-connect-workflow.git
synced 2025-02-22 12:48:25 +00:00
Merge branch 'rrt/dev' into rrt/testing
This commit is contained in:
commit
1d2f074312
@ -17,7 +17,6 @@ RUN pip install pipenv && \
|
|||||||
COPY . /app/
|
COPY . /app/
|
||||||
|
|
||||||
ENV FLASK_APP=/app/crc/__init__.py
|
ENV FLASK_APP=/app/crc/__init__.py
|
||||||
CMD ["pipenv", "run", "flask", "db", "upgrade"]
|
|
||||||
CMD ["pipenv", "run", "python", "/app/run.py"]
|
CMD ["pipenv", "run", "python", "/app/run.py"]
|
||||||
|
|
||||||
# expose ports
|
# expose ports
|
||||||
|
1
Pipfile
1
Pipfile
@ -31,7 +31,6 @@ sphinx = "*"
|
|||||||
recommonmark = "*"
|
recommonmark = "*"
|
||||||
psycopg2-binary = "*"
|
psycopg2-binary = "*"
|
||||||
docxtpl = "*"
|
docxtpl = "*"
|
||||||
flask-sso = "*"
|
|
||||||
python-dateutil = "*"
|
python-dateutil = "*"
|
||||||
pandas = "*"
|
pandas = "*"
|
||||||
xlrd = "*"
|
xlrd = "*"
|
||||||
|
31
Pipfile.lock
generated
31
Pipfile.lock
generated
@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "26d23456010d3e5a559386d412cef3beacd92d5a4e474f2afdb0737ea0f20f04"
|
"sha256": "1ca737db75750ea4351c15b4b0b26155d90bc5522705ed293a0c2773600b6a0a"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
"python_version": "3.7"
|
"python_version": "3.6.9"
|
||||||
},
|
},
|
||||||
"sources": [
|
"sources": [
|
||||||
{
|
{
|
||||||
@ -96,12 +96,6 @@
|
|||||||
],
|
],
|
||||||
"version": "==3.6.3.0"
|
"version": "==3.6.3.0"
|
||||||
},
|
},
|
||||||
"blinker": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:471aee25f3992bd325afa3772f1063dbdbbca947a041b8b89466dc00d606f8b6"
|
|
||||||
],
|
|
||||||
"version": "==1.4"
|
|
||||||
},
|
|
||||||
"celery": {
|
"celery": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:108a0bf9018a871620936c33a3ee9f6336a89f8ef0a0f567a9001f4aa361415f",
|
"sha256:108a0bf9018a871620936c33a3ee9f6336a89f8ef0a0f567a9001f4aa361415f",
|
||||||
@ -307,13 +301,6 @@
|
|||||||
],
|
],
|
||||||
"version": "==2.4.1"
|
"version": "==2.4.1"
|
||||||
},
|
},
|
||||||
"flask-sso": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:541a8a2387c6eac4325c53f8f7f863a03173b37aa558a37a430010d7fc1a3633"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==0.4.0"
|
|
||||||
},
|
|
||||||
"future": {
|
"future": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"
|
"sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"
|
||||||
@ -711,10 +698,10 @@
|
|||||||
},
|
},
|
||||||
"six": {
|
"six": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
|
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
|
||||||
"sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"
|
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
|
||||||
],
|
],
|
||||||
"version": "==1.14.0"
|
"version": "==1.15.0"
|
||||||
},
|
},
|
||||||
"snowballstemmer": {
|
"snowballstemmer": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -783,7 +770,7 @@
|
|||||||
"spiffworkflow": {
|
"spiffworkflow": {
|
||||||
"editable": true,
|
"editable": true,
|
||||||
"git": "https://github.com/sartography/SpiffWorkflow.git",
|
"git": "https://github.com/sartography/SpiffWorkflow.git",
|
||||||
"ref": "2c9698894f7e526a91bf3ca8c4b9fc9b6b01e807"
|
"ref": "cb098ee6d55b85bf7795997f4ad5f78c27d15381"
|
||||||
},
|
},
|
||||||
"sqlalchemy": {
|
"sqlalchemy": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -955,10 +942,10 @@
|
|||||||
},
|
},
|
||||||
"six": {
|
"six": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
|
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
|
||||||
"sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"
|
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
|
||||||
],
|
],
|
||||||
"version": "==1.14.0"
|
"version": "==1.15.0"
|
||||||
},
|
},
|
||||||
"wcwidth": {
|
"wcwidth": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -27,20 +27,8 @@ TOKEN_AUTH_SECRET_KEY = environ.get('TOKEN_AUTH_SECRET_KEY', default="Shhhh!!! T
|
|||||||
FRONTEND_AUTH_CALLBACK = environ.get('FRONTEND_AUTH_CALLBACK', default="http://localhost:4200/session")
|
FRONTEND_AUTH_CALLBACK = environ.get('FRONTEND_AUTH_CALLBACK', default="http://localhost:4200/session")
|
||||||
SWAGGER_AUTH_KEY = environ.get('SWAGGER_AUTH_KEY', default="SWAGGER")
|
SWAGGER_AUTH_KEY = environ.get('SWAGGER_AUTH_KEY', default="SWAGGER")
|
||||||
|
|
||||||
#: Default attribute map for single signon.
|
|
||||||
SSO_LOGIN_URL = '/login'
|
|
||||||
SSO_ATTRIBUTE_MAP = {
|
|
||||||
'eppn': (False, 'eppn'), # dhf8r@virginia.edu
|
|
||||||
'uid': (True, 'uid'), # dhf8r
|
|
||||||
'givenName': (False, 'first_name'), # Daniel
|
|
||||||
'mail': (False, 'email_address'), # dhf8r@Virginia.EDU
|
|
||||||
'sn': (False, 'last_name'), # Funk
|
|
||||||
'affiliation': (False, 'affiliation'), # 'staff@virginia.edu;member@virginia.edu'
|
|
||||||
'displayName': (False, 'display_name'), # Daniel Harold Funk
|
|
||||||
'title': (False, 'title') # SOFTWARE ENGINEER V
|
|
||||||
}
|
|
||||||
|
|
||||||
# %s/%i placeholders expected for uva_id and study_id in various calls.
|
# %s/%i placeholders expected for uva_id and study_id in various calls.
|
||||||
|
PB_ENABLED = environ.get('PB_ENABLED', default=True)
|
||||||
PB_BASE_URL = environ.get('PB_BASE_URL', default="http://localhost:5001/pb/")
|
PB_BASE_URL = environ.get('PB_BASE_URL', default="http://localhost:5001/pb/")
|
||||||
PB_USER_STUDIES_URL = environ.get('PB_USER_STUDIES_URL', default=PB_BASE_URL + "user_studies?uva_id=%s")
|
PB_USER_STUDIES_URL = environ.get('PB_USER_STUDIES_URL', default=PB_BASE_URL + "user_studies?uva_id=%s")
|
||||||
PB_INVESTIGATORS_URL = environ.get('PB_INVESTIGATORS_URL', default=PB_BASE_URL + "investigators?studyid=%i")
|
PB_INVESTIGATORS_URL = environ.get('PB_INVESTIGATORS_URL', default=PB_BASE_URL + "investigators?studyid=%i")
|
||||||
|
@ -6,6 +6,7 @@ DEVELOPMENT = True
|
|||||||
TESTING = True
|
TESTING = True
|
||||||
SQLALCHEMY_DATABASE_URI = "postgresql://crc_user:crc_pass@localhost:5432/crc_test"
|
SQLALCHEMY_DATABASE_URI = "postgresql://crc_user:crc_pass@localhost:5432/crc_test"
|
||||||
TOKEN_AUTH_SECRET_KEY = "Shhhh!!! This is secret! And better darn well not show up in prod."
|
TOKEN_AUTH_SECRET_KEY = "Shhhh!!! This is secret! And better darn well not show up in prod."
|
||||||
|
PB_ENABLED = False
|
||||||
|
|
||||||
print('### USING TESTING CONFIG: ###')
|
print('### USING TESTING CONFIG: ###')
|
||||||
print('SQLALCHEMY_DATABASE_URI = ', SQLALCHEMY_DATABASE_URI)
|
print('SQLALCHEMY_DATABASE_URI = ', SQLALCHEMY_DATABASE_URI)
|
||||||
|
@ -6,7 +6,6 @@ from flask_cors import CORS
|
|||||||
from flask_marshmallow import Marshmallow
|
from flask_marshmallow import Marshmallow
|
||||||
from flask_migrate import Migrate
|
from flask_migrate import Migrate
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from flask_sso import SSO
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
@ -31,7 +30,6 @@ session = db.session
|
|||||||
|
|
||||||
migrate = Migrate(app, db)
|
migrate = Migrate(app, db)
|
||||||
ma = Marshmallow(app)
|
ma = Marshmallow(app)
|
||||||
sso = SSO(app=app)
|
|
||||||
|
|
||||||
from crc import models
|
from crc import models
|
||||||
from crc import api
|
from crc import api
|
||||||
|
100
crc/api.yml
100
crc/api.yml
@ -110,6 +110,19 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Study"
|
$ref: "#/components/schemas/Study"
|
||||||
|
/study-files:
|
||||||
|
get:
|
||||||
|
operationId: crc.api.study.all_studies_and_files
|
||||||
|
summary: Provides a list of studies with submitted files
|
||||||
|
tags:
|
||||||
|
- Studies
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: An array of studies, with submitted files, ordered by the last modified date.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Study"
|
||||||
/study/{study_id}:
|
/study/{study_id}:
|
||||||
parameters:
|
parameters:
|
||||||
- name: study_id
|
- name: study_id
|
||||||
@ -156,26 +169,6 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Study"
|
$ref: "#/components/schemas/Study"
|
||||||
/study-update/{study_id}:
|
|
||||||
post:
|
|
||||||
operationId: crc.api.study.post_update_study_from_protocol_builder
|
|
||||||
summary: If the study is up-to-date with Protocol Builder, returns a 304 Not Modified. If out of date, return a 202 Accepted and study state changes to updating.
|
|
||||||
tags:
|
|
||||||
- Study Status
|
|
||||||
parameters:
|
|
||||||
- name: study_id
|
|
||||||
in: path
|
|
||||||
required: true
|
|
||||||
description: The id of the study that should be checked for updates.
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
format: int32
|
|
||||||
responses:
|
|
||||||
'304':
|
|
||||||
description: Study is currently up to date and does not need to be reloaded from Protocol Builder
|
|
||||||
'202':
|
|
||||||
description: Request accepted, will preform an update. Study state set to "updating"
|
|
||||||
|
|
||||||
/workflow-specification:
|
/workflow-specification:
|
||||||
get:
|
get:
|
||||||
operationId: crc.api.workflow.all_specifications
|
operationId: crc.api.workflow.all_specifications
|
||||||
@ -755,13 +748,6 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
/render_docx:
|
/render_docx:
|
||||||
parameters:
|
|
||||||
- name: data
|
|
||||||
in: query
|
|
||||||
required: true
|
|
||||||
description: The json data to use in populating the template
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
put:
|
put:
|
||||||
operationId: crc.api.tools.render_docx
|
operationId: crc.api.tools.render_docx
|
||||||
security: [] # Disable security for this endpoint only.
|
security: [] # Disable security for this endpoint only.
|
||||||
@ -777,6 +763,9 @@ paths:
|
|||||||
file:
|
file:
|
||||||
type: string
|
type: string
|
||||||
format: binary
|
format: binary
|
||||||
|
data:
|
||||||
|
type: string
|
||||||
|
format: json
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Returns the generated document.
|
description: Returns the generated document.
|
||||||
@ -802,6 +791,54 @@ paths:
|
|||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: "#/components/schemas/Script"
|
$ref: "#/components/schemas/Script"
|
||||||
|
/approval:
|
||||||
|
parameters:
|
||||||
|
- name: approver_uid
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
description: Restrict results to a given approver uid, maybe we restrict the use of this at somepoint.
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
get:
|
||||||
|
operationId: crc.api.approval.get_approvals
|
||||||
|
summary: Provides a list of workflows approvals
|
||||||
|
tags:
|
||||||
|
- Approvals
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: An array of approvals
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/Approval"
|
||||||
|
/approval/{approval_id}:
|
||||||
|
parameters:
|
||||||
|
- name: approval_id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
description: The id of the approval in question.
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
format: int32
|
||||||
|
put:
|
||||||
|
operationId: crc.api.approval.update_approval
|
||||||
|
summary: Updates an approval with the given parameters
|
||||||
|
tags:
|
||||||
|
- Approvals
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Approval'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Study updated successfully.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Approval"
|
||||||
components:
|
components:
|
||||||
securitySchemes:
|
securitySchemes:
|
||||||
jwt:
|
jwt:
|
||||||
@ -1209,7 +1246,10 @@ components:
|
|||||||
readOnly: true
|
readOnly: true
|
||||||
task:
|
task:
|
||||||
$ref: "#/components/schemas/Task"
|
$ref: "#/components/schemas/Task"
|
||||||
|
Approval:
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: number
|
||||||
|
format: integer
|
||||||
|
example: 5
|
||||||
|
|
||||||
|
10
crc/api/approval.py
Normal file
10
crc/api/approval.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
from crc.models.approval import ApprovalModel, Approval
|
||||||
|
|
||||||
|
|
||||||
|
def get_approvals(approver_uid = None):
|
||||||
|
approval_model = ApprovalModel()
|
||||||
|
approval = Approval.from_model(approval_model)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def update_approval(approval_id):
|
||||||
|
return {}
|
@ -1,20 +1,16 @@
|
|||||||
from typing import List
|
|
||||||
|
|
||||||
from connexion import NoContent
|
|
||||||
from flask import g
|
from flask import g
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
|
||||||
from crc import session
|
from crc import session
|
||||||
from crc.api.common import ApiError, ApiErrorSchema
|
from crc.api.common import ApiError, ApiErrorSchema
|
||||||
from crc.models.protocol_builder import ProtocolBuilderStatus, ProtocolBuilderStudy
|
from crc.models.protocol_builder import ProtocolBuilderStatus, ProtocolBuilderStudy
|
||||||
from crc.models.study import StudySchema, StudyModel, Study
|
from crc.models.study import StudySchema, StudyFilesSchema, StudyModel, Study
|
||||||
from crc.services.protocol_builder import ProtocolBuilderService
|
from crc.services.protocol_builder import ProtocolBuilderService
|
||||||
from crc.services.study_service import StudyService
|
from crc.services.study_service import StudyService
|
||||||
|
|
||||||
|
|
||||||
def add_study(body):
|
def add_study(body):
|
||||||
"""This should never get called, and is subject to deprication. Studies
|
"""Or any study like object. """
|
||||||
should be added through the protocol builder only."""
|
|
||||||
study: Study = StudySchema().load(body)
|
study: Study = StudySchema().load(body)
|
||||||
study_model = StudyModel(**study.model_args())
|
study_model = StudyModel(**study.model_args())
|
||||||
session.add(study_model)
|
session.add(study_model)
|
||||||
@ -59,30 +55,17 @@ def delete_study(study_id):
|
|||||||
|
|
||||||
|
|
||||||
def all_studies():
|
def all_studies():
|
||||||
"""Returns all the studies associated with the current user. Assures we are
|
"""Returns all the studies associated with the current user. """
|
||||||
in sync with values read in from the protocol builder. """
|
StudyService.synch_with_protocol_builder_if_enabled(g.user)
|
||||||
StudyService.synch_all_studies_with_protocol_builder(g.user)
|
|
||||||
studies = StudyService.get_studies_for_user(g.user)
|
studies = StudyService.get_studies_for_user(g.user)
|
||||||
results = StudySchema(many=True).dump(studies)
|
results = StudySchema(many=True).dump(studies)
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
def post_update_study_from_protocol_builder(study_id):
|
def all_studies_and_files():
|
||||||
"""Update a single study based on data received from
|
"""Returns all studies with submitted files"""
|
||||||
the protocol builder."""
|
studies = StudyService.get_studies_with_files()
|
||||||
|
results = StudyFilesSchema(many=True).dump(studies)
|
||||||
db_study = session.query(StudyModel).filter_by(study_id=study_id).all()
|
return results
|
||||||
pb_studies: List[ProtocolBuilderStudy] = ProtocolBuilderService.get_studies(g.user.uid)
|
|
||||||
pb_study = next((pbs for pbs in pb_studies if pbs.STUDYID == study_id), None)
|
|
||||||
if pb_study:
|
|
||||||
db_study.update_from_protocol_builder(pb_study)
|
|
||||||
else:
|
|
||||||
db_study.inactive = True
|
|
||||||
db_study.protocol_builder_status = ProtocolBuilderStatus.ABANDONED
|
|
||||||
|
|
||||||
return NoContent, 304
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -25,13 +25,14 @@ def render_markdown(data, template):
|
|||||||
raise ApiError(code="invalid", message=str(e))
|
raise ApiError(code="invalid", message=str(e))
|
||||||
|
|
||||||
|
|
||||||
def render_docx(data):
|
def render_docx():
|
||||||
"""
|
"""
|
||||||
Provides a quick way to verify that a Jinja docx template will work properly on a given json
|
Provides a quick way to verify that a Jinja docx template will work properly on a given json
|
||||||
data structure. Useful for folks that are building these templates.
|
data structure. Useful for folks that are building these templates.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
file = connexion.request.files['file']
|
file = connexion.request.files['file']
|
||||||
|
data = connexion.request.form['data']
|
||||||
target_stream = CompleteTemplate().make_template(file, json.loads(data))
|
target_stream = CompleteTemplate().make_template(file, json.loads(data))
|
||||||
return send_file(
|
return send_file(
|
||||||
io.BytesIO(target_stream.read()),
|
io.BytesIO(target_stream.read()),
|
||||||
|
107
crc/api/user.py
107
crc/api/user.py
@ -1,12 +1,12 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
import connexion
|
import connexion
|
||||||
from flask import redirect, g
|
from flask import redirect, g, request
|
||||||
|
|
||||||
from crc import sso, app, db
|
from crc import app, db
|
||||||
from crc.api.common import ApiError
|
from crc.api.common import ApiError
|
||||||
from crc.models.user import UserModel, UserModelSchema
|
from crc.models.user import UserModel, UserModelSchema
|
||||||
|
from crc.services.ldap_service import LdapService, LdapUserInfo
|
||||||
|
|
||||||
"""
|
"""
|
||||||
.. module:: crc.api.user
|
.. module:: crc.api.user
|
||||||
@ -32,53 +32,76 @@ def verify_token(token):
|
|||||||
def get_current_user():
|
def get_current_user():
|
||||||
return UserModelSchema().dump(g.user)
|
return UserModelSchema().dump(g.user)
|
||||||
|
|
||||||
|
@app.route('/v1.0/login')
|
||||||
|
def sso_login():
|
||||||
|
# This what I see coming back:
|
||||||
|
# X-Remote-Cn: Daniel Harold Funk (dhf8r)
|
||||||
|
# X-Remote-Sn: Funk
|
||||||
|
# X-Remote-Givenname: Daniel
|
||||||
|
# X-Remote-Uid: dhf8r
|
||||||
|
# Eppn: dhf8r@virginia.edu
|
||||||
|
# Cn: Daniel Harold Funk (dhf8r)
|
||||||
|
# Sn: Funk
|
||||||
|
# Givenname: Daniel
|
||||||
|
# Uid: dhf8r
|
||||||
|
# X-Remote-User: dhf8r@virginia.edu
|
||||||
|
# X-Forwarded-For: 128.143.0.10
|
||||||
|
# X-Forwarded-Host: dev.crconnect.uvadcos.io
|
||||||
|
# X-Forwarded-Server: dev.crconnect.uvadcos.io
|
||||||
|
# Connection: Keep-Alive
|
||||||
|
uid = request.headers.get("Uid")
|
||||||
|
if not uid:
|
||||||
|
uid = request.headers.get("X-Remote-Uid")
|
||||||
|
|
||||||
@sso.login_handler
|
if not uid:
|
||||||
def sso_login(user_info):
|
raise ApiError("invalid_sso_credentials", "'Uid' nor 'X-Remote-Uid' were present in the headers: %s"
|
||||||
app.logger.info("Login from Shibboleth happening. " + json.dump(user_info))
|
% str(request.headers))
|
||||||
# TODO: Get redirect URL from Shibboleth request header
|
|
||||||
_handle_login(user_info)
|
redirect = request.args.get('redirect')
|
||||||
|
app.logger.info("SSO_LOGIN: Full URL: " + request.url)
|
||||||
|
app.logger.info("SSO_LOGIN: User Id: " + uid)
|
||||||
|
app.logger.info("SSO_LOGIN: Will try to redirect to : " + str(redirect))
|
||||||
|
|
||||||
|
ldap_service = LdapService()
|
||||||
|
info = ldap_service.user_info(uid)
|
||||||
|
|
||||||
|
return _handle_login(info, redirect)
|
||||||
|
|
||||||
|
@app.route('/sso')
|
||||||
|
def sso():
|
||||||
|
response = ""
|
||||||
|
response += "<h1>Headers</h1>"
|
||||||
|
response += "<ul>"
|
||||||
|
for k,v in request.headers:
|
||||||
|
response += "<li><b>%s</b> %s</li>\n" % (k, v)
|
||||||
|
response += "<h1>Environment</h1>"
|
||||||
|
for k,v in request.environ:
|
||||||
|
response += "<li><b>%s</b> %s</li>\n" % (k, v)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
def _handle_login(user_info, redirect_url=app.config['FRONTEND_AUTH_CALLBACK']):
|
def _handle_login(user_info: LdapUserInfo, redirect_url=app.config['FRONTEND_AUTH_CALLBACK']):
|
||||||
"""On successful login, adds user to database if the user is not already in the system,
|
"""On successful login, adds user to database if the user is not already in the system,
|
||||||
then returns the frontend auth callback URL, with auth token appended.
|
then returns the frontend auth callback URL, with auth token appended.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
user_info (dict of {
|
user_info - an ldap user_info object.
|
||||||
uid: str,
|
redirect_url: Optional[str]
|
||||||
affiliation: Optional[str],
|
|
||||||
display_name: Optional[str],
|
|
||||||
email_address: Optional[str],
|
|
||||||
eppn: Optional[str],
|
|
||||||
first_name: Optional[str],
|
|
||||||
last_name: Optional[str],
|
|
||||||
title: Optional[str],
|
|
||||||
}): Dictionary of user attributes
|
|
||||||
redirect_url: Optional[str]
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Response. 302 - Redirects to the frontend auth callback URL, with auth token appended.
|
Response. 302 - Redirects to the frontend auth callback URL, with auth token appended.
|
||||||
"""
|
"""
|
||||||
uid = user_info['uid']
|
user = db.session.query(UserModel).filter(UserModel.uid == user_info.uid).first()
|
||||||
user = db.session.query(UserModel).filter(UserModel.uid == uid).first()
|
|
||||||
|
|
||||||
if user is None:
|
if user is None:
|
||||||
# Add new user
|
# Add new user
|
||||||
user = UserModelSchema().load(user_info, session=db.session)
|
user = UserModel()
|
||||||
else:
|
|
||||||
# Update existing user data
|
|
||||||
user = UserModelSchema().load(user_info, session=db.session, instance=user, partial=True)
|
|
||||||
|
|
||||||
# Build display_name if not set
|
user.uid = user_info.uid
|
||||||
if 'display_name' not in user_info or len(user_info['display_name']) == 0:
|
user.display_name = user_info.display_name
|
||||||
display_name_list = []
|
user.email_address = user_info.email_address
|
||||||
|
user.affiliation = user_info.affiliation
|
||||||
for prop in ['first_name', 'last_name']:
|
user.title = user_info.title
|
||||||
if prop in user_info and len(user_info[prop]) > 0:
|
|
||||||
display_name_list.append(user_info[prop])
|
|
||||||
|
|
||||||
user.display_name = ' '.join(display_name_list)
|
|
||||||
|
|
||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
@ -86,10 +109,14 @@ def _handle_login(user_info, redirect_url=app.config['FRONTEND_AUTH_CALLBACK']):
|
|||||||
# Return the frontend auth callback URL, with auth token appended.
|
# Return the frontend auth callback URL, with auth token appended.
|
||||||
auth_token = user.encode_auth_token().decode()
|
auth_token = user.encode_auth_token().decode()
|
||||||
if redirect_url is not None:
|
if redirect_url is not None:
|
||||||
|
app.logger.info("SSO_LOGIN: REDIRECTING TO: " + redirect_url)
|
||||||
return redirect('%s/%s' % (redirect_url, auth_token))
|
return redirect('%s/%s' % (redirect_url, auth_token))
|
||||||
else:
|
else:
|
||||||
|
app.logger.info("SSO_LOGIN: NO REDIRECT, JUST RETURNING AUTH TOKEN.")
|
||||||
return auth_token
|
return auth_token
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def backdoor(
|
def backdoor(
|
||||||
uid=None,
|
uid=None,
|
||||||
affiliation=None,
|
affiliation=None,
|
||||||
@ -122,11 +149,9 @@ def backdoor(
|
|||||||
ApiError. If on production, returns a 404 error.
|
ApiError. If on production, returns a 404 error.
|
||||||
"""
|
"""
|
||||||
if not 'PRODUCTION' in app.config or not app.config['PRODUCTION']:
|
if not 'PRODUCTION' in app.config or not app.config['PRODUCTION']:
|
||||||
user_info = {}
|
ldap_info = LdapUserInfo()
|
||||||
for key in UserModel.__dict__.keys():
|
ldap_info.uid = connexion.request.args["uid"]
|
||||||
if key in connexion.request.args:
|
ldap_info.email_address = connexion.request.args["email_address"]
|
||||||
user_info[key] = connexion.request.args[key]
|
return _handle_login(ldap_info, redirect_url)
|
||||||
|
|
||||||
return _handle_login(user_info, redirect_url)
|
|
||||||
else:
|
else:
|
||||||
raise ApiError('404', 'unknown')
|
raise ApiError('404', 'unknown')
|
||||||
|
77
crc/models/approval.py
Normal file
77
crc/models/approval.py
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import enum
|
||||||
|
|
||||||
|
from marshmallow import INCLUDE
|
||||||
|
|
||||||
|
from crc import db, ma
|
||||||
|
from crc.models.study import StudyModel
|
||||||
|
from crc.models.workflow import WorkflowModel
|
||||||
|
|
||||||
|
|
||||||
|
class ApprovalStatus(enum.Enum):
|
||||||
|
WAITING = "WAITING" # no one has done jack.
|
||||||
|
APPROVED = "APPROVED" # approved by the reviewer
|
||||||
|
DECLINED = "DECLINED" # rejected by the reviewer
|
||||||
|
CANCELED = "CANCELED" # The document was replaced with a new version and this review is no longer needed.
|
||||||
|
|
||||||
|
|
||||||
|
class ApprovalModel(db.Model):
|
||||||
|
__tablename__ = 'approval'
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
study_id = db.Column(db.Integer, db.ForeignKey(StudyModel.id), nullable=False)
|
||||||
|
study = db.relationship(StudyModel, backref='approval', cascade='all,delete')
|
||||||
|
workflow_id = db.Column(db.Integer, db.ForeignKey(WorkflowModel.id), nullable=False)
|
||||||
|
workflow_version = db.Column(db.String)
|
||||||
|
approver_uid = db.Column(db.String) # Not linked to user model, as they may not have logged in yet.
|
||||||
|
status = db.Column(db.String)
|
||||||
|
message = db.Column(db.String)
|
||||||
|
|
||||||
|
|
||||||
|
class Approval(object):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_model(cls, model: ApprovalModel):
|
||||||
|
instance = cls()
|
||||||
|
|
||||||
|
instance.id = model.id
|
||||||
|
instance.workflow_version = model.workflow_version
|
||||||
|
instance.approver_uid = model.approver_uid
|
||||||
|
instance.status = model.status
|
||||||
|
instance.study_id = model.study_id
|
||||||
|
if model.study:
|
||||||
|
instance.title = model.study.title
|
||||||
|
|
||||||
|
|
||||||
|
class ApprovalSchema(ma.Schema):
|
||||||
|
class Meta:
|
||||||
|
model = Approval
|
||||||
|
fields = ["id", "workflow_version", "approver_uid", "status",
|
||||||
|
"study_id", "title"]
|
||||||
|
unknown = INCLUDE
|
||||||
|
|
||||||
|
# Carlos: Here is the data structure I was trying to imagine.
|
||||||
|
# If I were to continue down my current traing of thought, I'd create
|
||||||
|
# another class called just "Approval" that can take an ApprovalModel from the
|
||||||
|
# database and construct a data structure like this one, that can
|
||||||
|
# be provided to the API at an /approvals endpoint with GET and PUT
|
||||||
|
# dat = { "approvals": [
|
||||||
|
# {"id": 1,
|
||||||
|
# "study_id": 20,
|
||||||
|
# "workflow_id": 454,
|
||||||
|
# "study_title": "Dan Funk (dhf8r)", # Really it's just the name of the Principal Investigator
|
||||||
|
# "workflow_version": "21",
|
||||||
|
# "approver": { # Pulled from ldap
|
||||||
|
# "uid": "bgb22",
|
||||||
|
# "display_name": "Billy Bob (bgb22)",
|
||||||
|
# "title": "E42:He's a hoopy frood",
|
||||||
|
# "department": "E0:EN-Eng Study of Parallel Universes",
|
||||||
|
# },
|
||||||
|
# "files": [
|
||||||
|
# {
|
||||||
|
# "id": 124,
|
||||||
|
# "name": "ResearchRestart.docx",
|
||||||
|
# "content_type": "docx-something-whatever"
|
||||||
|
# }
|
||||||
|
# ]
|
||||||
|
# }
|
||||||
|
# ...
|
||||||
|
# ]
|
@ -6,7 +6,7 @@ from marshmallow_sqlalchemy import SQLAlchemyAutoSchema
|
|||||||
from sqlalchemy import func, Index
|
from sqlalchemy import func, Index
|
||||||
from sqlalchemy.dialects.postgresql import UUID
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
|
|
||||||
from crc import db
|
from crc import db, ma
|
||||||
|
|
||||||
|
|
||||||
class FileType(enum.Enum):
|
class FileType(enum.Enum):
|
||||||
@ -139,3 +139,9 @@ class LookupDataSchema(SQLAlchemyAutoSchema):
|
|||||||
include_relationships = False
|
include_relationships = False
|
||||||
include_fk = False # Includes foreign keys
|
include_fk = False # Includes foreign keys
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleFileSchema(ma.Schema):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = FileModel
|
||||||
|
fields = ["name"]
|
||||||
|
@ -5,6 +5,7 @@ from sqlalchemy import func
|
|||||||
|
|
||||||
from crc import db, ma
|
from crc import db, ma
|
||||||
from crc.api.common import ApiErrorSchema
|
from crc.api.common import ApiErrorSchema
|
||||||
|
from crc.models.file import FileModel, SimpleFileSchema
|
||||||
from crc.models.protocol_builder import ProtocolBuilderStatus, ProtocolBuilderStudy
|
from crc.models.protocol_builder import ProtocolBuilderStatus, ProtocolBuilderStudy
|
||||||
from crc.models.workflow import WorkflowSpecCategoryModel, WorkflowState, WorkflowStatus, WorkflowSpecModel, \
|
from crc.models.workflow import WorkflowSpecCategoryModel, WorkflowState, WorkflowStatus, WorkflowSpecModel, \
|
||||||
WorkflowModel
|
WorkflowModel
|
||||||
@ -39,6 +40,10 @@ class StudyModel(db.Model):
|
|||||||
if self.on_hold:
|
if self.on_hold:
|
||||||
self.protocol_builder_status = ProtocolBuilderStatus.HOLD
|
self.protocol_builder_status = ProtocolBuilderStatus.HOLD
|
||||||
|
|
||||||
|
def files(self):
|
||||||
|
_files = FileModel.query.filter_by(workflow_id=self.workflow[0].id)
|
||||||
|
return _files
|
||||||
|
|
||||||
|
|
||||||
class WorkflowMetadata(object):
|
class WorkflowMetadata(object):
|
||||||
def __init__(self, id, name, display_name, description, spec_version, category_id, state: WorkflowState, status: WorkflowStatus,
|
def __init__(self, id, name, display_name, description, spec_version, category_id, state: WorkflowState, status: WorkflowStatus,
|
||||||
@ -154,3 +159,16 @@ class StudySchema(ma.Schema):
|
|||||||
def make_study(self, data, **kwargs):
|
def make_study(self, data, **kwargs):
|
||||||
"""Can load the basic study data for updates to the database, but categories are write only"""
|
"""Can load the basic study data for updates to the database, but categories are write only"""
|
||||||
return Study(**data)
|
return Study(**data)
|
||||||
|
|
||||||
|
|
||||||
|
class StudyFilesSchema(ma.Schema):
|
||||||
|
|
||||||
|
# files = fields.List(fields.Nested(SimpleFileSchema), dump_only=True)
|
||||||
|
files = fields.Method('_files')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Study
|
||||||
|
additional = ["id", "title", "last_updated", "primary_investigator_id"]
|
||||||
|
|
||||||
|
def _files(self, obj):
|
||||||
|
return [file.name for file in obj.files()]
|
||||||
|
@ -19,6 +19,8 @@ class UserModel(db.Model):
|
|||||||
last_name = db.Column(db.String, nullable=True)
|
last_name = db.Column(db.String, nullable=True)
|
||||||
title = db.Column(db.String, nullable=True)
|
title = db.Column(db.String, nullable=True)
|
||||||
|
|
||||||
|
# Add Department and School
|
||||||
|
|
||||||
|
|
||||||
def encode_auth_token(self):
|
def encode_auth_token(self):
|
||||||
"""
|
"""
|
||||||
|
@ -73,10 +73,12 @@ class WorkflowModel(db.Model):
|
|||||||
bpmn_workflow_json = db.Column(db.JSON)
|
bpmn_workflow_json = db.Column(db.JSON)
|
||||||
status = db.Column(db.Enum(WorkflowStatus))
|
status = db.Column(db.Enum(WorkflowStatus))
|
||||||
study_id = db.Column(db.Integer, db.ForeignKey('study.id'))
|
study_id = db.Column(db.Integer, db.ForeignKey('study.id'))
|
||||||
|
study = db.relationship("StudyModel", backref='workflow')
|
||||||
workflow_spec_id = db.Column(db.String, db.ForeignKey('workflow_spec.id'))
|
workflow_spec_id = db.Column(db.String, db.ForeignKey('workflow_spec.id'))
|
||||||
workflow_spec = db.relationship("WorkflowSpecModel")
|
workflow_spec = db.relationship("WorkflowSpecModel")
|
||||||
spec_version = db.Column(db.String)
|
spec_version = db.Column(db.String)
|
||||||
total_tasks = db.Column(db.Integer, default=0)
|
total_tasks = db.Column(db.Integer, default=0)
|
||||||
completed_tasks = db.Column(db.Integer, default=0)
|
completed_tasks = db.Column(db.Integer, default=0)
|
||||||
# task_history = db.Column(db.ARRAY(db.String), default=[]) # The history stack of user completed tasks.
|
last_updated = db.Column(db.DateTime)
|
||||||
last_updated = db.Column(db.DateTime)
|
# todo: Add a version that represents the files associated with this workflow
|
||||||
|
# version = "32"
|
@ -8,17 +8,30 @@ from crc.api.common import ApiError
|
|||||||
|
|
||||||
class LdapUserInfo(object):
|
class LdapUserInfo(object):
|
||||||
|
|
||||||
def __init__(self, entry):
|
def __init__(self):
|
||||||
self.display_name = entry.displayName.value
|
self.display_name = ''
|
||||||
self.given_name = ", ".join(entry.givenName)
|
self.given_name = ''
|
||||||
self.email = entry.mail.value
|
self.email_address = ''
|
||||||
self.telephone_number = ", ".join(entry.telephoneNumber)
|
self.telephone_number = ''
|
||||||
self.title = ", ".join(entry.title)
|
self.title = ''
|
||||||
self.department = ", ".join(entry.uvaDisplayDepartment)
|
self.department = ''
|
||||||
self.affiliation = ", ".join(entry.uvaPersonIAMAffiliation)
|
self.affiliation = ''
|
||||||
self.sponsor_type = ", ".join(entry.uvaPersonSponsoredType)
|
self.sponsor_type = ''
|
||||||
self.uid = entry.uid.value
|
self.uid = ''
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_entry(cls, entry):
|
||||||
|
instance = cls()
|
||||||
|
instance.display_name = entry.displayName.value
|
||||||
|
instance.given_name = ", ".join(entry.givenName)
|
||||||
|
instance.email_address = entry.mail.value
|
||||||
|
instance.telephone_number = ", ".join(entry.telephoneNumber)
|
||||||
|
instance.title = ", ".join(entry.title)
|
||||||
|
instance.department = ", ".join(entry.uvaDisplayDepartment)
|
||||||
|
instance.affiliation = ", ".join(entry.uvaPersonIAMAffiliation)
|
||||||
|
instance.sponsor_type = ", ".join(entry.uvaPersonSponsoredType)
|
||||||
|
instance.uid = entry.uid.value
|
||||||
|
return instance
|
||||||
|
|
||||||
class LdapService(object):
|
class LdapService(object):
|
||||||
search_base = "ou=People,o=University of Virginia,c=US"
|
search_base = "ou=People,o=University of Virginia,c=US"
|
||||||
@ -50,7 +63,7 @@ class LdapService(object):
|
|||||||
if len(self.conn.entries) < 1:
|
if len(self.conn.entries) < 1:
|
||||||
raise ApiError("missing_ldap_record", "Unable to locate a user with id %s in LDAP" % uva_uid)
|
raise ApiError("missing_ldap_record", "Unable to locate a user with id %s in LDAP" % uva_uid)
|
||||||
entry = self.conn.entries[0]
|
entry = self.conn.entries[0]
|
||||||
return(LdapUserInfo(entry))
|
return LdapUserInfo.from_entry(entry)
|
||||||
|
|
||||||
def search_users(self, query, limit):
|
def search_users(self, query, limit):
|
||||||
search_string = LdapService.uid_search_string % query
|
search_string = LdapService.uid_search_string % query
|
||||||
@ -64,6 +77,6 @@ class LdapService(object):
|
|||||||
for entry in self.conn.entries:
|
for entry in self.conn.entries:
|
||||||
if count > limit:
|
if count > limit:
|
||||||
break
|
break
|
||||||
results.append(LdapUserInfo(entry))
|
results.append(LdapUserInfo.from_entry(entry))
|
||||||
count += 1
|
count += 1
|
||||||
return results
|
return results
|
||||||
|
@ -10,6 +10,7 @@ from crc.models.protocol_builder import ProtocolBuilderStudy, ProtocolBuilderStu
|
|||||||
|
|
||||||
|
|
||||||
class ProtocolBuilderService(object):
|
class ProtocolBuilderService(object):
|
||||||
|
ENABLED = app.config['PB_ENABLED']
|
||||||
STUDY_URL = app.config['PB_USER_STUDIES_URL']
|
STUDY_URL = app.config['PB_USER_STUDIES_URL']
|
||||||
INVESTIGATOR_URL = app.config['PB_INVESTIGATORS_URL']
|
INVESTIGATOR_URL = app.config['PB_INVESTIGATORS_URL']
|
||||||
REQUIRED_DOCS_URL = app.config['PB_REQUIRED_DOCS_URL']
|
REQUIRED_DOCS_URL = app.config['PB_REQUIRED_DOCS_URL']
|
||||||
@ -17,6 +18,7 @@ class ProtocolBuilderService(object):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_studies(user_id) -> {}:
|
def get_studies(user_id) -> {}:
|
||||||
|
ProtocolBuilderService.__enabled_or_raise()
|
||||||
if not isinstance(user_id, str):
|
if not isinstance(user_id, str):
|
||||||
raise ApiError("invalid_user_id", "This user id is invalid: " + str(user_id))
|
raise ApiError("invalid_user_id", "This user id is invalid: " + str(user_id))
|
||||||
response = requests.get(ProtocolBuilderService.STUDY_URL % user_id)
|
response = requests.get(ProtocolBuilderService.STUDY_URL % user_id)
|
||||||
@ -30,40 +32,32 @@ class ProtocolBuilderService(object):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_investigators(study_id) -> {}:
|
def get_investigators(study_id) -> {}:
|
||||||
ProtocolBuilderService.check_args(study_id)
|
return ProtocolBuilderService.__make_request(study_id, ProtocolBuilderService.INVESTIGATOR_URL)
|
||||||
response = requests.get(ProtocolBuilderService.INVESTIGATOR_URL % study_id)
|
|
||||||
if response.ok and response.text:
|
|
||||||
pb_studies = json.loads(response.text)
|
|
||||||
return pb_studies
|
|
||||||
else:
|
|
||||||
raise ApiError("protocol_builder_error",
|
|
||||||
"Received an invalid response from the protocol builder (status %s): %s" %
|
|
||||||
(response.status_code, response.text))
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_required_docs(study_id) -> Optional[List[ProtocolBuilderRequiredDocument]]:
|
def get_required_docs(study_id) -> Optional[List[ProtocolBuilderRequiredDocument]]:
|
||||||
ProtocolBuilderService.check_args(study_id)
|
return ProtocolBuilderService.__make_request(study_id, ProtocolBuilderService.REQUIRED_DOCS_URL)
|
||||||
response = requests.get(ProtocolBuilderService.REQUIRED_DOCS_URL % study_id)
|
|
||||||
|
@staticmethod
|
||||||
|
def get_study_details(study_id) -> {}:
|
||||||
|
return ProtocolBuilderService.__make_request(study_id, ProtocolBuilderService.STUDY_DETAILS_URL)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __enabled_or_raise():
|
||||||
|
if not ProtocolBuilderService.ENABLED:
|
||||||
|
raise ApiError("protocol_builder_disabled", "The Protocol Builder Service is currently disabled.")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __make_request(study_id, url):
|
||||||
|
ProtocolBuilderService.__enabled_or_raise()
|
||||||
|
if not isinstance(study_id, int):
|
||||||
|
raise ApiError("invalid_study_id", "This study id is invalid: " + str(study_id))
|
||||||
|
response = requests.get(url % study_id)
|
||||||
if response.ok and response.text:
|
if response.ok and response.text:
|
||||||
return json.loads(response.text)
|
return json.loads(response.text)
|
||||||
else:
|
else:
|
||||||
raise ApiError("protocol_builder_error",
|
raise ApiError("protocol_builder_error",
|
||||||
"Received an invalid response from the protocol builder (status %s): %s" %
|
"Received an invalid response from the protocol builder (status %s): %s when calling "
|
||||||
(response.status_code, response.text))
|
"url '%s'." %
|
||||||
|
(response.status_code, response.text, url))
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_study_details(study_id) -> {}:
|
|
||||||
ProtocolBuilderService.check_args(study_id)
|
|
||||||
response = requests.get(ProtocolBuilderService.STUDY_DETAILS_URL % study_id)
|
|
||||||
if response.ok and response.text:
|
|
||||||
pb_study_details = json.loads(response.text)
|
|
||||||
return pb_study_details
|
|
||||||
else:
|
|
||||||
raise ApiError("protocol_builder_error",
|
|
||||||
"Received an invalid response from the protocol builder (status %s): %s" %
|
|
||||||
(response.status_code, response.text))
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def check_args(study_id):
|
|
||||||
if not isinstance(study_id, int):
|
|
||||||
raise ApiError("invalid_study_id", "This study id is invalid: " + str(study_id))
|
|
||||||
|
@ -32,6 +32,12 @@ class StudyService(object):
|
|||||||
studies.append(StudyService.get_study(study_model.id, study_model))
|
studies.append(StudyService.get_study(study_model.id, study_model))
|
||||||
return studies
|
return studies
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_studies_with_files():
|
||||||
|
"""Returns a list of all studies"""
|
||||||
|
db_studies = session.query(StudyModel).all()
|
||||||
|
return db_studies
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_study(study_id, study_model: StudyModel = None):
|
def get_study(study_id, study_model: StudyModel = None):
|
||||||
"""Returns a study model that contains all the workflows organized by category.
|
"""Returns a study model that contains all the workflows organized by category.
|
||||||
@ -110,23 +116,29 @@ class StudyService(object):
|
|||||||
"""Returns a list of documents related to the study, and any file information
|
"""Returns a list of documents related to the study, and any file information
|
||||||
that is available.."""
|
that is available.."""
|
||||||
|
|
||||||
# Get PB required docs
|
# Get PB required docs, if Protocol Builder Service is enabled.
|
||||||
try:
|
if ProtocolBuilderService.ENABLED:
|
||||||
pb_docs = ProtocolBuilderService.get_required_docs(study_id=study_id)
|
try:
|
||||||
except requests.exceptions.ConnectionError as ce:
|
pb_docs = ProtocolBuilderService.get_required_docs(study_id=study_id)
|
||||||
app.logger.error("Failed to connect to the Protocol Builder - %s" % str(ce))
|
except requests.exceptions.ConnectionError as ce:
|
||||||
|
app.logger.error("Failed to connect to the Protocol Builder - %s" % str(ce))
|
||||||
|
pb_docs = []
|
||||||
|
else:
|
||||||
pb_docs = []
|
pb_docs = []
|
||||||
|
|
||||||
# Loop through all known document types, get the counts for those files, and use pb_docs to mark those required.
|
# Loop through all known document types, get the counts for those files,
|
||||||
|
# and use pb_docs to mark those as required.
|
||||||
doc_dictionary = FileService.get_reference_data(FileService.DOCUMENT_LIST, 'code', ['id'])
|
doc_dictionary = FileService.get_reference_data(FileService.DOCUMENT_LIST, 'code', ['id'])
|
||||||
|
|
||||||
documents = {}
|
documents = {}
|
||||||
for code, doc in doc_dictionary.items():
|
for code, doc in doc_dictionary.items():
|
||||||
|
|
||||||
pb_data = next((item for item in pb_docs if int(item['AUXDOCID']) == int(doc['id'])), None)
|
if ProtocolBuilderService.ENABLED:
|
||||||
doc['required'] = False
|
pb_data = next((item for item in pb_docs if int(item['AUXDOCID']) == int(doc['id'])), None)
|
||||||
if pb_data:
|
doc['required'] = False
|
||||||
doc['required'] = True
|
if pb_data:
|
||||||
|
doc['required'] = True
|
||||||
|
|
||||||
doc['study_id'] = study_id
|
doc['study_id'] = study_id
|
||||||
doc['code'] = code
|
doc['code'] = code
|
||||||
|
|
||||||
@ -153,7 +165,6 @@ class StudyService(object):
|
|||||||
doc['status'] = workflow.status.value
|
doc['status'] = workflow.status.value
|
||||||
|
|
||||||
documents[code] = doc
|
documents[code] = doc
|
||||||
|
|
||||||
return documents
|
return documents
|
||||||
|
|
||||||
|
|
||||||
@ -201,9 +212,13 @@ class StudyService(object):
|
|||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def synch_all_studies_with_protocol_builder(user):
|
def synch_with_protocol_builder_if_enabled(user):
|
||||||
"""Assures that the studies we have locally for the given user are
|
"""Assures that the studies we have locally for the given user are
|
||||||
in sync with the studies available in protocol builder. """
|
in sync with the studies available in protocol builder. """
|
||||||
|
|
||||||
|
if not ProtocolBuilderService.ENABLED:
|
||||||
|
return
|
||||||
|
|
||||||
# Get studies matching this user from Protocol Builder
|
# Get studies matching this user from Protocol Builder
|
||||||
pb_studies: List[ProtocolBuilderStudy] = ProtocolBuilderService.get_studies(user.uid)
|
pb_studies: List[ProtocolBuilderStudy] = ProtocolBuilderService.get_studies(user.uid)
|
||||||
|
|
||||||
|
@ -3,17 +3,15 @@
|
|||||||
# run migrations
|
# run migrations
|
||||||
export FLASK_APP=./crc/__init__.py
|
export FLASK_APP=./crc/__init__.py
|
||||||
|
|
||||||
for entry in ./instance/* ; do
|
|
||||||
echo "$entry"
|
|
||||||
cat $entry
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ "$DOWNGRADE_DB" = "true" ]; then
|
if [ "$DOWNGRADE_DB" = "true" ]; then
|
||||||
echo 'Downgrading...'
|
echo 'Downgrading database...'
|
||||||
pipenv run flask db downgrade
|
pipenv run flask db downgrade
|
||||||
fi
|
fi
|
||||||
|
|
||||||
pipenv run flask db upgrade
|
if [ "$UPGRADE_DB" = "true" ]; then
|
||||||
|
echo 'Upgrading database...'
|
||||||
|
pipenv run flask db upgrade
|
||||||
|
fi
|
||||||
|
|
||||||
if [ "$RESET_DB" = "true" ]; then
|
if [ "$RESET_DB" = "true" ]; then
|
||||||
echo 'Resetting database...'
|
echo 'Resetting database...'
|
||||||
|
@ -14,7 +14,8 @@ class ExampleDataLoader:
|
|||||||
session.flush() # Clear out any transactions before deleting it all to avoid spurious errors.
|
session.flush() # Clear out any transactions before deleting it all to avoid spurious errors.
|
||||||
for table in reversed(db.metadata.sorted_tables):
|
for table in reversed(db.metadata.sorted_tables):
|
||||||
session.execute(table.delete())
|
session.execute(table.delete())
|
||||||
session.flush()
|
session.commit()
|
||||||
|
session.flush()
|
||||||
|
|
||||||
def load_all(self):
|
def load_all(self):
|
||||||
|
|
||||||
|
39
migrations/versions/55c6cd407d89_.py
Normal file
39
migrations/versions/55c6cd407d89_.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 55c6cd407d89
|
||||||
|
Revises: cc4bccc5e5a8
|
||||||
|
Create Date: 2020-05-22 22:02:46.650170
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '55c6cd407d89'
|
||||||
|
down_revision = 'cc4bccc5e5a8'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('approval',
|
||||||
|
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_version', sa.String(), nullable=True),
|
||||||
|
sa.Column('approver_uid', sa.String(), nullable=True),
|
||||||
|
sa.Column('status', sa.String(), nullable=True),
|
||||||
|
sa.Column('message', sa.String(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['study_id'], ['study.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['workflow_id'], ['workflow.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('approval')
|
||||||
|
# ### end Alembic commands ###
|
@ -1,7 +1,9 @@
|
|||||||
# Set environment variable to testing before loading.
|
# Set environment variable to testing before loading.
|
||||||
# IMPORTANT - Environment must be loaded before app, models, etc....
|
# IMPORTANT - Environment must be loaded before app, models, etc....
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
|
os.environ["TESTING"] = "true"
|
||||||
|
|
||||||
|
import json
|
||||||
import unittest
|
import unittest
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import datetime
|
import datetime
|
||||||
@ -10,10 +12,6 @@ from crc.models.protocol_builder import ProtocolBuilderStatus
|
|||||||
from crc.models.study import StudyModel
|
from crc.models.study import StudyModel
|
||||||
from crc.services.file_service import FileService
|
from crc.services.file_service import FileService
|
||||||
from crc.services.study_service import StudyService
|
from crc.services.study_service import StudyService
|
||||||
from crc.services.workflow_processor import WorkflowProcessor
|
|
||||||
|
|
||||||
os.environ["TESTING"] = "true"
|
|
||||||
|
|
||||||
from crc.models.file import FileModel, FileDataModel, CONTENT_TYPES
|
from crc.models.file import FileModel, FileDataModel, CONTENT_TYPES
|
||||||
from crc.models.workflow import WorkflowSpecModel, WorkflowSpecModelSchema, WorkflowModel
|
from crc.models.workflow import WorkflowSpecModel, WorkflowSpecModelSchema, WorkflowModel
|
||||||
from crc.models.user import UserModel
|
from crc.models.user import UserModel
|
||||||
@ -32,6 +30,10 @@ class BaseTest(unittest.TestCase):
|
|||||||
efficiently when we have a database in place.
|
efficiently when we have a database in place.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if not app.config['TESTING']:
|
||||||
|
raise (Exception("INVALID TEST CONFIGURATION. This is almost always in import order issue."
|
||||||
|
"The first class to import in each test should be the base_test.py file."))
|
||||||
|
|
||||||
auths = {}
|
auths = {}
|
||||||
test_uid = "dhf8r"
|
test_uid = "dhf8r"
|
||||||
|
|
||||||
|
26
tests/data/empty_workflow/empty_workflow.bpmn
Normal file
26
tests/data/empty_workflow/empty_workflow.bpmn
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_1v1rp1q" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.3">
|
||||||
|
<bpmn:process id="empty_workflow" isExecutable="true">
|
||||||
|
<bpmn:startEvent id="StartEvent_1">
|
||||||
|
<bpmn:outgoing>SequenceFlow_0lvudp8</bpmn:outgoing>
|
||||||
|
</bpmn:startEvent>
|
||||||
|
<bpmn:sequenceFlow id="SequenceFlow_0lvudp8" sourceRef="StartEvent_1" targetRef="EndEvent_0q4qzl9" />
|
||||||
|
<bpmn:endEvent id="EndEvent_0q4qzl9">
|
||||||
|
<bpmn:incoming>SequenceFlow_0lvudp8</bpmn:incoming>
|
||||||
|
</bpmn:endEvent>
|
||||||
|
</bpmn:process>
|
||||||
|
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||||
|
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="empty_workflow">
|
||||||
|
<bpmndi:BPMNEdge id="SequenceFlow_0lvudp8_di" bpmnElement="SequenceFlow_0lvudp8">
|
||||||
|
<di:waypoint x="215" y="117" />
|
||||||
|
<di:waypoint x="432" y="117" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
|
||||||
|
<dc:Bounds x="179" y="99" width="36" height="36" />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="EndEvent_0q4qzl9_di" bpmnElement="EndEvent_0q4qzl9">
|
||||||
|
<dc:Bounds x="432" y="99" width="36" height="36" />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
</bpmndi:BPMNPlane>
|
||||||
|
</bpmndi:BPMNDiagram>
|
||||||
|
</bpmn:definitions>
|
14
tests/test_approvals.py
Normal file
14
tests/test_approvals.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from tests.base_test import BaseTest
|
||||||
|
|
||||||
|
|
||||||
|
class TestApprovals(BaseTest):
|
||||||
|
|
||||||
|
def test_list_approvals(self):
|
||||||
|
rv = self.app.get('/v1.0/approval', headers=self.logged_in_headers())
|
||||||
|
self.assert_success(rv)
|
||||||
|
|
||||||
|
def test_update_approval(self):
|
||||||
|
rv = self.app.put('/v1.0/approval/1',
|
||||||
|
headers=self.logged_in_headers(),
|
||||||
|
data={})
|
||||||
|
self.assert_success(rv)
|
@ -12,7 +12,7 @@ class TestAuthentication(BaseTest):
|
|||||||
self.assertTrue(isinstance(auth_token, bytes))
|
self.assertTrue(isinstance(auth_token, bytes))
|
||||||
self.assertEqual("dhf8r", user.decode_auth_token(auth_token).get("sub"))
|
self.assertEqual("dhf8r", user.decode_auth_token(auth_token).get("sub"))
|
||||||
|
|
||||||
def test_auth_creates_user(self):
|
def test_backdoor_auth_creates_user(self):
|
||||||
new_uid = 'czn1z';
|
new_uid = 'czn1z';
|
||||||
self.load_example_data()
|
self.load_example_data()
|
||||||
user = db.session.query(UserModel).filter(UserModel.uid == new_uid).first()
|
user = db.session.query(UserModel).filter(UserModel.uid == new_uid).first()
|
||||||
@ -37,6 +37,23 @@ class TestAuthentication(BaseTest):
|
|||||||
self.assertTrue(rv_2.status_code == 302)
|
self.assertTrue(rv_2.status_code == 302)
|
||||||
self.assertTrue(str.startswith(rv_2.location, redirect_url))
|
self.assertTrue(str.startswith(rv_2.location, redirect_url))
|
||||||
|
|
||||||
|
def test_normal_auth_creates_user(self):
|
||||||
|
new_uid = 'lb3dp' # This user is in the test ldap system.
|
||||||
|
self.load_example_data()
|
||||||
|
user = db.session.query(UserModel).filter(UserModel.uid == new_uid).first()
|
||||||
|
self.assertIsNone(user)
|
||||||
|
redirect_url = 'http://worlds.best.website/admin'
|
||||||
|
headers = dict(Uid=new_uid)
|
||||||
|
rv = self.app.get('v1.0/login', follow_redirects=False, headers=headers)
|
||||||
|
self.assert_success(rv)
|
||||||
|
user = db.session.query(UserModel).filter(UserModel.uid == new_uid).first()
|
||||||
|
self.assertIsNotNone(user)
|
||||||
|
self.assertEquals(new_uid, user.uid)
|
||||||
|
self.assertEquals("Laura Barnes", user.display_name)
|
||||||
|
self.assertEquals("lb3dp@virginia.edu", user.email_address)
|
||||||
|
self.assertEquals("E0:Associate Professor of Systems and Information Engineering", user.title)
|
||||||
|
|
||||||
|
|
||||||
def test_current_user_status(self):
|
def test_current_user_status(self):
|
||||||
self.load_example_data()
|
self.load_example_data()
|
||||||
rv = self.app.get('/v1.0/user')
|
rv = self.app.get('/v1.0/user')
|
||||||
|
@ -21,7 +21,7 @@ class TestLdapService(BaseTest):
|
|||||||
self.assertEqual("lb3dp", user_info.uid)
|
self.assertEqual("lb3dp", user_info.uid)
|
||||||
self.assertEqual("Laura Barnes", user_info.display_name)
|
self.assertEqual("Laura Barnes", user_info.display_name)
|
||||||
self.assertEqual("Laura", user_info.given_name)
|
self.assertEqual("Laura", user_info.given_name)
|
||||||
self.assertEqual("lb3dp@virginia.edu", user_info.email)
|
self.assertEqual("lb3dp@virginia.edu", user_info.email_address)
|
||||||
self.assertEqual("+1 (434) 924-1723", user_info.telephone_number)
|
self.assertEqual("+1 (434) 924-1723", user_info.telephone_number)
|
||||||
self.assertEqual("E0:Associate Professor of Systems and Information Engineering", user_info.title)
|
self.assertEqual("E0:Associate Professor of Systems and Information Engineering", user_info.title)
|
||||||
self.assertEqual("E0:EN-Eng Sys and Environment", user_info.department)
|
self.assertEqual("E0:EN-Eng Sys and Environment", user_info.department)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from crc.services.protocol_builder import ProtocolBuilderService
|
|
||||||
from tests.base_test import BaseTest
|
from tests.base_test import BaseTest
|
||||||
|
from crc.services.protocol_builder import ProtocolBuilderService
|
||||||
|
|
||||||
|
|
||||||
class TestProtocolBuilder(BaseTest):
|
class TestProtocolBuilder(BaseTest):
|
||||||
@ -10,6 +10,7 @@ class TestProtocolBuilder(BaseTest):
|
|||||||
|
|
||||||
@patch('crc.services.protocol_builder.requests.get')
|
@patch('crc.services.protocol_builder.requests.get')
|
||||||
def test_get_studies(self, mock_get):
|
def test_get_studies(self, mock_get):
|
||||||
|
ProtocolBuilderService.ENABLED = True
|
||||||
mock_get.return_value.ok = True
|
mock_get.return_value.ok = True
|
||||||
mock_get.return_value.text = self.protocol_builder_response('user_studies.json')
|
mock_get.return_value.text = self.protocol_builder_response('user_studies.json')
|
||||||
response = ProtocolBuilderService.get_studies(self.test_uid)
|
response = ProtocolBuilderService.get_studies(self.test_uid)
|
||||||
@ -17,6 +18,7 @@ class TestProtocolBuilder(BaseTest):
|
|||||||
|
|
||||||
@patch('crc.services.protocol_builder.requests.get')
|
@patch('crc.services.protocol_builder.requests.get')
|
||||||
def test_get_investigators(self, mock_get):
|
def test_get_investigators(self, mock_get):
|
||||||
|
ProtocolBuilderService.ENABLED = True
|
||||||
mock_get.return_value.ok = True
|
mock_get.return_value.ok = True
|
||||||
mock_get.return_value.text = self.protocol_builder_response('investigators.json')
|
mock_get.return_value.text = self.protocol_builder_response('investigators.json')
|
||||||
response = ProtocolBuilderService.get_investigators(self.test_study_id)
|
response = ProtocolBuilderService.get_investigators(self.test_study_id)
|
||||||
@ -28,6 +30,7 @@ class TestProtocolBuilder(BaseTest):
|
|||||||
|
|
||||||
@patch('crc.services.protocol_builder.requests.get')
|
@patch('crc.services.protocol_builder.requests.get')
|
||||||
def test_get_required_docs(self, mock_get):
|
def test_get_required_docs(self, mock_get):
|
||||||
|
ProtocolBuilderService.ENABLED = True
|
||||||
mock_get.return_value.ok = True
|
mock_get.return_value.ok = True
|
||||||
mock_get.return_value.text = self.protocol_builder_response('required_docs.json')
|
mock_get.return_value.text = self.protocol_builder_response('required_docs.json')
|
||||||
response = ProtocolBuilderService.get_required_docs(self.test_study_id)
|
response = ProtocolBuilderService.get_required_docs(self.test_study_id)
|
||||||
@ -37,6 +40,7 @@ class TestProtocolBuilder(BaseTest):
|
|||||||
|
|
||||||
@patch('crc.services.protocol_builder.requests.get')
|
@patch('crc.services.protocol_builder.requests.get')
|
||||||
def test_get_details(self, mock_get):
|
def test_get_details(self, mock_get):
|
||||||
|
ProtocolBuilderService.ENABLED = True
|
||||||
mock_get.return_value.ok = True
|
mock_get.return_value.ok = True
|
||||||
mock_get.return_value.text = self.protocol_builder_response('study_details.json')
|
mock_get.return_value.text = self.protocol_builder_response('study_details.json')
|
||||||
response = ProtocolBuilderService.get_study_details(self.test_study_id)
|
response = ProtocolBuilderService.get_study_details(self.test_study_id)
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import json
|
import json
|
||||||
|
from tests.base_test import BaseTest
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
@ -8,7 +9,7 @@ from crc.models.protocol_builder import ProtocolBuilderStatus, \
|
|||||||
from crc.models.stats import TaskEventModel
|
from crc.models.stats import TaskEventModel
|
||||||
from crc.models.study import StudyModel, StudySchema
|
from crc.models.study import StudyModel, StudySchema
|
||||||
from crc.models.workflow import WorkflowSpecModel, WorkflowModel, WorkflowSpecCategoryModel
|
from crc.models.workflow import WorkflowSpecModel, WorkflowModel, WorkflowSpecCategoryModel
|
||||||
from tests.base_test import BaseTest
|
from crc.services.protocol_builder import ProtocolBuilderService
|
||||||
|
|
||||||
|
|
||||||
class TestStudyApi(BaseTest):
|
class TestStudyApi(BaseTest):
|
||||||
@ -38,24 +39,12 @@ class TestStudyApi(BaseTest):
|
|||||||
study = session.query(StudyModel).first()
|
study = session.query(StudyModel).first()
|
||||||
self.assertIsNotNone(study)
|
self.assertIsNotNone(study)
|
||||||
|
|
||||||
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_investigators') # mock_studies
|
def test_get_study(self):
|
||||||
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_required_docs') # mock_docs
|
|
||||||
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_study_details') # mock_details
|
|
||||||
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_studies') # mock_studies
|
|
||||||
def test_get_study(self, mock_studies, mock_details, mock_docs, mock_investigators):
|
|
||||||
"""Generic test, but pretty detailed, in that the study should return a categorized list of workflows
|
"""Generic test, but pretty detailed, in that the study should return a categorized list of workflows
|
||||||
This starts with out loading the example data, to show that all the bases are covered from ground 0."""
|
This starts with out loading the example data, to show that all the bases are covered from ground 0."""
|
||||||
|
|
||||||
# Mock Protocol Builder responses
|
"""NOTE: The protocol builder is not enabled or mocked out. As the master workflow (which is empty),
|
||||||
studies_response = self.protocol_builder_response('user_studies.json')
|
and the test workflow do not need it, and it is disabled in the configuration."""
|
||||||
mock_studies.return_value = ProtocolBuilderStudySchema(many=True).loads(studies_response)
|
|
||||||
details_response = self.protocol_builder_response('study_details.json')
|
|
||||||
mock_details.return_value = json.loads(details_response)
|
|
||||||
docs_response = self.protocol_builder_response('required_docs.json')
|
|
||||||
mock_docs.return_value = json.loads(docs_response)
|
|
||||||
investigators_response = self.protocol_builder_response('investigators.json')
|
|
||||||
mock_investigators.return_value = json.loads(investigators_response)
|
|
||||||
|
|
||||||
new_study = self.add_test_study()
|
new_study = self.add_test_study()
|
||||||
new_study = session.query(StudyModel).filter_by(id=new_study["id"]).first()
|
new_study = session.query(StudyModel).filter_by(id=new_study["id"]).first()
|
||||||
# Add a category
|
# Add a category
|
||||||
@ -65,7 +54,7 @@ class TestStudyApi(BaseTest):
|
|||||||
# Create a workflow specification
|
# Create a workflow specification
|
||||||
self.create_workflow("random_fact", study=new_study, category_id=new_category.id)
|
self.create_workflow("random_fact", study=new_study, category_id=new_category.id)
|
||||||
# Assure there is a master specification, and it has the lookup files it needs.
|
# Assure there is a master specification, and it has the lookup files it needs.
|
||||||
spec = self.load_test_spec("top_level_workflow", master_spec=True)
|
spec = self.load_test_spec("empty_workflow", master_spec=True)
|
||||||
self.create_reference_document()
|
self.create_reference_document()
|
||||||
|
|
||||||
api_response = self.app.get('/v1.0/study/%i' % new_study.id,
|
api_response = self.app.get('/v1.0/study/%i' % new_study.id,
|
||||||
@ -126,6 +115,9 @@ class TestStudyApi(BaseTest):
|
|||||||
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_study_details') # mock_details
|
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_study_details') # mock_details
|
||||||
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_studies') # mock_studies
|
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_studies') # mock_studies
|
||||||
def test_get_all_studies(self, mock_studies, mock_details, mock_docs, mock_investigators):
|
def test_get_all_studies(self, mock_studies, mock_details, mock_docs, mock_investigators):
|
||||||
|
# Enable the protocol builder for these tests, as the master_workflow and other workflows
|
||||||
|
# depend on using the PB for data.
|
||||||
|
ProtocolBuilderService.ENABLED = True
|
||||||
self.load_example_data()
|
self.load_example_data()
|
||||||
s = StudyModel(
|
s = StudyModel(
|
||||||
id=54321, # This matches one of the ids from the study_details_json data.
|
id=54321, # This matches one of the ids from the study_details_json data.
|
||||||
@ -208,6 +200,7 @@ class TestStudyApi(BaseTest):
|
|||||||
self.assertEqual(study.sponsor, json_data['sponsor'])
|
self.assertEqual(study.sponsor, json_data['sponsor'])
|
||||||
self.assertEqual(study.ind_number, json_data['ind_number'])
|
self.assertEqual(study.ind_number, json_data['ind_number'])
|
||||||
|
|
||||||
|
|
||||||
def test_delete_study(self):
|
def test_delete_study(self):
|
||||||
self.load_example_data()
|
self.load_example_data()
|
||||||
study = session.query(StudyModel).first()
|
study = session.query(StudyModel).first()
|
||||||
|
@ -8,6 +8,7 @@ from crc.models.api_models import WorkflowApiSchema, MultiInstanceType, TaskSche
|
|||||||
from crc.models.file import FileModelSchema
|
from crc.models.file import FileModelSchema
|
||||||
from crc.models.stats import TaskEventModel
|
from crc.models.stats import TaskEventModel
|
||||||
from crc.models.workflow import WorkflowStatus
|
from crc.models.workflow import WorkflowStatus
|
||||||
|
from crc.services.protocol_builder import ProtocolBuilderService
|
||||||
from crc.services.workflow_service import WorkflowService
|
from crc.services.workflow_service import WorkflowService
|
||||||
from tests.base_test import BaseTest
|
from tests.base_test import BaseTest
|
||||||
|
|
||||||
@ -302,6 +303,9 @@ class TestTasksApi(BaseTest):
|
|||||||
|
|
||||||
@patch('crc.services.protocol_builder.requests.get')
|
@patch('crc.services.protocol_builder.requests.get')
|
||||||
def test_multi_instance_task(self, mock_get):
|
def test_multi_instance_task(self, mock_get):
|
||||||
|
# Enable the protocol builder.
|
||||||
|
ProtocolBuilderService.ENABLED = True
|
||||||
|
|
||||||
# This depends on getting a list of investigators back from the protocol builder.
|
# This depends on getting a list of investigators back from the protocol builder.
|
||||||
mock_get.return_value.ok = True
|
mock_get.return_value.ok = True
|
||||||
mock_get.return_value.text = self.protocol_builder_response('investigators.json')
|
mock_get.return_value.text = self.protocol_builder_response('investigators.json')
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from crc import app
|
|
||||||
from tests.base_test import BaseTest
|
from tests.base_test import BaseTest
|
||||||
|
from crc import app
|
||||||
|
|
||||||
|
|
||||||
class TestStudyApi(BaseTest):
|
class TestStudyApi(BaseTest):
|
||||||
@ -22,11 +22,13 @@ class TestStudyApi(BaseTest):
|
|||||||
{"option": "Address", "selected": False},
|
{"option": "Address", "selected": False},
|
||||||
{"option": "Phone", "selected": True, "stored": ["Send or Transmit outside of UVA"]}]}
|
{"option": "Phone", "selected": True, "stored": ["Send or Transmit outside of UVA"]}]}
|
||||||
with open(filepath, 'rb') as f:
|
with open(filepath, 'rb') as f:
|
||||||
file_data = {'file': (f, 'my_new_file.bpmn')}
|
file_data = {'file': (f, 'my_new_file.bpmn'), 'data': json.dumps(template_data)}
|
||||||
rv = self.app.put('/v1.0/render_docx?data=%s' % json.dumps(template_data),
|
rv = self.app.put('/v1.0/render_docx',
|
||||||
data=file_data, follow_redirects=True,
|
data=file_data, follow_redirects=True,
|
||||||
content_type='multipart/form-data')
|
content_type='multipart/form-data')
|
||||||
self.assert_success(rv)
|
self.assert_success(rv)
|
||||||
|
self.assertIsNotNone(rv.data)
|
||||||
|
self.assertEquals('application/octet-stream', rv.content_type)
|
||||||
|
|
||||||
def test_list_scripts(self):
|
def test_list_scripts(self):
|
||||||
rv = self.app.get('/v1.0/list_scripts')
|
rv = self.app.get('/v1.0/list_scripts')
|
||||||
|
@ -20,21 +20,7 @@ class TestWorkflowSpecValidation(BaseTest):
|
|||||||
json_data = json.loads(rv.get_data(as_text=True))
|
json_data = json.loads(rv.get_data(as_text=True))
|
||||||
return ApiErrorSchema(many=True).load(json_data)
|
return ApiErrorSchema(many=True).load(json_data)
|
||||||
|
|
||||||
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_investigators') # mock_studies
|
def test_successful_validation_of_test_workflows(self):
|
||||||
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_required_docs') # mock_docs
|
|
||||||
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_study_details') # mock_details
|
|
||||||
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_studies') # mock_studies
|
|
||||||
def test_successful_validation_of_test_workflows(self, mock_studies, mock_details, mock_docs, mock_investigators):
|
|
||||||
|
|
||||||
# Mock Protocol Builder responses
|
|
||||||
studies_response = self.protocol_builder_response('user_studies.json')
|
|
||||||
mock_studies.return_value = ProtocolBuilderStudySchema(many=True).loads(studies_response)
|
|
||||||
details_response = self.protocol_builder_response('study_details.json')
|
|
||||||
mock_details.return_value = json.loads(details_response)
|
|
||||||
docs_response = self.protocol_builder_response('required_docs.json')
|
|
||||||
mock_docs.return_value = json.loads(docs_response)
|
|
||||||
investigators_response = self.protocol_builder_response('investigators.json')
|
|
||||||
mock_investigators.return_value = json.loads(investigators_response)
|
|
||||||
|
|
||||||
self.assertEqual(0, len(self.validate_workflow("parallel_tasks")))
|
self.assertEqual(0, len(self.validate_workflow("parallel_tasks")))
|
||||||
self.assertEqual(0, len(self.validate_workflow("decision_table")))
|
self.assertEqual(0, len(self.validate_workflow("decision_table")))
|
||||||
|
Loading…
x
Reference in New Issue
Block a user