commit
7b8c797996
13
crc/api.yml
13
crc/api.yml
|
@ -854,6 +854,19 @@ paths:
|
|||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Approval"
|
||||
/approval/csv:
|
||||
get:
|
||||
operationId: crc.api.approval.get_csv
|
||||
summary: Provides a list of all users for all approved studies
|
||||
tags:
|
||||
- Approvals
|
||||
responses:
|
||||
'200':
|
||||
description: An array of approvals
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
components:
|
||||
securitySchemes:
|
||||
jwt:
|
||||
|
|
|
@ -1,19 +1,30 @@
|
|||
import json
|
||||
import pickle
|
||||
from base64 import b64decode
|
||||
from datetime import datetime
|
||||
|
||||
from SpiffWorkflow import Workflow
|
||||
from SpiffWorkflow.serializer.dict import DictionarySerializer
|
||||
from SpiffWorkflow.serializer.json import JSONSerializer
|
||||
from flask import g
|
||||
|
||||
from crc import app, db, session
|
||||
|
||||
from crc.api.common import ApiError, ApiErrorSchema
|
||||
from crc.models.approval import Approval, ApprovalModel, ApprovalSchema
|
||||
from crc.models.approval import Approval, ApprovalModel, ApprovalSchema, ApprovalStatus
|
||||
from crc.models.ldap import LdapSchema
|
||||
from crc.models.study import Study
|
||||
from crc.models.workflow import WorkflowModel
|
||||
from crc.services.approval_service import ApprovalService
|
||||
from crc.services.ldap_service import LdapService
|
||||
from crc.services.workflow_processor import WorkflowProcessor
|
||||
|
||||
|
||||
def get_approvals(everything=False):
|
||||
if everything:
|
||||
db_approvals = ApprovalService.get_all_approvals()
|
||||
approvals = ApprovalService.get_all_approvals()
|
||||
else:
|
||||
db_approvals = ApprovalService.get_approvals_per_user(g.user.uid)
|
||||
approvals = [Approval.from_model(approval_model) for approval_model in db_approvals]
|
||||
|
||||
approvals = ApprovalService.get_approvals_per_user(g.user.uid)
|
||||
results = ApprovalSchema(many=True).dump(approvals)
|
||||
return results
|
||||
|
||||
|
@ -25,6 +36,57 @@ def get_approvals_for_study(study_id=None):
|
|||
return results
|
||||
|
||||
|
||||
# ----- Being decent into madness ---- #
|
||||
def get_csv():
|
||||
"""A huge bit of a one-off for RRT, but 3 weeks of midnight work can convince a
|
||||
man to do just about anything"""
|
||||
approvals = ApprovalService.get_all_approvals()
|
||||
output = []
|
||||
errors = []
|
||||
ldapService = LdapService()
|
||||
for approval in approvals:
|
||||
try:
|
||||
if approval.status != ApprovalStatus.APPROVED.value:
|
||||
continue
|
||||
for related_approval in approval.related_approvals:
|
||||
if related_approval.status != ApprovalStatus.APPROVED.value:
|
||||
continue
|
||||
workflow = db.session.query(WorkflowModel).filter(WorkflowModel.id == approval.workflow_id).first()
|
||||
personnel = extract_personnel(workflow.bpmn_workflow_json)
|
||||
pi_uid = workflow.study.primary_investigator_id
|
||||
pi_details = ldapService.user_info(pi_uid)
|
||||
details = []
|
||||
details.append(pi_details)
|
||||
for person in personnel:
|
||||
uid = person['PersonnelComputingID']['value']
|
||||
details.append(ldapService.user_info(uid))
|
||||
|
||||
for person in details:
|
||||
output.append({
|
||||
"pi_uid": pi_details.uid,
|
||||
"pi": pi_details.display_name,
|
||||
"name": person.display_name,
|
||||
"email": person.email_address
|
||||
})
|
||||
except Exception as e:
|
||||
errors.append("Error pulling data for workflow #%i: %s" % (approval.workflow_id, str(e)))
|
||||
return {"results": output, "errors": errors }
|
||||
|
||||
def extract_personnel(data):
|
||||
data = json.loads(data)
|
||||
last_task = find_task(data['last_task']['__uuid__'], data['task_tree'])
|
||||
last_task_personnel = pickle.loads(b64decode(last_task['data']['personnel']['__bytes__']))
|
||||
return last_task_personnel
|
||||
|
||||
def find_task(uuid, task):
|
||||
if task['id']['__uuid__'] == uuid:
|
||||
return task
|
||||
for child in task['children']:
|
||||
task = find_task(uuid, child)
|
||||
if task:
|
||||
return task
|
||||
# ----- come back to the world of the living ---- #
|
||||
|
||||
def update_approval(approval_id, body):
|
||||
if approval_id is None:
|
||||
raise ApiError('unknown_approval', 'Please provide a valid Approval ID.')
|
||||
|
@ -33,12 +95,14 @@ def update_approval(approval_id, body):
|
|||
if approval_model is None:
|
||||
raise ApiError('unknown_approval', 'The approval "' + str(approval_id) + '" is not recognized.')
|
||||
|
||||
approval: Approval = ApprovalSchema().load(body)
|
||||
if approval_model.approver_uid != g.user.uid:
|
||||
raise ApiError("not_your_approval", "You may not modify this approval. It belongs to another user.")
|
||||
|
||||
approval.update_model(approval_model)
|
||||
approval_model.status = body['status']
|
||||
approval_model.message = body['message']
|
||||
approval_model.date_approved = datetime.now()
|
||||
session.add(approval_model)
|
||||
session.commit()
|
||||
|
||||
result = ApprovalSchema().dump(approval)
|
||||
result = ApprovalSchema().dump(approval_model)
|
||||
return result
|
||||
|
|
|
@ -4,7 +4,7 @@ from flask import g, request
|
|||
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
|
||||
from crc.services.ldap_service import LdapService, LdapModel
|
||||
|
||||
"""
|
||||
.. module:: crc.api.user
|
||||
|
@ -78,7 +78,7 @@ def sso():
|
|||
return response
|
||||
|
||||
|
||||
def _handle_login(user_info: LdapUserInfo, redirect_url=app.config['FRONTEND_AUTH_CALLBACK']):
|
||||
def _handle_login(user_info: LdapModel, 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.
|
||||
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import enum
|
||||
|
||||
import marshmallow
|
||||
from ldap3.core.exceptions import LDAPSocketOpenError
|
||||
from marshmallow import INCLUDE
|
||||
from marshmallow import INCLUDE, fields
|
||||
from sqlalchemy import func
|
||||
|
||||
from crc import db, ma
|
||||
from crc import db, ma, app
|
||||
from crc.api.common import ApiError
|
||||
from crc.models.file import FileDataModel
|
||||
from crc.models.ldap import LdapSchema
|
||||
from crc.models.study import StudyModel
|
||||
from crc.models.workflow import WorkflowModel
|
||||
from crc.services.ldap_service import LdapService
|
||||
from crc.services.file_service import FileService
|
||||
from crc.services.ldap_service import LdapService
|
||||
|
||||
|
||||
class ApprovalStatus(enum.Enum):
|
||||
|
@ -33,13 +33,14 @@ 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')
|
||||
study = db.relationship(StudyModel)
|
||||
workflow_id = db.Column(db.Integer, db.ForeignKey(WorkflowModel.id), nullable=False)
|
||||
workflow = db.relationship(WorkflowModel)
|
||||
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, default='')
|
||||
date_created = db.Column(db.DateTime(timezone=True), default=func.now())
|
||||
date_approved = db.Column(db.DateTime(timezone=True), default=None)
|
||||
version = db.Column(db.Integer) # Incremented integer, so 1,2,3 as requests are made.
|
||||
approval_files = db.relationship(ApprovalFile, back_populates="approval",
|
||||
cascade="all, delete, delete-orphan",
|
||||
|
@ -63,40 +64,22 @@ class Approval(object):
|
|||
instance.status = model.status
|
||||
instance.message = model.message
|
||||
instance.date_created = model.date_created
|
||||
instance.date_approved = model.date_approved
|
||||
instance.version = model.version
|
||||
instance.title = ''
|
||||
instance.related_approvals = []
|
||||
|
||||
if model.study:
|
||||
instance.title = model.study.title
|
||||
|
||||
instance.approver = {}
|
||||
ldap_service = LdapService()
|
||||
try:
|
||||
ldap_service = LdapService()
|
||||
user_info = ldap_service.user_info(model.approver_uid)
|
||||
instance.approver['uid'] = model.approver_uid
|
||||
instance.approver['display_name'] = user_info.display_name
|
||||
instance.approver['title'] = user_info.title
|
||||
instance.approver['department'] = user_info.department
|
||||
except (ApiError, LDAPSocketOpenError) as exception:
|
||||
user_info = None
|
||||
instance.approver['display_name'] = 'Unknown'
|
||||
instance.approver['department'] = 'currently not available'
|
||||
|
||||
instance.primary_investigator = {}
|
||||
try:
|
||||
ldap_service = LdapService()
|
||||
user_info = ldap_service.user_info(model.study.primary_investigator_id)
|
||||
instance.primary_investigator['uid'] = model.approver_uid
|
||||
instance.primary_investigator['display_name'] = user_info.display_name
|
||||
instance.primary_investigator['title'] = user_info.title
|
||||
instance.primary_investigator['department'] = user_info.department
|
||||
except (ApiError, LDAPSocketOpenError) as exception:
|
||||
user_info = None
|
||||
instance.primary_investigator['display_name'] = 'Primary Investigator details'
|
||||
instance.primary_investigator['department'] = 'currently not available'
|
||||
|
||||
instance.approver = ldap_service.user_info(model.approver_uid)
|
||||
instance.primary_investigator = ldap_service.user_info(model.study.primary_investigator_id)
|
||||
except ApiError as ae:
|
||||
app.logger.error("Ldap lookup failed for approval record %i" % model.id)
|
||||
|
||||
doc_dictionary = FileService.get_reference_data(FileService.DOCUMENT_LIST, 'code', ['id'])
|
||||
|
||||
instance.associated_files = []
|
||||
for approval_file in model.approval_files:
|
||||
try:
|
||||
|
@ -125,11 +108,17 @@ class Approval(object):
|
|||
|
||||
|
||||
class ApprovalSchema(ma.Schema):
|
||||
|
||||
approver = fields.Nested(LdapSchema, dump_only=True)
|
||||
primary_investigator = fields.Nested(LdapSchema, dump_only=True)
|
||||
related_approvals = fields.List(fields.Nested('ApprovalSchema', allow_none=True, dump_only=True))
|
||||
|
||||
class Meta:
|
||||
model = Approval
|
||||
fields = ["id", "study_id", "workflow_id", "version", "title",
|
||||
"status", "message", "approver", "primary_investigator",
|
||||
"associated_files", "date_created"]
|
||||
"associated_files", "date_created", "date_approved",
|
||||
"related_approvals"]
|
||||
unknown = INCLUDE
|
||||
|
||||
@marshmallow.post_load
|
||||
|
@ -137,30 +126,4 @@ class ApprovalSchema(ma.Schema):
|
|||
"""Loads the basic approval data for updates to the database"""
|
||||
return Approval(**data)
|
||||
|
||||
# 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"
|
||||
# }
|
||||
# ]
|
||||
# }
|
||||
# ...
|
||||
# ]
|
||||
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
from flask_marshmallow.sqla import SQLAlchemyAutoSchema
|
||||
from marshmallow import EXCLUDE
|
||||
from sqlalchemy import func, inspect
|
||||
|
||||
from crc import db
|
||||
|
||||
|
||||
class LdapModel(db.Model):
|
||||
uid = db.Column(db.String, primary_key=True)
|
||||
display_name = db.Column(db.String)
|
||||
given_name = db.Column(db.String)
|
||||
email_address = db.Column(db.String)
|
||||
telephone_number = db.Column(db.String)
|
||||
title = db.Column(db.String)
|
||||
department = db.Column(db.String)
|
||||
affiliation = db.Column(db.String)
|
||||
sponsor_type = db.Column(db.String)
|
||||
date_cached = db.Column(db.DateTime(timezone=True), default=func.now())
|
||||
|
||||
@classmethod
|
||||
def from_entry(cls, entry):
|
||||
return LdapModel(uid=entry.uid.value,
|
||||
display_name=entry.displayName.value,
|
||||
given_name=", ".join(entry.givenName),
|
||||
email_address=entry.mail.value,
|
||||
telephone_number=entry.telephoneNumber.value,
|
||||
title=", ".join(entry.title),
|
||||
department=", ".join(entry.uvaDisplayDepartment),
|
||||
affiliation=", ".join(entry.uvaPersonIAMAffiliation),
|
||||
sponsor_type=", ".join(entry.uvaPersonSponsoredType))
|
||||
|
||||
|
||||
class LdapSchema(SQLAlchemyAutoSchema):
|
||||
class Meta:
|
||||
model = LdapModel
|
||||
load_instance = True
|
||||
include_relationships = True
|
||||
include_fk = True # Includes foreign keys
|
||||
unknown = EXCLUDE
|
|
@ -5,7 +5,8 @@ from sqlalchemy import desc
|
|||
from crc import db, session
|
||||
from crc.api.common import ApiError
|
||||
|
||||
from crc.models.approval import ApprovalModel, ApprovalStatus, ApprovalFile
|
||||
from crc.models.approval import ApprovalModel, ApprovalStatus, ApprovalFile, Approval
|
||||
from crc.models.study import StudyModel
|
||||
from crc.models.workflow import WorkflowModel
|
||||
from crc.services.file_service import FileService
|
||||
|
||||
|
@ -14,22 +15,57 @@ class ApprovalService(object):
|
|||
"""Provides common tools for working with an Approval"""
|
||||
|
||||
@staticmethod
|
||||
def get_approvals_per_user(approver_uid):
|
||||
"""Returns a list of all approvals for the given user (approver)"""
|
||||
db_approvals = session.query(ApprovalModel).filter_by(approver_uid=approver_uid).all()
|
||||
return db_approvals
|
||||
def __one_approval_from_study(study, approver_uid = None):
|
||||
"""Returns one approval, with all additional approvals as 'related_approvals',
|
||||
the main approval can be pinned to an approver with an optional argument.
|
||||
Will return null if no approvals exist on the study."""
|
||||
main_approval = None
|
||||
related_approvals = []
|
||||
approvals = db.session.query(ApprovalModel).filter(ApprovalModel.study_id == study.id).all()
|
||||
for approval_model in approvals:
|
||||
if approval_model.approver_uid == approver_uid:
|
||||
main_approval = Approval.from_model(approval_model)
|
||||
else:
|
||||
related_approvals.append(Approval.from_model(approval_model))
|
||||
if not main_approval and len(related_approvals) > 0:
|
||||
main_approval = related_approvals[0]
|
||||
related_approvals = related_approvals[1:]
|
||||
if len(related_approvals) > 0:
|
||||
main_approval.related_approvals = related_approvals
|
||||
return main_approval
|
||||
|
||||
@staticmethod
|
||||
def get_approvals_for_study(study_id):
|
||||
"""Returns a list of all approvals for the given study"""
|
||||
db_approvals = session.query(ApprovalModel).filter_by(study_id=study_id).all()
|
||||
return db_approvals
|
||||
def get_approvals_per_user(approver_uid):
|
||||
"""Returns a list of approval objects (not db models) for the given
|
||||
approver. """
|
||||
studies = db.session.query(StudyModel).join(ApprovalModel).\
|
||||
filter(ApprovalModel.approver_uid == approver_uid).all()
|
||||
approvals = []
|
||||
for study in studies:
|
||||
approval = ApprovalService.__one_approval_from_study(study, approver_uid)
|
||||
if approval:
|
||||
approvals.append(approval)
|
||||
return approvals
|
||||
|
||||
@staticmethod
|
||||
def get_all_approvals():
|
||||
"""Returns a list of all approvlas"""
|
||||
db_approvals = session.query(ApprovalModel).all()
|
||||
return db_approvals
|
||||
"""Returns a list of all approval objects (not db models), one record
|
||||
per study, with any associated approvals grouped under the first approval."""
|
||||
studies = db.session.query(StudyModel).all()
|
||||
approvals = []
|
||||
for study in studies:
|
||||
approval = ApprovalService.__one_approval_from_study(study)
|
||||
if approval:
|
||||
approvals.append(approval)
|
||||
return approvals
|
||||
|
||||
@staticmethod
|
||||
def get_approvals_for_study(study_id):
|
||||
"""Returns an array of Approval objects for the study, it does not
|
||||
compute the related approvals."""
|
||||
db_approvals = session.query(ApprovalModel).filter_by(study_id=study_id).all()
|
||||
return [Approval.from_model(approval_model) for approval_model in db_approvals]
|
||||
|
||||
|
||||
@staticmethod
|
||||
def update_approval(approval_id, approver_uid, status):
|
||||
|
|
|
@ -1,40 +1,15 @@
|
|||
import os
|
||||
|
||||
from attr import asdict
|
||||
from ldap3.core.exceptions import LDAPExceptionError
|
||||
|
||||
from crc import app
|
||||
from crc import app, db
|
||||
from ldap3 import Connection, Server, MOCK_SYNC
|
||||
|
||||
from crc.api.common import ApiError
|
||||
from crc.models.ldap import LdapModel, LdapSchema
|
||||
|
||||
|
||||
class LdapUserInfo(object):
|
||||
|
||||
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"
|
||||
attributes = ['uid', 'cn', 'sn', 'displayName', 'givenName', 'mail', 'objectClass', 'UvaDisplayDepartment',
|
||||
|
@ -63,12 +38,16 @@ class LdapService(object):
|
|||
self.conn.unbind()
|
||||
|
||||
def user_info(self, uva_uid):
|
||||
search_string = LdapService.uid_search_string % uva_uid
|
||||
self.conn.search(LdapService.search_base, search_string, attributes=LdapService.attributes)
|
||||
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.from_entry(entry)
|
||||
user_info = db.session.query(LdapModel).filter(LdapModel.uid == uva_uid).first()
|
||||
if not user_info:
|
||||
search_string = LdapService.uid_search_string % uva_uid
|
||||
self.conn.search(LdapService.search_base, search_string, attributes=LdapService.attributes)
|
||||
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]
|
||||
user_info = LdapModel.from_entry(entry)
|
||||
db.session.add(user_info)
|
||||
return user_info
|
||||
|
||||
def search_users(self, query, limit):
|
||||
if len(query.strip()) < 3:
|
||||
|
@ -95,7 +74,7 @@ class LdapService(object):
|
|||
for entry in self.conn.entries:
|
||||
if count > limit:
|
||||
break
|
||||
results.append(LdapUserInfo.from_entry(entry))
|
||||
results.append(LdapSchema().dump(LdapModel.from_entry(entry)))
|
||||
count += 1
|
||||
except LDAPExceptionError as le:
|
||||
app.logger.info("Failed to execute ldap search. %s", str(le))
|
||||
|
|
|
@ -196,8 +196,8 @@ class LookupService(object):
|
|||
we return a lookup data model."""
|
||||
user_list = []
|
||||
for user in users:
|
||||
user_list.append( {"value": user.uid,
|
||||
"label": user.display_name + " (" + user.uid + ")",
|
||||
"data": user.__dict__
|
||||
user_list.append( {"value": user['uid'],
|
||||
"label": user['display_name'] + " (" + user['uid'] + ")",
|
||||
"data": user
|
||||
})
|
||||
return user_list
|
|
@ -10,6 +10,7 @@ from ldap3.core.exceptions import LDAPSocketOpenError
|
|||
from crc import db, session, app
|
||||
from crc.api.common import ApiError
|
||||
from crc.models.file import FileModel, FileModelSchema, File
|
||||
from crc.models.ldap import LdapSchema
|
||||
from crc.models.protocol_builder import ProtocolBuilderStudy, ProtocolBuilderStatus
|
||||
from crc.models.stats import TaskEventModel
|
||||
from crc.models.study import StudyModel, Study, Category, WorkflowMetadata
|
||||
|
@ -56,9 +57,7 @@ class StudyService(object):
|
|||
study = Study.from_model(study_model)
|
||||
study.categories = StudyService.get_categories()
|
||||
workflow_metas = StudyService.__get_workflow_metas(study_id)
|
||||
approvals = ApprovalService.get_approvals_for_study(study.id)
|
||||
study.approvals = [Approval.from_model(approval_model) for approval_model in approvals]
|
||||
|
||||
study.approvals = ApprovalService.get_approvals_for_study(study.id)
|
||||
files = FileService.get_files_for_study(study.id)
|
||||
files = (File.from_models(model, FileService.get_file_data(model.id),
|
||||
FileService.get_doc_dictionary()) for model in files)
|
||||
|
@ -208,7 +207,7 @@ class StudyService(object):
|
|||
def get_ldap_dict_if_available(user_id):
|
||||
try:
|
||||
ldap_service = LdapService()
|
||||
return ldap_service.user_info(user_id).__dict__
|
||||
return LdapSchema().dump(ldap_service.user_info(user_id))
|
||||
except ApiError as ae:
|
||||
app.logger.info(str(ae))
|
||||
return {"error": str(ae)}
|
||||
|
|
|
@ -107,11 +107,13 @@ class WorkflowService(object):
|
|||
|
||||
if not hasattr(task.task_spec, 'form'): return
|
||||
|
||||
form_data = {}
|
||||
form_data = task.data # Just like with the front end, we start with what was already there, and modify it.
|
||||
for field in task_api.form.fields:
|
||||
if required_only and (not field.has_validation(Task.VALIDATION_REQUIRED) or
|
||||
field.get_validation(Task.VALIDATION_REQUIRED).lower().strip() != "true"):
|
||||
continue # Don't include any fields that aren't specifically marked as required.
|
||||
if field.has_property("read_only") and field.get_property("read_only").lower().strip() == "true":
|
||||
continue # Don't mess about with read only fields.
|
||||
if field.has_property(Task.PROP_OPTIONS_REPEAT):
|
||||
group = field.get_property(Task.PROP_OPTIONS_REPEAT)
|
||||
if group not in form_data:
|
||||
|
|
|
@ -5,10 +5,7 @@
|
|||
<bpmn:outgoing>SequenceFlow_05ja25w</bpmn:outgoing>
|
||||
</bpmn:startEvent>
|
||||
<bpmn:manualTask id="ManualTask_Instructions" name="Read RRP Instructions">
|
||||
<bpmn:documentation>## **Beta Stage: All data entered will be destroyed before public launch**
|
||||
|
||||
|
||||
### UNIVERSITY OF VIRGINIA RESEARCH
|
||||
<bpmn:documentation>### UNIVERSITY OF VIRGINIA RESEARCH
|
||||
[From Research Ramp-up Guidance](https://research.virginia.edu/research-ramp-guidance)
|
||||
|
||||
|
||||
|
@ -319,8 +316,11 @@ When your Research Ramp-up Plan is complete and ready to submit for review and a
|
|||
<camunda:property id="repeat" value="shared" />
|
||||
<camunda:property id="description" value="Enter the number of square feet in this space." />
|
||||
</camunda:properties>
|
||||
<camunda:validation>
|
||||
<camunda:constraint name="required" config="true" />
|
||||
</camunda:validation>
|
||||
</camunda:formField>
|
||||
<camunda:formField id="SharedSpacePercentUsable" label="Percent of Space Usable By Personnel" type="long">
|
||||
<camunda:formField id="SharedSpacePercentUsable" label="Percent of Space Usable By Personnel" type="long" defaultValue="100">
|
||||
<camunda:properties>
|
||||
<camunda:property id="repeat" value="shared" />
|
||||
<camunda:property id="description" value="If known, enter a number between 1 & 100 which indicates the approximate percent of the total space personnel will work in and move around in during the workday." />
|
||||
|
@ -328,6 +328,7 @@ When your Research Ramp-up Plan is complete and ready to submit for review and a
|
|||
<camunda:validation>
|
||||
<camunda:constraint name="min" config="1" />
|
||||
<camunda:constraint name="max" config="100" />
|
||||
<camunda:constraint name="required" config="true" />
|
||||
</camunda:validation>
|
||||
</camunda:formField>
|
||||
<camunda:formField id="SharedSpaceMaxPersonnel" label="Maximum Number of Personnel Occupying Space" type="long">
|
||||
|
@ -423,7 +424,7 @@ Submit one entry for each space the PI is the exclusive investigator. If all sp
|
|||
<camunda:constraint name="required" config="true" />
|
||||
</camunda:validation>
|
||||
</camunda:formField>
|
||||
<camunda:formField id="ExclusiveSpacePercentUsable" label="Percent of Space Usable By Personnel" type="long">
|
||||
<camunda:formField id="ExclusiveSpacePercentUsable" label="Percent of Space Usable By Personnel" type="long" defaultValue="100">
|
||||
<camunda:properties>
|
||||
<camunda:property id="repeat" value="exclusive" />
|
||||
<camunda:property id="description" value="If known, enter a number between 1 & 100 which indicates the approximate percent of the total space personnel will work in and move around in during the workday." />
|
||||
|
@ -431,6 +432,7 @@ Submit one entry for each space the PI is the exclusive investigator. If all sp
|
|||
<camunda:validation>
|
||||
<camunda:constraint name="min" config="1" />
|
||||
<camunda:constraint name="max" config="100" />
|
||||
<camunda:constraint name="required" config="true" />
|
||||
</camunda:validation>
|
||||
</camunda:formField>
|
||||
<camunda:formField id="ExclusiveSpaceMaxPersonnel" label="Maximum Number of Personnel Occupying Space" type="long">
|
||||
|
@ -644,7 +646,7 @@ If a rejection notification is received, go back to the first step that needs to
|
|||
</camunda:formField>
|
||||
<camunda:formField id="ExclusiveSpaceBuilding" label="Room No. & Building Name" type="autocomplete">
|
||||
<camunda:properties>
|
||||
<camunda:property id="repeat" value="Exclusive" />
|
||||
<camunda:property id="repeat" value="exclusive" />
|
||||
<camunda:property id="read_only" value="true" />
|
||||
<camunda:property id="spreadsheet.name" value="Buildinglist.xls" />
|
||||
<camunda:property id="spreadsheet.value.column" value="Value" />
|
||||
|
@ -656,14 +658,14 @@ If a rejection notification is received, go back to the first step that needs to
|
|||
</camunda:formField>
|
||||
<camunda:formField id="ExclusiveSpaceRoomID" label="Exclusive Space Room ID" type="string">
|
||||
<camunda:properties>
|
||||
<camunda:property id="repeat" value="Exclusive" />
|
||||
<camunda:property id="repeat" value="exclusive" />
|
||||
<camunda:property id="read_only" value="true" />
|
||||
</camunda:properties>
|
||||
</camunda:formField>
|
||||
<camunda:formField id="ExclusiveSpaceType" label="Space Room Type" type="enum">
|
||||
<camunda:properties>
|
||||
<camunda:property id="read_only" value="true" />
|
||||
<camunda:property id="repeat" value="Exclusive" />
|
||||
<camunda:property id="repeat" value="exclusive" />
|
||||
</camunda:properties>
|
||||
<camunda:value id="Lab" name="Lab" />
|
||||
<camunda:value id="Office" name="Office" />
|
||||
|
@ -672,7 +674,7 @@ If a rejection notification is received, go back to the first step that needs to
|
|||
<camunda:properties>
|
||||
<camunda:property id="ldap.lookup" value="true" />
|
||||
<camunda:property id="placeholder" value="wxy0z or Smith" />
|
||||
<camunda:property id="repeat" value="Exclusive" />
|
||||
<camunda:property id="repeat" value="exclusive" />
|
||||
</camunda:properties>
|
||||
<camunda:validation>
|
||||
<camunda:constraint name="required" config="true" />
|
||||
|
@ -683,7 +685,7 @@ If a rejection notification is received, go back to the first step that needs to
|
|||
<camunda:property id="spreadsheet.name" value="Buildinglist.xls" />
|
||||
<camunda:property id="spreadsheet.value.column" value="Value" />
|
||||
<camunda:property id="spreadsheet.label.column" value="Building Name" />
|
||||
<camunda:property id="repeat" value="Shared" />
|
||||
<camunda:property id="repeat" value="shared" />
|
||||
<camunda:property id="repeat_title" value="Any missing Area Monitors for Shared Spaces must be entered" />
|
||||
<camunda:property id="read_only" value="true" />
|
||||
<camunda:property id="repeat_hide_expression" value="model.isAllSharedAreaMonitors" />
|
||||
|
@ -693,13 +695,13 @@ If a rejection notification is received, go back to the first step that needs to
|
|||
<camunda:formField id="SharedSpaceRoomID" label="Shared Space Room ID" type="string">
|
||||
<camunda:properties>
|
||||
<camunda:property id="read_only" value="true" />
|
||||
<camunda:property id="repeat" value="Shared" />
|
||||
<camunda:property id="repeat" value="shared" />
|
||||
</camunda:properties>
|
||||
</camunda:formField>
|
||||
<camunda:formField id="SharedSpaceAMComputingID" label="Area Monitor" type="autocomplete">
|
||||
<camunda:properties>
|
||||
<camunda:property id="ldap.lookup" value="true" />
|
||||
<camunda:property id="repeat" value="Shared" />
|
||||
<camunda:property id="repeat" value="shared" />
|
||||
<camunda:property id="placeholder" value="wxy0z or Smith" />
|
||||
</camunda:properties>
|
||||
<camunda:validation>
|
||||
|
@ -940,6 +942,9 @@ This step is internal to the system and do not require and user interaction</bpm
|
|||
<bpmndi:BPMNShape id="Activity_1xgrlzr_di" bpmnElement="Activity_SharedSpaceInfo">
|
||||
<dc:Bounds x="850" y="77" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_1jefdme_di" bpmnElement="Activity_ExclusiveSpace">
|
||||
<dc:Bounds x="690" y="77" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_0ysw6zo_di" bpmnElement="Activity_nonUVASpaces">
|
||||
<dc:Bounds x="1160" y="77" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
|
@ -988,9 +993,6 @@ This step is internal to the system and do not require and user interaction</bpm
|
|||
<bpmndi:BPMNShape id="Activity_1i34vt6_di" bpmnElement="Activity_14xt8is">
|
||||
<dc:Bounds x="3040" y="77" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_1jefdme_di" bpmnElement="Activity_ExclusiveSpace">
|
||||
<dc:Bounds x="690" y="77" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
</bpmndi:BPMNPlane>
|
||||
</bpmndi:BPMNDiagram>
|
||||
</bpmn:definitions>
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
"""empty message
|
||||
|
||||
Revision ID: 13424d5a6de8
|
||||
Revises: 5064b72284b7
|
||||
Create Date: 2020-06-02 18:17:29.990159
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '13424d5a6de8'
|
||||
down_revision = '5064b72284b7'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('ldap_model',
|
||||
sa.Column('uid', sa.String(), nullable=False),
|
||||
sa.Column('display_name', sa.String(), nullable=True),
|
||||
sa.Column('given_name', sa.String(), nullable=True),
|
||||
sa.Column('email_address', sa.String(), nullable=True),
|
||||
sa.Column('telephone_number', sa.String(), nullable=True),
|
||||
sa.Column('title', sa.String(), nullable=True),
|
||||
sa.Column('department', sa.String(), nullable=True),
|
||||
sa.Column('affiliation', sa.String(), nullable=True),
|
||||
sa.Column('sponsor_type', sa.String(), nullable=True),
|
||||
sa.Column('date_cached', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.PrimaryKeyConstraint('uid')
|
||||
)
|
||||
op.add_column('approval', sa.Column('date_approved', sa.DateTime(timezone=True), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('approval', 'date_approved')
|
||||
op.drop_table('ldap_model')
|
||||
# ### end Alembic commands ###
|
|
@ -1,8 +1,10 @@
|
|||
import json
|
||||
from tests.base_test import BaseTest
|
||||
|
||||
from crc import app, db, session
|
||||
from crc import session, db
|
||||
from crc.models.approval import ApprovalModel, ApprovalSchema, ApprovalStatus
|
||||
from crc.models.protocol_builder import ProtocolBuilderStatus
|
||||
from crc.models.study import StudyModel
|
||||
|
||||
|
||||
class TestApprovals(BaseTest):
|
||||
|
@ -11,11 +13,16 @@ class TestApprovals(BaseTest):
|
|||
self.load_example_data()
|
||||
self.study = self.create_study()
|
||||
self.workflow = self.create_workflow('random_fact')
|
||||
self.unrelated_study = StudyModel(title="second study",
|
||||
protocol_builder_status=ProtocolBuilderStatus.ACTIVE,
|
||||
user_uid="dhf8r", primary_investigator_id="dhf8r")
|
||||
self.unrelated_workflow = self.create_workflow('random_fact', study=self.unrelated_study)
|
||||
|
||||
# TODO: Move to base_test as a helper
|
||||
self.approval = ApprovalModel(
|
||||
study=self.study,
|
||||
workflow=self.workflow,
|
||||
approver_uid='arc93',
|
||||
approver_uid='lb3dp',
|
||||
status=ApprovalStatus.PENDING.value,
|
||||
version=1
|
||||
)
|
||||
|
@ -30,6 +37,16 @@ class TestApprovals(BaseTest):
|
|||
)
|
||||
session.add(self.approval_2)
|
||||
|
||||
# A third study, unrelated to the first.
|
||||
self.approval_3 = ApprovalModel(
|
||||
study=self.unrelated_study,
|
||||
workflow=self.unrelated_workflow,
|
||||
approver_uid='lb3dp',
|
||||
status=ApprovalStatus.PENDING.value,
|
||||
version=1
|
||||
)
|
||||
session.add(self.approval_3)
|
||||
|
||||
session.commit()
|
||||
|
||||
def test_list_approvals_per_approver(self):
|
||||
|
@ -40,16 +57,16 @@ class TestApprovals(BaseTest):
|
|||
|
||||
response = json.loads(rv.get_data(as_text=True))
|
||||
|
||||
# Stored approvals are 2
|
||||
# Stored approvals are 3
|
||||
approvals_count = ApprovalModel.query.count()
|
||||
self.assertEqual(approvals_count, 2)
|
||||
self.assertEqual(approvals_count, 3)
|
||||
|
||||
# but Dan's approvals should be only 1
|
||||
self.assertEqual(len(response), 1)
|
||||
|
||||
# Confirm approver UID matches returned payload
|
||||
approval = ApprovalSchema().load(response[0])
|
||||
self.assertEqual(approval.approver['uid'], approver_uid)
|
||||
approval = response[0]
|
||||
self.assertEqual(approval['approver']['uid'], approver_uid)
|
||||
|
||||
def test_list_approvals_per_admin(self):
|
||||
"""All approvals will be returned"""
|
||||
|
@ -58,7 +75,8 @@ class TestApprovals(BaseTest):
|
|||
|
||||
response = json.loads(rv.get_data(as_text=True))
|
||||
|
||||
# Returned approvals should match what's in the db
|
||||
# Returned approvals should match what's in the db, we should get one approval back
|
||||
# per study (2 studies), and that approval should have one related approval.
|
||||
approvals_count = ApprovalModel.query.count()
|
||||
response_count = len(response)
|
||||
self.assertEqual(2, response_count)
|
||||
|
@ -68,9 +86,10 @@ class TestApprovals(BaseTest):
|
|||
response = json.loads(rv.get_data(as_text=True))
|
||||
response_count = len(response)
|
||||
self.assertEqual(1, response_count)
|
||||
self.assertEqual(1, len(response[0]['related_approvals'])) # this approval has a related approval.
|
||||
|
||||
def test_update_approval_fails_if_not_the_approver(self):
|
||||
approval = session.query(ApprovalModel).filter_by(approver_uid='arc93').first()
|
||||
approval = session.query(ApprovalModel).filter_by(approver_uid='lb3dp').first()
|
||||
data = {'id': approval.id,
|
||||
"approver_uid": "dhf8r",
|
||||
'message': "Approved. I like the cut of your jib.",
|
||||
|
@ -125,3 +144,11 @@ class TestApprovals(BaseTest):
|
|||
# Updated record should now have the data sent to the endpoint
|
||||
self.assertEqual(approval.message, data['message'])
|
||||
self.assertEqual(approval.status, ApprovalStatus.DECLINED.value)
|
||||
|
||||
def test_csv_export(self):
|
||||
approvals = db.session.query(ApprovalModel).all()
|
||||
for app in approvals:
|
||||
app.status = ApprovalStatus.APPROVED.value
|
||||
db.session.commit()
|
||||
rv = self.app.get(f'/v1.0/approval/csv', headers=self.logged_in_headers())
|
||||
self.assert_success(rv)
|
|
@ -1,10 +1,7 @@
|
|||
import os
|
||||
from tests.base_test import BaseTest
|
||||
|
||||
from crc import app
|
||||
from crc.api.common import ApiError
|
||||
from crc.services.ldap_service import LdapService
|
||||
from tests.base_test import BaseTest
|
||||
from ldap3 import Server, Connection, ALL, MOCK_SYNC
|
||||
|
||||
|
||||
class TestLdapService(BaseTest):
|
||||
|
|
Loading…
Reference in New Issue