Merge pull request #76 from sartography/feature/rrp-endpoints
Feature/rrp endpoints
This commit is contained in:
commit
49c322177b
|
@ -1,7 +1,7 @@
|
|||
language: python
|
||||
|
||||
python:
|
||||
- "3.7"
|
||||
- "3.6.9"
|
||||
|
||||
services:
|
||||
- postgresql
|
||||
|
|
35
Dockerfile
35
Dockerfile
|
@ -1,27 +1,24 @@
|
|||
FROM python:3.7
|
||||
FROM python:3.6.9-slim
|
||||
|
||||
ENV PATH=/root/.local/bin:/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin
|
||||
WORKDIR /app
|
||||
|
||||
# install node and yarn
|
||||
RUN apt-get update
|
||||
RUN apt-get -y install postgresql-client
|
||||
COPY Pipfile Pipfile.lock /app/
|
||||
|
||||
# config project dir
|
||||
RUN mkdir /crc-workflow
|
||||
WORKDIR /crc-workflow
|
||||
RUN pip install pipenv && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
gcc python3-dev libssl-dev \
|
||||
curl postgresql-client git-core && \
|
||||
pipenv install --dev && \
|
||||
apt-get remove -y gcc python3-dev libssl-dev && \
|
||||
apt-get purge -y --auto-remove && \
|
||||
rm -rf /var/lib/apt/lists/ *
|
||||
|
||||
# install python requirements
|
||||
RUN pip install pipenv
|
||||
ADD Pipfile /crc-workflow/
|
||||
ADD Pipfile.lock /crc-workflow/
|
||||
RUN pipenv install --dev
|
||||
COPY . /app/
|
||||
|
||||
# include rejoiner code (gets overriden by local changes)
|
||||
COPY . /crc-workflow/
|
||||
|
||||
# run webserver by default
|
||||
ENV FLASK_APP=./crc/__init__.py
|
||||
CMD ["pipenv", "run", "python", "./run.py"]
|
||||
ENV FLASK_APP=/app/crc/__init__.py
|
||||
CMD ["pipenv", "run", "flask", "db", "upgrade"]
|
||||
CMD ["pipenv", "run", "python", "/app/run.py"]
|
||||
|
||||
# expose ports
|
||||
EXPOSE 5000
|
||||
|
|
5
Pipfile
5
Pipfile
|
@ -24,18 +24,17 @@ pyjwt = "*"
|
|||
requests = "*"
|
||||
xlsxwriter = "*"
|
||||
webtest = "*"
|
||||
spiffworkflow = {editable = true,git = "https://github.com/sartography/SpiffWorkflow.git",ref = "bug/the_horror"}
|
||||
spiffworkflow = {editable = true,git = "https://github.com/sartography/SpiffWorkflow.git",ref = "master"}
|
||||
alembic = "*"
|
||||
coverage = "*"
|
||||
sphinx = "*"
|
||||
recommonmark = "*"
|
||||
psycopg2-binary = "*"
|
||||
docxtpl = "*"
|
||||
flask-sso = "*"
|
||||
python-dateutil = "*"
|
||||
pandas = "*"
|
||||
xlrd = "*"
|
||||
ldap3 = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.7"
|
||||
python_version = "3.6.9"
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "bd289126c41b0f5f2761f0415d85e1110a584256460374a9ce4cda07c0033ddd"
|
||||
"sha256": "1ca737db75750ea4351c15b4b0b26155d90bc5522705ed293a0c2773600b6a0a"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
"python_version": "3.7"
|
||||
"python_version": "3.6.9"
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
|
@ -96,12 +96,6 @@
|
|||
],
|
||||
"version": "==3.6.3.0"
|
||||
},
|
||||
"blinker": {
|
||||
"hashes": [
|
||||
"sha256:471aee25f3992bd325afa3772f1063dbdbbca947a041b8b89466dc00d606f8b6"
|
||||
],
|
||||
"version": "==1.4"
|
||||
},
|
||||
"celery": {
|
||||
"hashes": [
|
||||
"sha256:108a0bf9018a871620936c33a3ee9f6336a89f8ef0a0f567a9001f4aa361415f",
|
||||
|
@ -307,13 +301,6 @@
|
|||
],
|
||||
"version": "==2.4.1"
|
||||
},
|
||||
"flask-sso": {
|
||||
"hashes": [
|
||||
"sha256:541a8a2387c6eac4325c53f8f7f863a03173b37aa558a37a430010d7fc1a3633"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.4.0"
|
||||
},
|
||||
"future": {
|
||||
"hashes": [
|
||||
"sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"
|
||||
|
@ -711,10 +698,10 @@
|
|||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
|
||||
"sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"
|
||||
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
|
||||
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
|
||||
],
|
||||
"version": "==1.14.0"
|
||||
"version": "==1.15.0"
|
||||
},
|
||||
"snowballstemmer": {
|
||||
"hashes": [
|
||||
|
@ -783,7 +770,7 @@
|
|||
"spiffworkflow": {
|
||||
"editable": true,
|
||||
"git": "https://github.com/sartography/SpiffWorkflow.git",
|
||||
"ref": "7dc54f1205de7006bdda6d966dc957e558f3c7f3"
|
||||
"ref": "cb098ee6d55b85bf7795997f4ad5f78c27d15381"
|
||||
},
|
||||
"sqlalchemy": {
|
||||
"hashes": [
|
||||
|
@ -955,10 +942,10 @@
|
|||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
|
||||
"sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"
|
||||
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
|
||||
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
|
||||
],
|
||||
"version": "==1.14.0"
|
||||
"version": "==1.15.0"
|
||||
},
|
||||
"wcwidth": {
|
||||
"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")
|
||||
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.
|
||||
PB_ENABLED = environ.get('PB_ENABLED', default=True)
|
||||
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_INVESTIGATORS_URL = environ.get('PB_INVESTIGATORS_URL', default=PB_BASE_URL + "investigators?studyid=%i")
|
||||
|
|
|
@ -6,6 +6,7 @@ DEVELOPMENT = True
|
|||
TESTING = True
|
||||
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."
|
||||
PB_ENABLED = False
|
||||
|
||||
print('### USING TESTING CONFIG: ###')
|
||||
print('SQLALCHEMY_DATABASE_URI = ', SQLALCHEMY_DATABASE_URI)
|
||||
|
|
|
@ -6,7 +6,6 @@ from flask_cors import CORS
|
|||
from flask_marshmallow import Marshmallow
|
||||
from flask_migrate import Migrate
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_sso import SSO
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
|
@ -31,7 +30,6 @@ session = db.session
|
|||
|
||||
migrate = Migrate(app, db)
|
||||
ma = Marshmallow(app)
|
||||
sso = SSO(app=app)
|
||||
|
||||
from crc import models
|
||||
from crc import api
|
||||
|
|
90
crc/api.yml
90
crc/api.yml
|
@ -110,6 +110,19 @@ paths:
|
|||
application/json:
|
||||
schema:
|
||||
$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}:
|
||||
parameters:
|
||||
- name: study_id
|
||||
|
@ -156,26 +169,6 @@ paths:
|
|||
application/json:
|
||||
schema:
|
||||
$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:
|
||||
get:
|
||||
operationId: crc.api.workflow.all_specifications
|
||||
|
@ -798,6 +791,54 @@ paths:
|
|||
type: array
|
||||
items:
|
||||
$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:
|
||||
securitySchemes:
|
||||
jwt:
|
||||
|
@ -1205,7 +1246,10 @@ components:
|
|||
readOnly: true
|
||||
task:
|
||||
$ref: "#/components/schemas/Task"
|
||||
|
||||
|
||||
|
||||
Approval:
|
||||
properties:
|
||||
id:
|
||||
type: number
|
||||
format: integer
|
||||
example: 5
|
||||
|
||||
|
|
|
@ -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 sqlalchemy.exc import IntegrityError
|
||||
|
||||
from crc import session
|
||||
from crc.api.common import ApiError, ApiErrorSchema
|
||||
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.study_service import StudyService
|
||||
|
||||
|
||||
def add_study(body):
|
||||
"""This should never get called, and is subject to deprication. Studies
|
||||
should be added through the protocol builder only."""
|
||||
"""Or any study like object. """
|
||||
study: Study = StudySchema().load(body)
|
||||
study_model = StudyModel(**study.model_args())
|
||||
session.add(study_model)
|
||||
|
@ -59,30 +55,17 @@ def delete_study(study_id):
|
|||
|
||||
|
||||
def all_studies():
|
||||
"""Returns all the studies associated with the current user. Assures we are
|
||||
in sync with values read in from the protocol builder. """
|
||||
StudyService.synch_all_studies_with_protocol_builder(g.user)
|
||||
"""Returns all the studies associated with the current user. """
|
||||
StudyService.synch_with_protocol_builder_if_enabled(g.user)
|
||||
studies = StudyService.get_studies_for_user(g.user)
|
||||
results = StudySchema(many=True).dump(studies)
|
||||
return results
|
||||
|
||||
|
||||
def post_update_study_from_protocol_builder(study_id):
|
||||
"""Update a single study based on data received from
|
||||
the protocol builder."""
|
||||
|
||||
db_study = session.query(StudyModel).filter_by(study_id=study_id).all()
|
||||
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
|
||||
|
||||
|
||||
|
||||
def all_studies_and_files():
|
||||
"""Returns all studies with submitted files"""
|
||||
studies = StudyService.get_studies_with_files()
|
||||
results = StudyFilesSchema(many=True).dump(studies)
|
||||
return results
|
||||
|
||||
|
||||
|
|
107
crc/api/user.py
107
crc/api/user.py
|
@ -1,12 +1,12 @@
|
|||
import json
|
||||
|
||||
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.models.user import UserModel, UserModelSchema
|
||||
|
||||
from crc.services.ldap_service import LdapService, LdapUserInfo
|
||||
|
||||
"""
|
||||
.. module:: crc.api.user
|
||||
|
@ -32,53 +32,76 @@ def verify_token(token):
|
|||
def get_current_user():
|
||||
return UserModelSchema().dump(g.user)
|
||||
|
||||
@app.route('/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
|
||||
def sso_login(user_info):
|
||||
app.logger.info("Login from Shibboleth happening. " + json.dump(user_info))
|
||||
# TODO: Get redirect URL from Shibboleth request header
|
||||
_handle_login(user_info)
|
||||
if not uid:
|
||||
raise ApiError("invalid_sso_credentials", "'Uid' nor 'X-Remote-Uid' were present in the headers: %s"
|
||||
% str(request.headers))
|
||||
|
||||
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,
|
||||
then returns the frontend auth callback URL, with auth token appended.
|
||||
|
||||
Args:
|
||||
user_info (dict of {
|
||||
uid: 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]
|
||||
user_info - an ldap user_info object.
|
||||
redirect_url: Optional[str]
|
||||
|
||||
Returns:
|
||||
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 == uid).first()
|
||||
user = db.session.query(UserModel).filter(UserModel.uid == user_info.uid).first()
|
||||
|
||||
if user is None:
|
||||
# Add new user
|
||||
user = UserModelSchema().load(user_info, session=db.session)
|
||||
else:
|
||||
# Update existing user data
|
||||
user = UserModelSchema().load(user_info, session=db.session, instance=user, partial=True)
|
||||
user = UserModel()
|
||||
|
||||
# Build display_name if not set
|
||||
if 'display_name' not in user_info or len(user_info['display_name']) == 0:
|
||||
display_name_list = []
|
||||
|
||||
for prop in ['first_name', 'last_name']:
|
||||
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)
|
||||
user.uid = user_info.uid
|
||||
user.display_name = user_info.display_name
|
||||
user.email_address = user_info.email_address
|
||||
user.affiliation = user_info.affiliation
|
||||
user.title = user_info.title
|
||||
|
||||
db.session.add(user)
|
||||
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.
|
||||
auth_token = user.encode_auth_token().decode()
|
||||
if redirect_url is not None:
|
||||
app.logger.info("SSO_LOGIN: REDIRECTING TO: " + redirect_url)
|
||||
return redirect('%s/%s' % (redirect_url, auth_token))
|
||||
else:
|
||||
app.logger.info("SSO_LOGIN: NO REDIRECT, JUST RETURNING AUTH TOKEN.")
|
||||
return auth_token
|
||||
|
||||
|
||||
|
||||
def backdoor(
|
||||
uid=None,
|
||||
affiliation=None,
|
||||
|
@ -122,11 +149,9 @@ def backdoor(
|
|||
ApiError. If on production, returns a 404 error.
|
||||
"""
|
||||
if not 'PRODUCTION' in app.config or not app.config['PRODUCTION']:
|
||||
user_info = {}
|
||||
for key in UserModel.__dict__.keys():
|
||||
if key in connexion.request.args:
|
||||
user_info[key] = connexion.request.args[key]
|
||||
|
||||
return _handle_login(user_info, redirect_url)
|
||||
ldap_info = LdapUserInfo()
|
||||
ldap_info.uid = connexion.request.args["uid"]
|
||||
ldap_info.email_address = connexion.request.args["email_address"]
|
||||
return _handle_login(ldap_info, redirect_url)
|
||||
else:
|
||||
raise ApiError('404', 'unknown')
|
||||
|
|
|
@ -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')
|
||||
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.dialects.postgresql import UUID
|
||||
|
||||
from crc import db
|
||||
from crc import db, ma
|
||||
|
||||
|
||||
class FileType(enum.Enum):
|
||||
|
@ -139,3 +139,9 @@ class LookupDataSchema(SQLAlchemyAutoSchema):
|
|||
include_relationships = False
|
||||
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.api.common import ApiErrorSchema
|
||||
from crc.models.file import FileModel, SimpleFileSchema
|
||||
from crc.models.protocol_builder import ProtocolBuilderStatus, ProtocolBuilderStudy
|
||||
from crc.models.workflow import WorkflowSpecCategoryModel, WorkflowState, WorkflowStatus, WorkflowSpecModel, \
|
||||
WorkflowModel
|
||||
|
@ -39,6 +40,10 @@ class StudyModel(db.Model):
|
|||
if self.on_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):
|
||||
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):
|
||||
"""Can load the basic study data for updates to the database, but categories are write only"""
|
||||
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)
|
||||
title = db.Column(db.String, nullable=True)
|
||||
|
||||
# Add Department and School
|
||||
|
||||
|
||||
def encode_auth_token(self):
|
||||
"""
|
||||
|
|
|
@ -73,10 +73,12 @@ class WorkflowModel(db.Model):
|
|||
bpmn_workflow_json = db.Column(db.JSON)
|
||||
status = db.Column(db.Enum(WorkflowStatus))
|
||||
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 = db.relationship("WorkflowSpecModel")
|
||||
spec_version = db.Column(db.String)
|
||||
total_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):
|
||||
|
||||
def __init__(self, entry):
|
||||
self.display_name = entry.displayName.value
|
||||
self.given_name = ", ".join(entry.givenName)
|
||||
self.email = entry.mail.value
|
||||
self.telephone_number = ", ".join(entry.telephoneNumber)
|
||||
self.title = ", ".join(entry.title)
|
||||
self.department = ", ".join(entry.uvaDisplayDepartment)
|
||||
self.affiliation = ", ".join(entry.uvaPersonIAMAffiliation)
|
||||
self.sponsor_type = ", ".join(entry.uvaPersonSponsoredType)
|
||||
self.uid = entry.uid.value
|
||||
def __init__(self):
|
||||
self.display_name = ''
|
||||
self.given_name = ''
|
||||
self.email_address = ''
|
||||
self.telephone_number = ''
|
||||
self.title = ''
|
||||
self.department = ''
|
||||
self.affiliation = ''
|
||||
self.sponsor_type = ''
|
||||
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):
|
||||
search_base = "ou=People,o=University of Virginia,c=US"
|
||||
|
@ -50,7 +63,7 @@ class LdapService(object):
|
|||
if len(self.conn.entries) < 1:
|
||||
raise ApiError("missing_ldap_record", "Unable to locate a user with id %s in LDAP" % uva_uid)
|
||||
entry = self.conn.entries[0]
|
||||
return(LdapUserInfo(entry))
|
||||
return LdapUserInfo.from_entry(entry)
|
||||
|
||||
def search_users(self, query, limit):
|
||||
search_string = LdapService.uid_search_string % query
|
||||
|
@ -64,6 +77,6 @@ class LdapService(object):
|
|||
for entry in self.conn.entries:
|
||||
if count > limit:
|
||||
break
|
||||
results.append(LdapUserInfo(entry))
|
||||
results.append(LdapUserInfo.from_entry(entry))
|
||||
count += 1
|
||||
return results
|
||||
|
|
|
@ -10,6 +10,7 @@ from crc.models.protocol_builder import ProtocolBuilderStudy, ProtocolBuilderStu
|
|||
|
||||
|
||||
class ProtocolBuilderService(object):
|
||||
ENABLED = app.config['PB_ENABLED']
|
||||
STUDY_URL = app.config['PB_USER_STUDIES_URL']
|
||||
INVESTIGATOR_URL = app.config['PB_INVESTIGATORS_URL']
|
||||
REQUIRED_DOCS_URL = app.config['PB_REQUIRED_DOCS_URL']
|
||||
|
@ -17,6 +18,7 @@ class ProtocolBuilderService(object):
|
|||
|
||||
@staticmethod
|
||||
def get_studies(user_id) -> {}:
|
||||
ProtocolBuilderService.__enabled_or_raise()
|
||||
if not isinstance(user_id, str):
|
||||
raise ApiError("invalid_user_id", "This user id is invalid: " + str(user_id))
|
||||
response = requests.get(ProtocolBuilderService.STUDY_URL % user_id)
|
||||
|
@ -30,40 +32,32 @@ class ProtocolBuilderService(object):
|
|||
|
||||
@staticmethod
|
||||
def get_investigators(study_id) -> {}:
|
||||
ProtocolBuilderService.check_args(study_id)
|
||||
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))
|
||||
return ProtocolBuilderService.__make_request(study_id, ProtocolBuilderService.INVESTIGATOR_URL)
|
||||
|
||||
@staticmethod
|
||||
def get_required_docs(study_id) -> Optional[List[ProtocolBuilderRequiredDocument]]:
|
||||
ProtocolBuilderService.check_args(study_id)
|
||||
response = requests.get(ProtocolBuilderService.REQUIRED_DOCS_URL % study_id)
|
||||
return ProtocolBuilderService.__make_request(study_id, ProtocolBuilderService.REQUIRED_DOCS_URL)
|
||||
|
||||
@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:
|
||||
return json.loads(response.text)
|
||||
else:
|
||||
raise ApiError("protocol_builder_error",
|
||||
"Received an invalid response from the protocol builder (status %s): %s" %
|
||||
(response.status_code, response.text))
|
||||
"Received an invalid response from the protocol builder (status %s): %s when calling "
|
||||
"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))
|
||||
return studies
|
||||
|
||||
@staticmethod
|
||||
def get_studies_with_files():
|
||||
"""Returns a list of all studies"""
|
||||
db_studies = session.query(StudyModel).all()
|
||||
return db_studies
|
||||
|
||||
@staticmethod
|
||||
def get_study(study_id, study_model: StudyModel = None):
|
||||
"""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
|
||||
that is available.."""
|
||||
|
||||
# Get PB required docs
|
||||
try:
|
||||
pb_docs = ProtocolBuilderService.get_required_docs(study_id=study_id)
|
||||
except requests.exceptions.ConnectionError as ce:
|
||||
app.logger.error("Failed to connect to the Protocol Builder - %s" % str(ce))
|
||||
# Get PB required docs, if Protocol Builder Service is enabled.
|
||||
if ProtocolBuilderService.ENABLED:
|
||||
try:
|
||||
pb_docs = ProtocolBuilderService.get_required_docs(study_id=study_id)
|
||||
except requests.exceptions.ConnectionError as ce:
|
||||
app.logger.error("Failed to connect to the Protocol Builder - %s" % str(ce))
|
||||
pb_docs = []
|
||||
else:
|
||||
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'])
|
||||
|
||||
documents = {}
|
||||
for code, doc in doc_dictionary.items():
|
||||
|
||||
pb_data = next((item for item in pb_docs if int(item['AUXDOCID']) == int(doc['id'])), None)
|
||||
doc['required'] = False
|
||||
if pb_data:
|
||||
doc['required'] = True
|
||||
if ProtocolBuilderService.ENABLED:
|
||||
pb_data = next((item for item in pb_docs if int(item['AUXDOCID']) == int(doc['id'])), None)
|
||||
doc['required'] = False
|
||||
if pb_data:
|
||||
doc['required'] = True
|
||||
|
||||
doc['study_id'] = study_id
|
||||
doc['code'] = code
|
||||
|
||||
|
@ -153,7 +165,6 @@ class StudyService(object):
|
|||
doc['status'] = workflow.status.value
|
||||
|
||||
documents[code] = doc
|
||||
|
||||
return documents
|
||||
|
||||
|
||||
|
@ -201,9 +212,13 @@ class StudyService(object):
|
|||
|
||||
|
||||
@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
|
||||
in sync with the studies available in protocol builder. """
|
||||
|
||||
if not ProtocolBuilderService.ENABLED:
|
||||
return
|
||||
|
||||
# Get studies matching this user from Protocol Builder
|
||||
pb_studies: List[ProtocolBuilderStudy] = ProtocolBuilderService.get_studies(user.uid)
|
||||
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
python_home = '/usr/local/envs/crcpython3'
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Calculate path to site-packages directory.
|
||||
|
||||
python_version = '.'.join(map(str, sys.version_info[:2]))
|
||||
site_packages = python_home + '/lib/python%s/site-packages' % python_version
|
||||
|
||||
# Add the site-packages directory.
|
||||
|
||||
site.addsitedir(site_packages)
|
||||
|
||||
from crc import app as application
|
|
@ -0,0 +1,90 @@
|
|||
alabaster==0.7.12
|
||||
alembic==1.4.2
|
||||
amqp==2.5.2
|
||||
aniso8601==8.0.0
|
||||
attrs==19.3.0
|
||||
babel==2.8.0
|
||||
bcrypt==3.1.7
|
||||
beautifulsoup4==4.9.1
|
||||
billiard==3.6.3.0
|
||||
blinker==1.4
|
||||
celery==4.4.2
|
||||
certifi==2020.4.5.1
|
||||
cffi==1.14.0
|
||||
chardet==3.0.4
|
||||
click==7.1.2
|
||||
clickclick==1.2.2
|
||||
commonmark==0.9.1
|
||||
configparser==5.0.0
|
||||
connexion==2.7.0
|
||||
coverage==5.1
|
||||
docutils==0.16
|
||||
docxtpl==0.9.2
|
||||
et-xmlfile==1.0.1
|
||||
flask==1.1.2
|
||||
flask-bcrypt==0.7.1
|
||||
flask-cors==3.0.8
|
||||
flask-marshmallow==0.12.0
|
||||
flask-migrate==2.5.3
|
||||
flask-restful==0.3.8
|
||||
flask-sqlalchemy==2.4.1
|
||||
flask-sso==0.4.0
|
||||
future==0.18.2
|
||||
httpretty==1.0.2
|
||||
idna==2.9
|
||||
imagesize==1.2.0
|
||||
importlib-metadata==1.6.0
|
||||
inflection==0.4.0
|
||||
itsdangerous==1.1.0
|
||||
jdcal==1.4.1
|
||||
jinja2==2.11.2
|
||||
jsonschema==3.2.0
|
||||
kombu==4.6.8
|
||||
ldap3==2.7
|
||||
lxml==4.5.1
|
||||
mako==1.1.2
|
||||
markupsafe==1.1.1
|
||||
marshmallow==3.6.0
|
||||
marshmallow-enum==1.5.1
|
||||
marshmallow-sqlalchemy==0.23.0
|
||||
numpy==1.18.4
|
||||
openapi-spec-validator==0.2.8
|
||||
openpyxl==3.0.3
|
||||
packaging==20.4
|
||||
pandas==1.0.3
|
||||
psycopg2-binary==2.8.5
|
||||
pyasn1==0.4.8
|
||||
pycparser==2.20
|
||||
pygments==2.6.1
|
||||
pyjwt==1.7.1
|
||||
pyparsing==2.4.7
|
||||
pyrsistent==0.16.0
|
||||
python-dateutil==2.8.1
|
||||
python-docx==0.8.10
|
||||
python-editor==1.0.4
|
||||
pytz==2020.1
|
||||
pyyaml==5.3.1
|
||||
recommonmark==0.6.0
|
||||
requests==2.23.0
|
||||
six==1.14.0
|
||||
snowballstemmer==2.0.0
|
||||
soupsieve==2.0.1
|
||||
sphinx==3.0.3
|
||||
sphinxcontrib-applehelp==1.0.2
|
||||
sphinxcontrib-devhelp==1.0.2
|
||||
sphinxcontrib-htmlhelp==1.0.3
|
||||
sphinxcontrib-jsmath==1.0.1
|
||||
sphinxcontrib-qthelp==1.0.3
|
||||
sphinxcontrib-serializinghtml==1.1.4
|
||||
spiffworkflow
|
||||
sqlalchemy==1.3.17
|
||||
swagger-ui-bundle==0.0.6
|
||||
urllib3==1.25.9
|
||||
vine==1.3.0
|
||||
waitress==1.4.3
|
||||
webob==1.8.6
|
||||
webtest==2.0.35
|
||||
werkzeug==1.0.1
|
||||
xlrd==1.2.0
|
||||
xlsxwriter==1.2.8
|
||||
zipp==3.1.0
|
|
@ -0,0 +1,4 @@
|
|||
jq -r '.default
|
||||
| to_entries[]
|
||||
| .key + .value.version' \
|
||||
../Pipfile.lock > requirements.txt
|
|
@ -1,7 +1,9 @@
|
|||
# Set environment variable to testing before loading.
|
||||
# IMPORTANT - Environment must be loaded before app, models, etc....
|
||||
import json
|
||||
import os
|
||||
os.environ["TESTING"] = "true"
|
||||
|
||||
import json
|
||||
import unittest
|
||||
import urllib.parse
|
||||
import datetime
|
||||
|
@ -10,10 +12,6 @@ from crc.models.protocol_builder import ProtocolBuilderStatus
|
|||
from crc.models.study import StudyModel
|
||||
from crc.services.file_service import FileService
|
||||
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.workflow import WorkflowSpecModel, WorkflowSpecModelSchema, WorkflowModel
|
||||
from crc.models.user import UserModel
|
||||
|
@ -32,6 +30,10 @@ class BaseTest(unittest.TestCase):
|
|||
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 = {}
|
||||
test_uid = "dhf8r"
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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.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';
|
||||
self.load_example_data()
|
||||
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(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('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):
|
||||
self.load_example_data()
|
||||
rv = self.app.get('/v1.0/user')
|
||||
|
|
|
@ -21,7 +21,7 @@ class TestLdapService(BaseTest):
|
|||
self.assertEqual("lb3dp", user_info.uid)
|
||||
self.assertEqual("Laura Barnes", user_info.display_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("E0:Associate Professor of Systems and Information Engineering", user_info.title)
|
||||
self.assertEqual("E0:EN-Eng Sys and Environment", user_info.department)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from unittest.mock import patch
|
||||
|
||||
from crc.services.protocol_builder import ProtocolBuilderService
|
||||
from tests.base_test import BaseTest
|
||||
from crc.services.protocol_builder import ProtocolBuilderService
|
||||
|
||||
|
||||
class TestProtocolBuilder(BaseTest):
|
||||
|
@ -10,6 +10,7 @@ class TestProtocolBuilder(BaseTest):
|
|||
|
||||
@patch('crc.services.protocol_builder.requests.get')
|
||||
def test_get_studies(self, mock_get):
|
||||
ProtocolBuilderService.ENABLED = True
|
||||
mock_get.return_value.ok = True
|
||||
mock_get.return_value.text = self.protocol_builder_response('user_studies.json')
|
||||
response = ProtocolBuilderService.get_studies(self.test_uid)
|
||||
|
@ -17,6 +18,7 @@ class TestProtocolBuilder(BaseTest):
|
|||
|
||||
@patch('crc.services.protocol_builder.requests.get')
|
||||
def test_get_investigators(self, mock_get):
|
||||
ProtocolBuilderService.ENABLED = True
|
||||
mock_get.return_value.ok = True
|
||||
mock_get.return_value.text = self.protocol_builder_response('investigators.json')
|
||||
response = ProtocolBuilderService.get_investigators(self.test_study_id)
|
||||
|
@ -28,6 +30,7 @@ class TestProtocolBuilder(BaseTest):
|
|||
|
||||
@patch('crc.services.protocol_builder.requests.get')
|
||||
def test_get_required_docs(self, mock_get):
|
||||
ProtocolBuilderService.ENABLED = True
|
||||
mock_get.return_value.ok = True
|
||||
mock_get.return_value.text = self.protocol_builder_response('required_docs.json')
|
||||
response = ProtocolBuilderService.get_required_docs(self.test_study_id)
|
||||
|
@ -37,6 +40,7 @@ class TestProtocolBuilder(BaseTest):
|
|||
|
||||
@patch('crc.services.protocol_builder.requests.get')
|
||||
def test_get_details(self, mock_get):
|
||||
ProtocolBuilderService.ENABLED = True
|
||||
mock_get.return_value.ok = True
|
||||
mock_get.return_value.text = self.protocol_builder_response('study_details.json')
|
||||
response = ProtocolBuilderService.get_study_details(self.test_study_id)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import json
|
||||
from tests.base_test import BaseTest
|
||||
from datetime import datetime, timezone
|
||||
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.study import StudyModel, StudySchema
|
||||
from crc.models.workflow import WorkflowSpecModel, WorkflowModel, WorkflowSpecCategoryModel
|
||||
from tests.base_test import BaseTest
|
||||
from crc.services.protocol_builder import ProtocolBuilderService
|
||||
|
||||
|
||||
class TestStudyApi(BaseTest):
|
||||
|
@ -38,24 +39,12 @@ class TestStudyApi(BaseTest):
|
|||
study = session.query(StudyModel).first()
|
||||
self.assertIsNotNone(study)
|
||||
|
||||
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_investigators') # mock_studies
|
||||
@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):
|
||||
def test_get_study(self):
|
||||
"""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."""
|
||||
|
||||
# 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)
|
||||
|
||||
"""NOTE: The protocol builder is not enabled or mocked out. As the master workflow (which is empty),
|
||||
and the test workflow do not need it, and it is disabled in the configuration."""
|
||||
new_study = self.add_test_study()
|
||||
new_study = session.query(StudyModel).filter_by(id=new_study["id"]).first()
|
||||
# Add a category
|
||||
|
@ -65,7 +54,7 @@ class TestStudyApi(BaseTest):
|
|||
# Create a workflow specification
|
||||
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.
|
||||
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()
|
||||
|
||||
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_studies') # mock_studies
|
||||
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()
|
||||
s = StudyModel(
|
||||
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.ind_number, json_data['ind_number'])
|
||||
|
||||
|
||||
def test_delete_study(self):
|
||||
self.load_example_data()
|
||||
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.stats import TaskEventModel
|
||||
from crc.models.workflow import WorkflowStatus
|
||||
from crc.services.protocol_builder import ProtocolBuilderService
|
||||
from crc.services.workflow_service import WorkflowService
|
||||
from tests.base_test import BaseTest
|
||||
|
||||
|
@ -302,6 +303,9 @@ class TestTasksApi(BaseTest):
|
|||
|
||||
@patch('crc.services.protocol_builder.requests.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.
|
||||
mock_get.return_value.ok = True
|
||||
mock_get.return_value.text = self.protocol_builder_response('investigators.json')
|
||||
|
|
|
@ -20,21 +20,7 @@ class TestWorkflowSpecValidation(BaseTest):
|
|||
json_data = json.loads(rv.get_data(as_text=True))
|
||||
return ApiErrorSchema(many=True).load(json_data)
|
||||
|
||||
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_investigators') # mock_studies
|
||||
@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)
|
||||
def test_successful_validation_of_test_workflows(self):
|
||||
|
||||
self.assertEqual(0, len(self.validate_workflow("parallel_tasks")))
|
||||
self.assertEqual(0, len(self.validate_workflow("decision_table")))
|
||||
|
|
Loading…
Reference in New Issue