From 7194d7d3742a305dd3a0e8b9a198b07eac80dcec Mon Sep 17 00:00:00 2001 From: Dan Funk Date: Tue, 3 Mar 2020 13:50:22 -0500 Subject: [PATCH 1/2] Standardizing the script tasks that can be executed on the server, adding tons of error messages for when things go wrong. All scripts must exist in side of the crc/scripts directory. Adding a new script that script tasks can use to add in data about the study. Moving all the test workflow specifications out of the main load. fixing a pile of tests so they can find workflow specs that are now moved into the test directory. --- crc/api/common.py | 1 + crc/api/tools.py | 2 +- crc/scripts/LoadStudies.py | 24 ------- ...mpleteTemplate.py => complete_template.py} | 8 ++- .../{FactService.py => fact_service.py} | 9 ++- crc/scripts/script.py | 15 ++++ crc/scripts/study_info.py | 46 +++++++++++++ crc/services/protocol_builder.py | 27 ++++++-- crc/services/workflow_processor.py | 38 +++++++--- crc/static/bpmn/docx/Letter.docx | Bin 9345 -> 0 bytes crc/static/bpmn/docx/docx.bpmn | 65 ------------------ example_data.py | 47 +++---------- .../data}/decision_table/decision_table.bpmn | 0 .../decision_table/message_to_ginger.dmn | 0 tests/data/docx/docx.bpmn | 2 +- .../exclusive_gateway/exclusive_gateway.bpmn | 0 .../data}/parallel_tasks/parallel_tasks.bpmn | 0 .../data}/random_fact/random_fact.bpmn | 2 +- tests/data/study_details/study_details.bpmn | 39 +++++++++++ .../data}/two_forms/two_forms.bpmn | 0 tests/test_study_api.py | 1 - tests/test_tasks_api.py | 11 ++- tests/test_tools_api.py | 3 +- tests/test_workflow_processor.py | 28 ++++++-- tests/test_workflow_spec_api.py | 2 +- 25 files changed, 211 insertions(+), 159 deletions(-) delete mode 100644 crc/scripts/LoadStudies.py rename crc/scripts/{CompleteTemplate.py => complete_template.py} (95%) rename crc/scripts/{FactService.py => fact_service.py} (79%) create mode 100644 crc/scripts/script.py create mode 100644 crc/scripts/study_info.py delete mode 100644 crc/static/bpmn/docx/Letter.docx delete mode 100644 crc/static/bpmn/docx/docx.bpmn rename {crc/static/bpmn => tests/data}/decision_table/decision_table.bpmn (100%) rename {crc/static/bpmn => tests/data}/decision_table/message_to_ginger.dmn (100%) rename {crc/static/bpmn => tests/data}/exclusive_gateway/exclusive_gateway.bpmn (100%) rename {crc/static/bpmn => tests/data}/parallel_tasks/parallel_tasks.bpmn (100%) rename {crc/static/bpmn => tests/data}/random_fact/random_fact.bpmn (99%) create mode 100644 tests/data/study_details/study_details.bpmn rename {crc/static/bpmn => tests/data}/two_forms/two_forms.bpmn (100%) diff --git a/crc/api/common.py b/crc/api/common.py index 33bdd483..0283c198 100644 --- a/crc/api/common.py +++ b/crc/api/common.py @@ -6,6 +6,7 @@ class ApiError(Exception): self.status_code = status_code self.code = code self.message = message + Exception.__init__(self, self.message) class ApiErrorSchema(ma.Schema): diff --git a/crc/api/tools.py b/crc/api/tools.py index efa76387..e0e1e41b 100644 --- a/crc/api/tools.py +++ b/crc/api/tools.py @@ -10,7 +10,7 @@ from flask import send_file from jinja2 import Template, UndefinedError from crc.api.common import ApiError, ApiErrorSchema -from crc.scripts.CompleteTemplate import CompleteTemplate +from crc.scripts.complete_template import CompleteTemplate from crc.services.file_service import FileService diff --git a/crc/scripts/LoadStudies.py b/crc/scripts/LoadStudies.py deleted file mode 100644 index d24ac455..00000000 --- a/crc/scripts/LoadStudies.py +++ /dev/null @@ -1,24 +0,0 @@ -import requests - - -class LoadStudies: - """Just your basic class that can pull in data from a few api endpoints and do a basic task.""" - - def do_task(self, task_data): - print('*** LoadStudies > do_task ***') - print('task_data', task_data) - -class LoadStudy: - """Just your basic class that can pull in data from a few api endpoints and do a basic task.""" - - def do_task(self, task_data): - print('*** LoadStudies > do_task ***') - print('task_data', task_data) - irb_study = { - 'tbd': 0, - 'protocol_builder_available': True, - 'irb_review_type': 'Full Board', - 'irb_requires': True, - } - - return irb_study diff --git a/crc/scripts/CompleteTemplate.py b/crc/scripts/complete_template.py similarity index 95% rename from crc/scripts/CompleteTemplate.py rename to crc/scripts/complete_template.py index c56bb18f..098f36a1 100644 --- a/crc/scripts/CompleteTemplate.py +++ b/crc/scripts/complete_template.py @@ -9,17 +9,21 @@ from crc.models.workflow import WorkflowSpecModel from docxtpl import DocxTemplate import jinja2 +from crc.scripts.script import Script from crc.services.file_service import FileService from crc.services.workflow_processor import WorkflowProcessor -class CompleteTemplate(object): +class CompleteTemplate(Script): + + def get_description(self): + return """Completes a word template, using the data available in the current task. Heavy on the error messages, because there is so much that can go wrong here, and we want to provide as much feedback as possible. Some of this might move up to a higher level object or be passed into all tasks as we complete more work.""" - def do_task(self, task, *args, **kwargs): + def do_task(self, task, study_id, *args, **kwargs): """Entry point, mostly worried about wiring it all up.""" if len(args) != 1: raise ApiError(code="missing_argument", diff --git a/crc/scripts/FactService.py b/crc/scripts/fact_service.py similarity index 79% rename from crc/scripts/FactService.py rename to crc/scripts/fact_service.py index 57d9e19c..18a712eb 100644 --- a/crc/scripts/FactService.py +++ b/crc/scripts/fact_service.py @@ -1,8 +1,11 @@ import requests +from crc.scripts.script import Script -class FactService: - """Just your basic class that can pull in data from a few api endpoints and do a basic task.""" + +class FactService(Script): + def get_description(self): + return """Just your basic class that can pull in data from a few api endpoints and do a basic task.""" def get_cat(self): response = requests.get('https://cat-fact.herokuapp.com/facts/random') @@ -16,7 +19,7 @@ class FactService: response = requests.get('https://api.chucknorris.io/jokes/random') return response.json()['value'] - def do_task(self, task, **kwargs): + def do_task(self, task, study_id, **kwargs): print(task.data) if "type" not in task.data: diff --git a/crc/scripts/script.py b/crc/scripts/script.py new file mode 100644 index 00000000..b4fb9f1c --- /dev/null +++ b/crc/scripts/script.py @@ -0,0 +1,15 @@ +from crc.api.common import ApiError + + +class Script: + """ Provides an abstract class that defines how scripts should work, this + must be extended in all Script Tasks.""" + + def get_description(self): + raise ApiError("invalid_script", + "This script does not supply a description.") + + def do_task(self, task, study_id, **kwargs): + raise ApiError("invalid_script", + "This is an internal error. The script you are trying to execute " + + "does not properly implement the do_task function.") diff --git a/crc/scripts/study_info.py b/crc/scripts/study_info.py new file mode 100644 index 00000000..c6e3a768 --- /dev/null +++ b/crc/scripts/study_info.py @@ -0,0 +1,46 @@ +import requests + +from crc import session +from crc.api.common import ApiError +from crc.models.study import StudyModel, StudyModelSchema +from crc.scripts.script import Script +from crc.services.protocol_builder import ProtocolBuilderService + + +class StudyInfo(Script): + """Just your basic class that can pull in data from a few api endpoints and do a basic task.""" + pb = ProtocolBuilderService() + type_options = ['info', 'investigators', 'required_docs', 'details'] + + def get_description(self): + return """ + StudyInfo [TYPE] is one of 'info', 'investigators','required_docs', 'details' + Adds details about the current study to the Task Data. The type of information required should be + provided as an argument. Basic returns the basic information such as the title. Investigators provides + detailed information about each investigator in th study. Details provides a large number + of details about the study, as gathered within the protocol builder, and 'required_docs', + lists all the documents the Protocol Builder has determined will be required as a part of + this study. + """ + + def do_task(self, task, study_id, *args, **kwargs): + if len(args) != 1 or (args[0] not in StudyInfo.type_options): + raise ApiError(code="missing_argument", + message="The StudyInfo script requires a single argument which must be " + "one of %s" % ",".join(StudyInfo.type_options)) + cmd = args[0] + if cmd == 'info': + study = session.query(StudyModel).filter_by(id=study_id).first() + schema = StudyModelSchema() + details = {"study": {"info": schema.dump(study)}} + task.data.update(details) + if cmd == 'investigators': + details = {"study": {"investigators": self.pb.get_investigators(study_id)}} + task.data.update(details) + if cmd == 'required_docs': + details = {"study": {"required_docs": self.pb.get_required_docs(study_id)}} + task.data.update(details) + if cmd == 'details': + details = {"study": {"details": self.pb.get_study_details(study_id)}} + task.data.update(details) + diff --git a/crc/services/protocol_builder.py b/crc/services/protocol_builder.py index e8387272..982d47ef 100644 --- a/crc/services/protocol_builder.py +++ b/crc/services/protocol_builder.py @@ -4,6 +4,7 @@ from typing import List, Optional import requests from crc import app +from crc.api.common import ApiError from crc.models.protocol_builder import ProtocolBuilderStudy, ProtocolBuilderStudySchema, ProtocolBuilderInvestigator, \ ProtocolBuilderRequiredDocument, ProtocolBuilderStudyDetails, ProtocolBuilderInvestigatorSchema, \ ProtocolBuilderRequiredDocumentSchema, ProtocolBuilderStudyDetailsSchema @@ -17,36 +18,54 @@ class ProtocolBuilderService(object): @staticmethod def get_studies(user_id) -> Optional[List[ProtocolBuilderStudy]]: + 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) if response.ok and response.text: pb_studies = ProtocolBuilderStudySchema(many=True).loads(response.text) return pb_studies else: - return None + raise ApiError("protocol_builder_error", + "Received an invalid response from the protocol builder (status %s): %s" % + (response.status_code, response.text)) @staticmethod def get_investigators(study_id) -> Optional[List[ProtocolBuilderInvestigator]]: + ProtocolBuilderService.check_args(study_id) response = requests.get(ProtocolBuilderService.INVESTIGATOR_URL % study_id) if response.ok and response.text: pb_studies = ProtocolBuilderInvestigatorSchema(many=True).loads(response.text) return pb_studies else: - return None + raise ApiError("protocol_builder_error", + "Received an invalid response from the protocol builder (status %s): %s" % + (response.status_code, response.text)) @staticmethod def get_required_docs(study_id) -> Optional[List[ProtocolBuilderRequiredDocument]]: + ProtocolBuilderService.check_args(study_id) response = requests.get(ProtocolBuilderService.REQUIRED_DOCS_URL % study_id) if response.ok and response.text: pb_studies = ProtocolBuilderRequiredDocumentSchema(many=True).loads(response.text) return pb_studies else: - return None + raise ApiError("protocol_builder_error", + "Received an invalid response from the protocol builder (status %s): %s" % + (response.status_code, response.text)) @staticmethod def get_study_details(study_id) -> Optional[ProtocolBuilderStudyDetails]: + ProtocolBuilderService.check_args(study_id) response = requests.get(ProtocolBuilderService.STUDY_DETAILS_URL % study_id) if response.ok and response.text: pb_study_details = ProtocolBuilderStudyDetailsSchema().loads(response.text) return pb_study_details else: - return None + 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)) diff --git a/crc/services/workflow_processor.py b/crc/services/workflow_processor.py index 54de4b92..761e12a9 100644 --- a/crc/services/workflow_processor.py +++ b/crc/services/workflow_processor.py @@ -1,3 +1,4 @@ +import re import xml.etree.ElementTree as ElementTree from SpiffWorkflow import Task as SpiffTask, Workflow @@ -13,6 +14,7 @@ from crc import session from crc.api.common import ApiError from crc.models.file import FileDataModel, FileModel, FileType from crc.models.workflow import WorkflowStatus, WorkflowModel +from crc.scripts.script import Script class CustomBpmnScriptEngine(BpmnScriptEngine): @@ -20,7 +22,7 @@ class CustomBpmnScriptEngine(BpmnScriptEngine): Rather than execute arbitrary code, this assumes the script references a fully qualified python class such as myapp.RandomFact. """ - def execute(self, task, script, **kwargs): + def execute(self, task:SpiffTask, script, **kwargs): """ Assume that the script read in from the BPMN file is a fully qualified python class. Instantiate that class, pass in any data available to the current task so that it might act on it. @@ -29,11 +31,31 @@ class CustomBpmnScriptEngine(BpmnScriptEngine): This allows us to reference custom code from the BPMN diagram. """ commands = script.split(" ") - module_name = "crc." + commands[0] - class_name = module_name.split(".")[-1] - mod = __import__(module_name, fromlist=[class_name]) - klass = getattr(mod, class_name) - klass().do_task(task, *commands[1:]) + path_and_command = commands[0].rsplit(".", 1) + if len(path_and_command) == 1: + module_name = "crc.scripts." + self.camel_to_snake(path_and_command[0]) + class_name = path_and_command[0] + else: + module_name = "crc.scripts." + path_and_command[0] + "." + self.camel_to_snake(path_and_command[1]) + class_name = path_and_command[1] + try: + mod = __import__(module_name, fromlist=[class_name]) + klass = getattr(mod, class_name) + study_id = task.workflow.data[WorkflowProcessor.STUDY_ID_KEY] + if not isinstance(klass(), Script): + raise ApiError("invalid_script", + "This is an internal error. The script '%s:%s' you called " + "does not properly implement the CRC Script class." % + (module_name, class_name)) + klass().do_task(task, study_id, *commands[1:]) + except ModuleNotFoundError as mnfe: + raise ApiError("invalid_script", + "Unable to locate Script: '%s:%s'" % (module_name, class_name), 400) + + @staticmethod + def camel_to_snake(camel): + camel = camel.strip() + return re.sub(r'(? z9pB?h94*;1MGTByPy(DN&*4MT501u1;kj%oAC|?DT2!L)`rEwEPDmykMt|igWkpAe zcbD0SgV?{eu-keq2(!H~o@8s$7DfU&7`nYR_hls zrK~MU1T8gOzV+wK%jKBwQ3RQnAd-syp2k#?w2(6`Iv3_+eA1khsxA|A&LhtvW zYg_JA6fXnod*6IMl+hD)ZkaLhD(_QWKAncfRQe)4Y0n26ZYRItwtoAshHKFy1>JMR zKXX-p@CZBOkW_Pz^A(1IfKd5A5y5!;hq;rPii?x8E1QY4Gpm<_eRkrAV>dZQ$T1iR zd6P&YX-J7mVd}*>&im9o-gYS*Ufxtx1lg1}@XOeZo5cmRTEALmKk#1b0Rjb)6%*$$ z;ov96l2`}EABLjJZ>Xc?Ua<&T%UF%o` zeDQtKRl>qr?w9GkT=vF;72QU4#J{I}vK?0!g&0h25-5HE)T_U_UXuY|3}Cl~EqihX zuKuj9Fi0|R@WJ2`B7^C??BJ58U5uE0TMfb1iCH(GYq%9X*f-m4bJ@)7(d)o#JNq5G zL!4$vXN(pdesXs=Xp$qYa|mvY=J?e~B{I>u>F93#MtmiT8qFQ1YMK}Loy_vLcNZhT z(3s67`$ZlXc9sEJ*%ebs??DpkD3g^n_$QF>_-nH1AR!=N{}p7+=OCLoxmY}*?4Y*d z6vU44dc{cm1`U?Gg*#xd;fgz%#%(iEn-Br~!rZv)g+ zO2q@EWs>Rh{S9Ht7nh*hT{bna;^B^7v8}C&@bMEYG{nxUI-OrC@AR*^&&3nTFICDz zFnm;FHwiQ|i;gjbHhQxfk_vY}q`Zdu9W>%ze|F0t7QS!cL|E#0#^c#^;#8NYB0>u8 zBRy(EEFRTgl`$>}WSh9%xwhb;lOies5tNt+8PH&-Da?7@&GEFO?g;A54j+}{$gQ{q zELAQY?GwY~ND~EAtD2(H-KhP#73pPr3dn3HT!7kRK%)LNAjFRW@pN)AXZ!!fz}mvZ z+`@(9DJs9~LTvm9(r5O^1Q3y$ZC&~n9Kn4k6=GNWXKDH;}?%+q6w zxcD&F!vsBQ(5pZHy{+-aJMBIjw3*X3VX+m5d1Mw!d|-y6Y=yc08v8a$T!h@Hxm1}S zxwc4hyw-+`bgcM5=6FbJ%d**>rC6isM{!U@ux&tWwb(<=JvX5o9ouo5{x8+QzO*AK z998u#V>!Z8%uGf;RGqNzwz`*aPxIv+>qjnmoDIg~od54EC;8`m9~0i)!NSq)uL)Ns zE82FmV}xGvhrR`~2--*>q}4Kn+DH3BXp%js=wVH~<8F+PP%jf>H?Y2z&bSnoF8p!A z6?;IO{UQcW#hjmgYKS}d2*z*9Nm-Cjqx=V{jNJFAWB7gDov(&s%I23@zSHZ}U`ysb z=Lw>ha!3>Ii}L3*f*bonD=zrHV(#06aj_7d_DII(b8G%>iVJ8+MzI2lrMr+pz5?+( z!G&(5NddZ6UGP_CcLFpWGIM#i-D52}YffrE^vvkD#(WYVveK&Gr{z-hVLBESVX(=0 zLPUOM?bmZIXO@~RjuFd$nbD;=+Pl)IlX`a)tcm{w9GeEV=J6veZ=nATxTnnhy--{& z+}vy&tz4fVvq)%n>}E#^xfBV_Z%-r56J)ZC%I7XC#1E3z{}u zwN6QwKoF6|!}-VpJv@6nFAe!;y|?a?xr_ib}t&E?&ptv^o~oH`#ii@`R$Dd zO|)ei(>-eK9<>?oa911GG3MN2+1r=uDZMOG-r@YN7m5&2!^FNi#lcQIC(a3Nv7MY&>Jv3TpP6CIuqOGooK34*C$I7bmBk3TGrXkpzAL{?bSj| zs?PiZ`|H{*q$NbaMX)0o_#%B45x#a2+C|`}A!nY>jT(Jt7he=35Jz>#5h6=cgHZYr zl@L8qLs7(qHU=KkN(3Q43_Wtvc_|H>QsL0;ZOa>C`W_LhAk@vkF+ezK_$)kE%ATR; zh^FiWB*33k2GH|jt+co=I-v(zp(cyS%AV<`pf=SI9}xzR-ex?wV2g_3xX#6hWG)8CL5%r_k}G0c!+~3^i0&hixLb;?m^P^w*^CdE1=TrEh;)5k)9BJAiCUkg8<@QqbJ&5AfD75ufNS8q>GBuY;`DrV6<|Zba%%_r8%iOq{WnT z)WW<%|726vdi)k0Zn}l96LR8KUKpO6L+3nQ8Je2BDZji4n>duN*^*2MLh`;S7f2>S z#1c`3Yx}_fz)hSg7tsZ@`9M`7%}^q($?rX_n_cDWVOYb{h@58Xi(ZpaRFk0@dY*_k z9kdmf)uyIo`1q$I&JrIxSEWl5xDSl|@6OL0Bh|GGorhj=m*BD}ms*Hq3f-(}j8D@C z3V#x24)wi4ycGU^H1I)l=)?ISNFwg4bCNmu8fr*r(AOP28nlbYjQ?}X31^uz$Hz=q zY{m;hzrC8%c8P*9jR02%@9m8OCzW3Nv1moBVboqm?1wtmRZLZG07)nKJ!iZLUIZSC z0~l8hE1w6iN7c?-yrQSIMt;#lZANEcF(gKZ`^_}>D{r$=SGK~s%5q_xU6MiX%)M&Z zPV#*z5-F09abHbQG)*E)&x-4`sag8%VJ^3Z{MDR+t#a*zYQa={ZSH^){W{CNWz+N5 zSCb_v6G3z0F>49ROPNFl4}KPSa3rhV`9k{_{mQUV61wScKB9Y?yTlijDb&VT+Db7c z#(R1-d^*AvI%~_$@%B6w!{f{%W{mm(kYO+yv6_*`@G_w&-KdX>6HY~b6Eg^okPBlO`E#qDYP{qY;)^V$nBoC(n?#O!`D%k14nXv@4{cwtvASz9@iDd7 zaRU#L{UG!yEobcNaRonfH>ku=O*9kEb*Bq;ytoUf6_ zI#>etTYIS^N&dpyoe}%zK1(_3KZYS@&pxSHK`tCLW{swlzzu~*x3%&#eydpH`4tO$ zXInA}(q_8qLs`-P)M(2z66*uIlJ$K_>Rym|=|~>-c=Ut``RZ5dbgCt8ZXiHunWs)^rjtXa%5I5ZfV=31k4MFncf&QSVh=X zo}hZV>m)k-G&NmWpCH;Y2XKmUX$0ZMdGrgzPOq>yG7(XqtFy0#GBW7?oGMG7KzZuz zH%DikIetOE{4Ng5`;9VA)%qVU)=N5n{Rky2bz2>-&iVE0Zg;p^^mhHWAMPsblLVtSN?RnYw*XU=N3q4a!aq64TBC_LmyS846AGW$bY%g-`qI z@VKF#5*%7n5N(9gxT|>E72Tm3 zyHpA1!oF4fXe0&DG`2LA)9H(UyC_H&J_Iy%7MVhu zA!w?mdP!S{<~rLrOI=18THs5&!Sba!ZgInR2}34Q)ru%w;6kDqo+0WhueLOeS3O{Y zzX!4(*bR=ya&MUL00xg|k?$sdU{8hlKJAlLgGmee`kkp=px_h~D;U%f%hm#tDEGS_ zNq$frUWTB1Yirq|%GN#7xXp?$0={iMqb2KKfI(UVyS`q@TItdycDhL^7v`^OgoI~@wV+1zFx;Yk< zZVei>d{anyXnl&OhiH+pYr~cjTWUa`ZS?K6i$te=n*B#za(D6AlrEc4o~pKVb~^YR zwT$(QqUL`2S{^6B5_wh!pR?9pv$ZVFmx$aOVJmjjVNn9glH%US7RteFcflX)-;;Ti zp3+1>H-ndD&!&YPUC!k;-k`PAVn=~a=U@j@LPH>pc#>>W(#0IHkXGUXtz6a1AFVy2 zyE(OJ0jDiV4JcD&?#^QLjs2`{9hpMMHw(y5&r*t(9==Aeui2IWN>i=9r(vqK zIgN?O#ca*@4+yho_F&&x`cgYwf+YY33t%QJj8LAIh3ZZ9-7#erSh52F`sTjCXfWDX zUlH1zg8G6w@rd0%Dj|ExQJ19o820dwIh)k4~> z?O8_eA5COiy)IW=O$(f0vtHEjviA@oJt2({VZ8K)iS&A}nPSw;2a~Or0b(fk-^ZDH zg>@V)BxpP8$PBtTT^l{NpINoL^wM64rm?lVOt5b_k->Q(0x_5A+)h~QCq>IX48K6h zQ_NS3zG* z;n<4? zr=?vZ?S#$?W{a7J-{%%c8TUH#Cd&OJ$|4r2z_CS z(&TuMk|IZ&o?>D;F%#D&&{`hp__eps{@nCSr6WS0&1zu1+dJ3m(UQW{1AMlz zcjVYLadtei9o?$+x%n3zht+#4#*kWU*3E8io7pErBB(Mm*4wf6YU|q zY3*3PBsY2A04pwhvoG(cU+CbHdmo82(zMH{RWi?M<_e3L@ZtlhfMWE(2d80-HkL2PtCZNkir6Sd9Y30pna+} zO?_@qWCY=towvwGR+_8GB_1v4XL|ycMkQ=NWGwF+LnqSWU|C@_BnvzTF=jsN)av?W z?5#vrl2M6bixuA^;s$TcaIy_!=%Ra7;*@ZOF#k7l2^q+L+u&J0iR)542-xxESg9Hq=V*PPiSs(X+?J%E12( zn}{wod}wI`b?RE8e^*g%tLj|yTc7qXt5?%wy{wFfJPSvIw!VNBFbf~zEyb;3JzX$dun6}MMr z#I0m1XF`jLvmCeyP6m_HG#_6H#~x1<#a3t zK>jNnhaBBvLpLl*Hk;&UtI#?C!k3Gh(L^vZ@1dm$N|NX+gHpH(u9~PZnvydx$29wN zRdQgIVWf0E&43^i>r_%yxnh%lEm{1g({`aUwVlYgo<`xqXBEIYs^&5MO@|Li40-QPbUo+s4Wx$d|NoCuQ)d zP4sjd4*lqAm464)UQ#uOEr3g;hvJU_j{gYIDA&f)eg_lkn=d7AY=#wrC`%mC{Q>z> z*1QOBRVW;muz}IxZH=WIR2eOQND!5UCN;?*2N>!)>H>U+rb3llPbKS-VIeM1mcwu^ zthPtMl)P$IK{LP@T*#5$)5|FJ;a3dD3VFcx0Aqoorhfvj6a+Ijjm(5BvqX$Gk8+>6 zb67p{e!)AVnk>RVDba83LL!l0M|*9z^%GcJ1h|CoG}R_RcaD76INm_2R{o&F_jjm- z(V@8IB$z6z1a9y)Bd3AAm21>Vuk_r!Ihv)G%jC?xRHuQ?A%T)*zQDxB4{FHF7qStr zY$_%LFkrL=N4*c?x=CPzm(ahT(?PhRSal?BYVheqET8QxE6&YyCRX^Y5`J$8&5xd& zH_FPwG}*1~J`0@r(ymvP?)!3C==hdHN=RrL8kmQ2 z06ze7>OzG(zfSL>C)nUd$i-OLwojOCtWeh*slO;!oUNpj1g#f;&OQ&p8!6U}F;$rq z|2Ua$B$#^TT|J|i3F$w95Eigln5P&2p^YBg_A*=fJ>}aK4Q~gyL2+>5D_@^E61(I2 zo~_vp!FcP{*U70t@cq)T(>uq)=Z6!SI>VsBR+o4L71O8*FX*y zaK8bfpv3Uzu*Ol~#yC0gA#u_pvA^&;IE$cimaf$rNbmg~SI%2~Jm-C1fWLt%;sx&@6yX;kDZ=@a(LA}{6GCS#Dv01!H$Y&Ui;Ewx!h z(Kgxhz0FpEU4yH;d)psJ8S)W-qtQ_$&NBRX=5qg6>g9>j|IJxFqcumgjFca_7yp%# znmeUrfH*T{9tRx2R2}vJx+!i_=PK8}#O89>vmv?eQ;5p#$M8PSi>2e&o)_6Zp!zhZ z%D3+N($dsnrOp)-@thF01g*Hs-KjuI= zxfW@NYk_5UO(5v`y}~Od2DsKEE>%GXCNmz3QE}9FvLKr8wjt?Txw3q#F=Sg@iA=zj z?1bvb#dqB-1-|h76K^46JA5!+dZ&UfF5yBVxaR@Wxis+hMhoX!o}HT|!ARnI)f2)fdGKXm%;_f8I_B(kA|Hi2&J z8LvjX+amMC7%p1Jzj{X}tH_b3v!2|;+Ba5A8+6NVhWUEOCbgTjU@R^I*uoIRr5lW0 zx&oTptBV8G=FzuD^|oxVd&WEBF@&Sx*2OP~IIYn#Fj!i0TZjY%uBz>rx{)SvE6oJ$ zZ#>BJ8%3-u?Dy>}&O8->I56ZWew2K=!c94I#uMtUmK9KU1-+x?^V5^bO34(Xu^{7v zxE|(To6UE~Pvpf9T8)L>NB*A^@!yKZUuk0L#xH|_E?JV;6f-kkZEDdW=Z)bTHDbsNU*ET4n)6dhD$ zS^J`62on++VPw|56kffeL%Bgh&Vc34j|oqy+^ol=?U|8tAr4ky&eJfG+opSUrhPA6 z&QO)DUSFReSflS1&oypaTAt2Lr;zc*x@kR&b1`d(zve=Huo}=1e5UYIG=Yu~n8Kk{ zT|&;L*l~J+>$;}E#8xOARJkNM>8I3~(WQ6T63OrPHCTg_nXu`1&UaOdJ~1x?hW!7EB;h3W`Y$@%W^0RN#uAnWX<3RJG{u>c-#6 z2Iu`)*&ty4Eo@l-t!|c1j&2$zruM(ZwDzPi$It92eaF60^p}aBO)?mfGPn=x@@;D1 zor8f?G$7n?yye&V0o)G4HHaYK-S+KVu2A-I7A^}M?*=kInT1;3OdaoQ^?|YJ;rl`y zlCJ8mq<+9r;-;U+%fla0e&&hu!rKNE`vjT#4BvPJRxx>Y)T;_s$%~z4OXEJbAihiF ze=k^0t9y}0LcpfX`5lX}jIr=E{pYv*c$vfiJk$9AyUCiY>|$(#qpqTS?LLZVSdX<} zlB*7}84Ln+9+p{vWx59V1)tgFw)M{Nt)y8&)z~RsP~gNGMNwRh(fHzm3jT4JMxkV`Sabus4BP;s^@C4E@t z#$B~SFTLfkt0nM>kom7uJl4iL<$hj$y2-blZp9;2m(9EnY6VI5pp;|G)1?kaxJ(K|uhvNcMgg}Ie| zzl}=2#bqXL_Ks8OLqL30aoVY}7Ib^2!V$wufdDj{2qN=WZIq*Okb_yxC!cXrLOKMk zg-6!plS(?sIf0)m9B8Z{@4pt|`PR9S_YSf4znr+yQy~B8nZ)jQ6myOfTKm=$XH+9~ zBY?;V3CO8qQUsCsDg_3gnxt>5uj}+gvHHV}*jgjVl)w>MAz+y5g2;85!l~a~u`V3r z1miaOlCSK04Zl~s&uo{p_aZ>2(HRjs`-om-^*rSDh%)cj!5B(U&zm-3C#onp;XR4K zgc$kIe%i5$s>?PvNTQVonvbp~qYFSbp?OIQg(iefe35{7p^LA-)`2`n#tCE!i#T<+ z-3tD{6GY4m?b_-wP6?0aW)&bL6b{7SPdPkS`=6e2_@_O4=HXAr=L+^yN&MTe9xwIh zqXB<%lp5Y_n+?1)ypSM_HRRYT#Nsr(Ed5lb2aZt?EKrV{yETd zBH_2z`{zi{<*k3th~p9V|Dgr`>HeHJf6~DIHe2}ry8oq!{ptLi6nWay{%y*SG5vk* zKbiJFhj_kW{^#x2JpTMW#B&J#bbr3X{O29a;Qz<{&pq~^Lp<-k|6EX;1pgW0*~auw z@8?b6pWY~sas7S&&-(SB-p?!9Q=|CXx*vI_|9C%Z8-Myg&# - - - - SequenceFlow_0637d8i - - - - - - - - - - - - - SequenceFlow_0637d8i - SequenceFlow_1i7hk1a - - - - - - - - - SequenceFlow_1i7hk1a - SequenceFlow_11c35oq - scripts.CompleteTemplate Letter.docx - - - SequenceFlow_11c35oq - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/example_data.py b/example_data.py index 59966a86..0fc72d86 100644 --- a/example_data.py +++ b/example_data.py @@ -55,15 +55,15 @@ class ExampleDataLoader: display_name="CR Connect2 - Training Session - Core Info", description='Part of Milestone 3 Deliverable') workflow_specifications += \ - self.create_spec(id="crc2_training_session_data_security_plan", - name="crc2_training_session_data_security_plan", - display_name="CR Connect2 - Training Session - Data Security Plan", - description='Part of Milestone 3 Deliverable') + self.create_spec(id="crc2_training_session_data_security_plan", + name="crc2_training_session_data_security_plan", + display_name="CR Connect2 - Training Session - Data Security Plan", + description='Part of Milestone 3 Deliverable') workflow_specifications += \ - self.create_spec(id="sponsor_funding_source", - name="sponsor_funding_source", - display_name="Sponsor and/or Funding Source ", - description='TBD') + self.create_spec(id="sponsor_funding_source", + name="sponsor_funding_source", + display_name="Sponsor and/or Funding Source ", + description='TBD') # workflow_specifications += \ # self.create_spec(id="m2_demo", # name="m2_demo", @@ -74,37 +74,6 @@ class ExampleDataLoader: # name="crc_study_workflow", # display_name="CR Connect Study Workflow", # description='Draft workflow for CR Connect studies.') - workflow_specifications += \ - self.create_spec(id="random_fact", - name="random_fact", - display_name="Random Fact Generator", - description='Displays a random fact about a topic of your choosing.') - workflow_specifications += \ - self.create_spec(id="two_forms", - name="two_forms", - display_name="Two dump questions on two separate tasks", - description='the name says it all') - workflow_specifications += \ - self.create_spec(id="decision_table", - name="decision_table", - display_name="Form with Decision Table", - description='the name says it all') - workflow_specifications += \ - self.create_spec(id="parallel_tasks", - name="parallel_tasks", - display_name="Parallel tasks", - description='Four tasks that can happen simultaneously') - workflow_specifications += \ - self.create_spec(id="exclusive_gateway", - name="exclusive_gateway", - display_name="Exclusive Gateway Example", - description='How to take different paths based on input.') - workflow_specifications += \ - self.create_spec(id="docx", - name="docx", - display_name="Form with document generation", - description='the name says it all') - all_data = users + studies + workflow_specifications return all_data diff --git a/crc/static/bpmn/decision_table/decision_table.bpmn b/tests/data/decision_table/decision_table.bpmn similarity index 100% rename from crc/static/bpmn/decision_table/decision_table.bpmn rename to tests/data/decision_table/decision_table.bpmn diff --git a/crc/static/bpmn/decision_table/message_to_ginger.dmn b/tests/data/decision_table/message_to_ginger.dmn similarity index 100% rename from crc/static/bpmn/decision_table/message_to_ginger.dmn rename to tests/data/decision_table/message_to_ginger.dmn diff --git a/tests/data/docx/docx.bpmn b/tests/data/docx/docx.bpmn index 38cd1f97..164ac182 100644 --- a/tests/data/docx/docx.bpmn +++ b/tests/data/docx/docx.bpmn @@ -27,7 +27,7 @@ SequenceFlow_1i7hk1a SequenceFlow_11c35oq - scripts.CompleteTemplate Letter.docx + CompleteTemplate Letter.docx SequenceFlow_11c35oq diff --git a/crc/static/bpmn/exclusive_gateway/exclusive_gateway.bpmn b/tests/data/exclusive_gateway/exclusive_gateway.bpmn similarity index 100% rename from crc/static/bpmn/exclusive_gateway/exclusive_gateway.bpmn rename to tests/data/exclusive_gateway/exclusive_gateway.bpmn diff --git a/crc/static/bpmn/parallel_tasks/parallel_tasks.bpmn b/tests/data/parallel_tasks/parallel_tasks.bpmn similarity index 100% rename from crc/static/bpmn/parallel_tasks/parallel_tasks.bpmn rename to tests/data/parallel_tasks/parallel_tasks.bpmn diff --git a/crc/static/bpmn/random_fact/random_fact.bpmn b/tests/data/random_fact/random_fact.bpmn similarity index 99% rename from crc/static/bpmn/random_fact/random_fact.bpmn rename to tests/data/random_fact/random_fact.bpmn index 11cddb7c..81f355c3 100644 --- a/crc/static/bpmn/random_fact/random_fact.bpmn +++ b/tests/data/random_fact/random_fact.bpmn @@ -130,7 +130,7 @@ Autoconverted link https://github.com/nodeca/pica (enable linkify to see) SequenceFlow_0641sh6 SequenceFlow_0t29gjo - scripts.FactService + FactService # Great Job! diff --git a/tests/data/study_details/study_details.bpmn b/tests/data/study_details/study_details.bpmn new file mode 100644 index 00000000..b9aead94 --- /dev/null +++ b/tests/data/study_details/study_details.bpmn @@ -0,0 +1,39 @@ + + + + + SequenceFlow_1nfe5m9 + + + + SequenceFlow_1nfe5m9 + SequenceFlow_1bqiin0 + StudyInfo info + + + + SequenceFlow_1bqiin0 + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/crc/static/bpmn/two_forms/two_forms.bpmn b/tests/data/two_forms/two_forms.bpmn similarity index 100% rename from crc/static/bpmn/two_forms/two_forms.bpmn rename to tests/data/two_forms/two_forms.bpmn diff --git a/tests/test_study_api.py b/tests/test_study_api.py index 4edeea8f..a0a23633 100644 --- a/tests/test_study_api.py +++ b/tests/test_study_api.py @@ -7,7 +7,6 @@ from crc.models.study import StudyModel, StudyModelSchema from crc.models.protocol_builder import ProtocolBuilderStatus from crc.models.workflow import WorkflowSpecModel, WorkflowSpecModelSchema, WorkflowModel, WorkflowStatus, \ WorkflowApiSchema -from services.protocol_builder import ProtocolBuilderService from tests.base_test import BaseTest diff --git a/tests/test_tasks_api.py b/tests/test_tasks_api.py index c89f791c..1e0d40d3 100644 --- a/tests/test_tasks_api.py +++ b/tests/test_tasks_api.py @@ -1,10 +1,12 @@ import json +from unittest.mock import patch from crc import session from crc.models.file import FileModelSchema from crc.models.study import StudyModel from crc.models.workflow import WorkflowSpecModel, WorkflowSpecModelSchema, WorkflowModel, \ WorkflowApiSchema, WorkflowStatus, Task +from crc.services.workflow_processor import WorkflowProcessor from tests.base_test import BaseTest @@ -12,7 +14,8 @@ class TestTasksApi(BaseTest): def create_workflow(self, workflow_name): study = session.query(StudyModel).first() - spec = session.query(WorkflowSpecModel).filter_by(id=workflow_name).first() + spec = self.load_test_spec(workflow_name) + processor = WorkflowProcessor.create(study.id, spec.id) rv = self.app.post( '/v1.0/study/%i/workflows' % study.id, headers=self.logged_in_headers(), @@ -186,8 +189,6 @@ class TestTasksApi(BaseTest): task.process_documentation(docs) self.assertEqual("This test works", task.documentation) - - def test_get_documentation_populated_in_end(self): self.load_example_data() workflow = self.create_workflow('random_fact') @@ -201,3 +202,7 @@ class TestTasksApi(BaseTest): self.assertEqual("EndEvent_0u1cgrf", workflow_api.next_task['name']) self.assertIsNotNone(workflow_api.next_task['documentation']) self.assertTrue("norris" in workflow_api.next_task['documentation']) + + + + # response = ProtocolBuilderService.get_study_details(self.test_study_id) diff --git a/tests/test_tools_api.py b/tests/test_tools_api.py index 9175e4c7..eb004144 100644 --- a/tests/test_tools_api.py +++ b/tests/test_tools_api.py @@ -22,7 +22,8 @@ class TestStudyApi(BaseTest): def test_render_docx(self): filepath = os.path.join(app.root_path, '..', 'tests', 'data', 'table.docx') - template_data = {"hippa": [{"option": "Name", "selected": True, "stored": ["Record at UVA", "Stored Long Term"]}, + template_data = {"hippa": [ + {"option": "Name", "selected": True, "stored": ["Record at UVA", "Stored Long Term"]}, {"option": "Address", "selected": False}, {"option": "Phone", "selected": True, "stored": ["Send or Transmit outside of UVA"]}]} with open(filepath, 'rb') as f: diff --git a/tests/test_workflow_processor.py b/tests/test_workflow_processor.py index d700894a..52b4ddaf 100644 --- a/tests/test_workflow_processor.py +++ b/tests/test_workflow_processor.py @@ -1,5 +1,6 @@ import string import random +from unittest.mock import patch from SpiffWorkflow.bpmn.specs.EndEvent import EndEvent @@ -29,7 +30,7 @@ class TestWorkflowProcessor(BaseTest): def test_create_and_complete_workflow(self): self.load_example_data() - workflow_spec_model = session.query(WorkflowSpecModel).filter_by(id="random_fact").first() + workflow_spec_model = self.load_test_spec("random_fact") study = session.query(StudyModel).first() processor = WorkflowProcessor.create(study.id, workflow_spec_model.id) self.assertEqual(study.id, processor.bpmn_workflow.data[WorkflowProcessor.STUDY_ID_KEY]) @@ -54,9 +55,9 @@ class TestWorkflowProcessor(BaseTest): def test_workflow_with_dmn(self): self.load_example_data() study = session.query(StudyModel).first() + workflow_spec_model = self.load_test_spec("decision_table") files = session.query(FileModel).filter_by(workflow_spec_id='decision_table').all() self.assertEqual(2, len(files)) - workflow_spec_model = session.query(WorkflowSpecModel).filter_by(id="decision_table").first() processor = WorkflowProcessor.create(study.id, workflow_spec_model.id) self.assertEqual(WorkflowStatus.user_input_required, processor.get_status()) next_user_tasks = processor.next_user_tasks() @@ -79,7 +80,7 @@ class TestWorkflowProcessor(BaseTest): def test_workflow_with_parallel_forms(self): self.load_example_data() - workflow_spec_model = session.query(WorkflowSpecModel).filter_by(id="parallel_tasks").first() + workflow_spec_model = self.load_test_spec("parallel_tasks") study = session.query(StudyModel).first() processor = WorkflowProcessor.create(study.id, workflow_spec_model.id) self.assertEqual(WorkflowStatus.user_input_required, processor.get_status()) @@ -121,7 +122,7 @@ class TestWorkflowProcessor(BaseTest): def test_workflow_processor_knows_the_text_task_even_when_parallel(self): self.load_example_data() study = session.query(StudyModel).first() - workflow_spec_model = session.query(WorkflowSpecModel).filter_by(id="parallel_tasks").first() + workflow_spec_model = self.load_test_spec("parallel_tasks") processor = WorkflowProcessor.create(study.id, workflow_spec_model.id) self.assertEqual(WorkflowStatus.user_input_required, processor.get_status()) next_user_tasks = processor.next_user_tasks() @@ -141,7 +142,7 @@ class TestWorkflowProcessor(BaseTest): def test_workflow_processor_returns_next_task_as_end_task_if_complete(self): self.load_example_data() - workflow_spec_model = session.query(WorkflowSpecModel).filter_by(id="random_fact").first() + workflow_spec_model = self.load_test_spec("random_fact") study = session.query(StudyModel).first() processor = WorkflowProcessor.create(study.id, workflow_spec_model.id) processor.do_engine_steps() @@ -196,3 +197,20 @@ class TestWorkflowProcessor(BaseTest): self.assertIsNotNone(file_data.data) self.assertTrue(len(file_data.data) > 0) # Not going any farther here, assuming this is tested in libraries correctly. + + def test_load_study_information(self): + """ Test a workflow that includes requests to pull in Study Details.""" + + self.load_example_data() + study = session.query(StudyModel).first() + workflow_spec_model = self.load_test_spec("study_details") + processor = WorkflowProcessor.create(study.id, workflow_spec_model.id) + processor.do_engine_steps() + task = processor.bpmn_workflow.last_task + self.assertIsNotNone(task.data) + self.assertIn("study", task.data) + self.assertIn("info", task.data["study"]) + self.assertIn("title", task.data["study"]["info"]) + self.assertIn("last_updated", task.data["study"]["info"]) + self.assertIn("sponsor", task.data["study"]["info"]) + diff --git a/tests/test_workflow_spec_api.py b/tests/test_workflow_spec_api.py index a8dd4be1..85c539bf 100644 --- a/tests/test_workflow_spec_api.py +++ b/tests/test_workflow_spec_api.py @@ -47,7 +47,7 @@ class TestWorkflowSpec(BaseTest): def test_delete_workflow_specification(self): self.load_example_data() spec_id = 'random_fact' - + self.load_test_spec(spec_id) num_specs_before = session.query(WorkflowSpecModel).filter_by(id=spec_id).count() self.assertEqual(num_specs_before, 1) From 94f828dfd6d64e81d65ca93d9aab01b0818a4ff2 Mon Sep 17 00:00:00 2001 From: Dan Funk Date: Tue, 3 Mar 2020 15:30:42 -0500 Subject: [PATCH 2/2] Adding a simple endpoint that describes what scripts are currently available, along with a brief description. --- crc/api.yml | 25 ++++++++++++++++++++++++- crc/api/tools.py | 24 +++++++++++++++++------- crc/scripts/IRBDocAPI.py | 9 --------- crc/scripts/complete_template.py | 7 ++----- crc/scripts/fact_service.py | 3 ++- crc/scripts/script.py | 26 ++++++++++++++++++++++++++ crc/scripts/study_info.py | 3 +-- tests/test_tools_api.py | 8 ++++++++ 8 files changed, 80 insertions(+), 25 deletions(-) delete mode 100644 crc/scripts/IRBDocAPI.py diff --git a/crc/api.yml b/crc/api.yml index a5dae070..783fde2d 100644 --- a/crc/api.yml +++ b/crc/api.yml @@ -680,7 +680,21 @@ paths: type: string format: binary example: '' - + /list_scripts: + get: + operationId: crc.api.tools.list_scripts + summary: Returns an list of scripts, along with their descriptions + tags: + - Configurator Tools + responses: + '201': + description: The list of scripts + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Script" components: schemas: User: @@ -969,3 +983,12 @@ components: message: type: string example: "You do not have permission to view the requested study." + Script: + properties: + name: + type: string + format: string + example: "random_fact" + description: + type: string + example: "Returns a random fact about a topic. Provide an argument of either 'cat', 'norris', or 'buzzword'" diff --git a/crc/api/tools.py b/crc/api/tools.py index e0e1e41b..ee732b5c 100644 --- a/crc/api/tools.py +++ b/crc/api/tools.py @@ -1,18 +1,14 @@ import io import json -import uuid -from io import BytesIO import connexion -import jinja2 -from docxtpl import DocxTemplate from flask import send_file from jinja2 import Template, UndefinedError -from crc.api.common import ApiError, ApiErrorSchema +from crc.api.common import ApiError from crc.scripts.complete_template import CompleteTemplate -from crc.services.file_service import FileService - +from crc.scripts.script import Script +import crc.scripts def render_markdown(data, template): """ @@ -48,3 +44,17 @@ def render_docx(data): raise ApiError(code="invalid", message=str(e)) except Exception as e: raise ApiError(code="invalid", message=str(e)) + + +def list_scripts(): + """Provides a list of scripts that can be called by a script task.""" + scripts = Script.get_all_subclasses() + script_meta = [] + for script_class in scripts: + script_meta.append( + { + "name": script_class.__name__, + "description": script_class().get_description() + }) + return script_meta + diff --git a/crc/scripts/IRBDocAPI.py b/crc/scripts/IRBDocAPI.py deleted file mode 100644 index f998343e..00000000 --- a/crc/scripts/IRBDocAPI.py +++ /dev/null @@ -1,9 +0,0 @@ -import requests - - -class IRBDocAPI: - """Just your basic class that can pull in data from a few api endpoints and do a basic task.""" - - def do_task(self, task_data): - print('*** IRB_Doc_API > do_task ***') - print('task_data', task_data) diff --git a/crc/scripts/complete_template.py b/crc/scripts/complete_template.py index 098f36a1..85243dd5 100644 --- a/crc/scripts/complete_template.py +++ b/crc/scripts/complete_template.py @@ -17,11 +17,8 @@ from crc.services.workflow_processor import WorkflowProcessor class CompleteTemplate(Script): def get_description(self): - return - """Completes a word template, using the data available in the current task. Heavy on the - error messages, because there is so much that can go wrong here, and we want to provide - as much feedback as possible. Some of this might move up to a higher level object or be - passed into all tasks as we complete more work.""" + return """Takes one argument, which is the name of a MS Word docx file to use as a template. + All data currently collected up to this Task will be available for use in the template.""" def do_task(self, task, study_id, *args, **kwargs): """Entry point, mostly worried about wiring it all up.""" diff --git a/crc/scripts/fact_service.py b/crc/scripts/fact_service.py index 18a712eb..3cfaf9d7 100644 --- a/crc/scripts/fact_service.py +++ b/crc/scripts/fact_service.py @@ -5,7 +5,8 @@ from crc.scripts.script import Script class FactService(Script): def get_description(self): - return """Just your basic class that can pull in data from a few api endpoints and do a basic task.""" + return """Just your basic class that can pull in data from a few api endpoints and + do a basic task.""" def get_cat(self): response = requests.get('https://cat-fact.herokuapp.com/facts/random') diff --git a/crc/scripts/script.py b/crc/scripts/script.py index b4fb9f1c..ee5a3dd9 100644 --- a/crc/scripts/script.py +++ b/crc/scripts/script.py @@ -1,3 +1,7 @@ +import importlib +import os +import pkgutil + from crc.api.common import ApiError @@ -13,3 +17,25 @@ class Script: raise ApiError("invalid_script", "This is an internal error. The script you are trying to execute " + "does not properly implement the do_task function.") + + @staticmethod + def get_all_subclasses(): + return Script._get_all_subclasses(Script) + + @staticmethod + def _get_all_subclasses(cls): + + # hackish mess to make sure we have all the modules loaded for the scripts + pkg_dir = os.path.dirname(__file__) + for (module_loader, name, ispkg) in pkgutil.iter_modules([pkg_dir]): + importlib.import_module('.' + name, __package__) + + + """Returns a list of all classes that extend this class.""" + all_subclasses = [] + + for subclass in cls.__subclasses__(): + all_subclasses.append(subclass) + all_subclasses.extend(Script._get_all_subclasses(subclass)) + + return all_subclasses \ No newline at end of file diff --git a/crc/scripts/study_info.py b/crc/scripts/study_info.py index c6e3a768..612bde2a 100644 --- a/crc/scripts/study_info.py +++ b/crc/scripts/study_info.py @@ -13,8 +13,7 @@ class StudyInfo(Script): type_options = ['info', 'investigators', 'required_docs', 'details'] def get_description(self): - return """ - StudyInfo [TYPE] is one of 'info', 'investigators','required_docs', 'details' + return """StudyInfo [TYPE], where TYPE is one of 'info', 'investigators','required_docs', or 'details' Adds details about the current study to the Task Data. The type of information required should be provided as an argument. Basic returns the basic information such as the title. Investigators provides detailed information about each investigator in th study. Details provides a large number diff --git a/tests/test_tools_api.py b/tests/test_tools_api.py index eb004144..4005afa3 100644 --- a/tests/test_tools_api.py +++ b/tests/test_tools_api.py @@ -32,3 +32,11 @@ class TestStudyApi(BaseTest): data=file_data, follow_redirects=True, content_type='multipart/form-data') self.assert_success(rv) + + def test_list_scripts(self): + rv = self.app.get('/v1.0/list_scripts') + self.assert_success(rv) + scripts = json.loads(rv.get_data(as_text=True)) + self.assertTrue(len(scripts) > 1) + self.assertIsNotNone(scripts[0]['name']) + self.assertIsNotNone(scripts[0]['description'])