Renamed the required_docs script to just "documents", and it returns all documented in the irb_documents look up table indexed on the "Code" - so details become available in the task data like "documents.IRB_INFOSEC_DOC.required".

Updated the irb_documents with shorter code names, thanks to Alex. Re-worked the DMN models so they can properly read from this new datastructure.
This commit is contained in:
Dan Funk 2020-04-06 16:56:00 -04:00
parent d58a90a727
commit c6b6ee5d70
16 changed files with 105 additions and 67 deletions

View File

@ -4,30 +4,31 @@ from crc.services.file_service import FileService
from crc.services.protocol_builder import ProtocolBuilderService from crc.services.protocol_builder import ProtocolBuilderService
class RequiredDocs(Script): class Documents(Script):
"""Provides information about the documents required by Protocol Builder.""" """Provides information about the documents required by Protocol Builder."""
pb = ProtocolBuilderService() pb = ProtocolBuilderService()
def get_description(self): def get_description(self):
return """ return """
Provides detailed information about the documents required by the Protocol Builder. Provides detailed information about the documents loaded as a part of completing tasks.
Makes an immediate call to the IRB Protocol Builder API to get a list of currently required Makes an immediate call to the IRB Protocol Builder API to get a list of currently required
documents. It then collects all the information in a reference file called 'irb_pro_categories.xls', documents. It then collects all the information in a reference file called 'irb_pro_categories.xls',
if the Id from Protocol Builder matches an Id in this table, all data available in that row if the Id from Protocol Builder matches an Id in this table, all data available in that row
is also provided. is also provided.
This place a dictionary of values in the current task, where the key is the numeric id. This place a dictionary of values in the current task, where the key is the code in the lookup table.
For example: For example:
``` "required_docs" : ``` "documents" :
{ {
6: { "UVACompliance_PRCApproval": {
"name": "Cancer Center's PRC Approval Form", "name": "Cancer Center's PRC Approval Form",
"category1": "UVA Compliance", "category1": "UVA Compliance",
"category2": "PRC Approval", "category2": "PRC Approval",
"category3": "", "category3": "",
"Who Uploads?": "CRC", "Who Uploads?": "CRC",
"required": True, "required": True,
"requirement_id": 6
"upload_count": 0 "upload_count": 0
}, },
24: { ... 24: { ...
@ -37,30 +38,30 @@ For example:
def do_task_validate_only(self, task, study_id, *args, **kwargs): def do_task_validate_only(self, task, study_id, *args, **kwargs):
"""For validation only, pretend no results come back from pb""" """For validation only, pretend no results come back from pb"""
pb_docs = [] pb_docs = []
self.get_required_docs(study_id, pb_docs) task.data["required_docs"] = self.get_documents(study_id, pb_docs)
task.data["required_docs"] = self.get_required_docs(study_id, pb_docs)
def do_task(self, task, study_id, *args, **kwargs): def do_task(self, task, study_id, *args, **kwargs):
"""Takes data from the protocol builder, and merges it with data from the IRB Pro Categories """Takes data from the protocol builder, and merges it with data from the IRB Pro Categories
spreadsheet to return pertinent details about the required documents.""" spreadsheet to return pertinent details about the required documents."""
pb_docs = self.pb.get_required_docs(study_id, as_objects=True) pb_docs = self.pb.get_required_docs(study_id, as_objects=True)
self.get_required_docs(study_id, pb_docs) task.data["documents"] = self.get_documents(study_id, pb_docs)
task.data["required_docs"] = self.get_required_docs(study_id, pb_docs)
def get_required_docs(self, study_id, pb_docs): def get_documents(self, study_id, pb_docs):
"""Takes data from the protocol builder, and merges it with data from the IRB Pro Categories spreadsheet to return """Takes data from the protocol builder, and merges it with data from the IRB Pro Categories spreadsheet to return
pertinant details about the required documents.""" pertinant details about the required documents."""
doc_dictionary = FileService.get_file_reference_dictionary() doc_dictionary = FileService.get_file_reference_dictionary()
required_docs = {} required_docs = {}
for doc in pb_docs: for code, required_doc in doc_dictionary.items():
id = int(doc.AUXDOCID) try:
required_doc = {'id': id, 'name': doc.AUXDOC, 'required': True, pb_data = next((item for item in pb_docs if int(item.AUXDOCID) == int(required_doc['Id'])), None)
'count': 0} except:
if id in doc_dictionary: pb_data = None
required_doc = {**required_doc, **doc_dictionary[id]} required_doc['required'] = False
required_doc['count'] = self.get_count(study_id, doc_dictionary[id]["Code"]) if pb_data:
required_docs[id] = required_doc required_doc['required'] = True
required_doc['count'] = self.get_count(study_id, code)
required_docs[code] = required_doc
return required_docs return required_docs
def get_count(self, study_id, irb_doc_code): def get_count(self, study_id, irb_doc_code):

View File

@ -24,7 +24,28 @@ class StudyInfo(Script):
def do_task_validate_only(self, task, study_id, *args, **kwargs): def do_task_validate_only(self, task, study_id, *args, **kwargs):
"""For validation only, pretend no results come back from pb""" """For validation only, pretend no results come back from pb"""
self.check_args(args) self.check_args(args)
data = {
"study":{
"info": {
"id": 12,
"title": "test",
"primary_investigator_id":21,
"user_uid": "dif84",
"sponsor": "sponsor",
"ind_number": "1234",
"inactive": False
},
"investigators":
{
"INVESTIGATORTYPE": "PI",
"INVESTIGATORTYPEFULL": "Primary Investigator",
"NETBADGEID": "dhf8r"
},
"details":
{}
}
}
task.data["study"] = data["study"]
def do_task(self, task, study_id, *args, **kwargs): def do_task(self, task, study_id, *args, **kwargs):
self.check_args(args) self.check_args(args)

View File

@ -67,12 +67,12 @@ class FileService(object):
data_model = FileService.get_reference_file_data(FileService.IRB_PRO_CATEGORIES_FILE) data_model = FileService.get_reference_file_data(FileService.IRB_PRO_CATEGORIES_FILE)
xls = ExcelFile(data_model.data) xls = ExcelFile(data_model.data)
df = xls.parse(xls.sheet_names[0]) df = xls.parse(xls.sheet_names[0])
# Pandas is lovely, but weird. Here we drop records without an Id, and convert it to an integer. return df.set_index('Code').to_dict('index')
df = df.drop_duplicates(subset='Id').astype({'Id': 'Int64'}) # # Pandas is lovely, but weird. Here we drop records without an Id, and convert it to an integer.
# df = df.drop_duplicates(subset='Id').astype({'Id': 'Int64'})
# Now we index on the ID column and convert to a dictionary, where the key is the id, and the value # Now we index on the ID column and convert to a dictionary, where the key is the id, and the value
# is a dictionary with all the remaining data in it. It's kinda pretty really. # is a dictionary with all the remaining data in it. It's kinda pretty really.
all_dict = df.set_index('Id').to_dict('index') # all_dict = df.set_index('Id').to_dict('index')
return all_dict
@staticmethod @staticmethod
def add_task_file(study_id, workflow_id, task_id, name, content_type, binary_data, def add_task_file(study_id, workflow_id, task_id, name, content_type, binary_data,

View File

@ -6,20 +6,28 @@
</extensionElements> </extensionElements>
<decisionTable id="DecisionTable_1mjqwlv"> <decisionTable id="DecisionTable_1mjqwlv">
<input id="InputClause_18pwfqu" label="Required Doc Keys"> <input id="InputClause_18pwfqu" label="Required Doc Keys">
<inputExpression id="LiteralExpression_1y84stb" typeRef="string" expressionLanguage="feel"> <inputExpression id="LiteralExpression_1y84stb" typeRef="boolean" expressionLanguage="feel">
<text>required_docs.keys()</text> <text>documents['UVACompl_PRCAppr']['required']</text>
</inputExpression> </inputExpression>
</input> </input>
<output id="OutputClause_05y0j7c" label="data_security_plan" name="data_security_plan" typeRef="string" /> <output id="OutputClause_05y0j7c" label="data_security_plan" name="data_security_plan" typeRef="string" />
<rule id="DecisionRule_17xsr74"> <rule id="DecisionRule_17xsr74">
<description></description> <description></description>
<inputEntry id="UnaryTests_05ldcq4"> <inputEntry id="UnaryTests_05ldcq4">
<text>contains(6)</text> <text>true</text>
</inputEntry> </inputEntry>
<outputEntry id="LiteralExpression_09oao3s"> <outputEntry id="LiteralExpression_09oao3s">
<text>"required"</text> <text>"required"</text>
</outputEntry> </outputEntry>
</rule> </rule>
<rule id="DecisionRule_1ucfx7k">
<inputEntry id="UnaryTests_0t09cyd">
<text>false</text>
</inputEntry>
<outputEntry id="LiteralExpression_1fk77fu">
<text>"disabled"</text>
</outputEntry>
</rule>
</decisionTable> </decisionTable>
</decision> </decision>
</definitions> </definitions>

View File

@ -6,8 +6,8 @@
</extensionElements> </extensionElements>
<decisionTable id="decisionTable_1"> <decisionTable id="decisionTable_1">
<input id="InputClause_1ki80j6" label="required doc ids"> <input id="InputClause_1ki80j6" label="required doc ids">
<inputExpression id="LiteralExpression_10mfcy7" typeRef="string" expressionLanguage="Python"> <inputExpression id="LiteralExpression_10mfcy7" typeRef="boolean" expressionLanguage="Python">
<text>required_docs.keys()</text> <text>documents['UVACompl_PRCAppr']['required']</text>
</inputExpression> </inputExpression>
</input> </input>
<output id="output_1" label="enter_core_info" name="enter_core_info" typeRef="string" /> <output id="output_1" label="enter_core_info" name="enter_core_info" typeRef="string" />

View File

@ -5,15 +5,15 @@
<biodi:bounds x="190" y="70" width="180" height="80" /> <biodi:bounds x="190" y="70" width="180" height="80" />
</extensionElements> </extensionElements>
<decisionTable id="DecisionTable_00zdxg0"> <decisionTable id="DecisionTable_00zdxg0">
<input id="InputClause_02n3ccs" label="Required Doc Ids"> <input id="InputClause_02n3ccs" label="CoCApplication Required?">
<inputExpression id="LiteralExpression_1ju4o1o" typeRef="string" expressionLanguage="feel"> <inputExpression id="LiteralExpression_1ju4o1o" typeRef="boolean" expressionLanguage="feel">
<text>required_docs.keys()</text> <text>documents['AD_LabManual']['required']</text>
</inputExpression> </inputExpression>
</input> </input>
<output id="OutputClause_1ybi1ud" label="sponsor_funding_source" name="eat_my_shorts" typeRef="string" /> <output id="OutputClause_1ybi1ud" label="sponsor_funding_source" name="eat_my_shorts" typeRef="string" />
<rule id="DecisionRule_1t97mw4"> <rule id="DecisionRule_1t97mw4">
<inputEntry id="UnaryTests_0ym4ln2"> <inputEntry id="UnaryTests_0ym4ln2">
<text>contains(12)</text> <text>true</text>
</inputEntry> </inputEntry>
<outputEntry id="LiteralExpression_1pweuqc"> <outputEntry id="LiteralExpression_1pweuqc">
<text>"required"</text> <text>"required"</text>
@ -21,7 +21,7 @@
</rule> </rule>
<rule id="DecisionRule_1q965wz"> <rule id="DecisionRule_1q965wz">
<inputEntry id="UnaryTests_1mlhh3t"> <inputEntry id="UnaryTests_1mlhh3t">
<text>not contains(12)</text> <text>false</text>
</inputEntry> </inputEntry>
<outputEntry id="LiteralExpression_073vd6i"> <outputEntry id="LiteralExpression_073vd6i">
<text>"disabled"</text> <text>"disabled"</text>

View File

@ -11,7 +11,7 @@
<bpmn:scriptTask id="Task_Load_Requirements" name="Load Required Documents From PM"> <bpmn:scriptTask id="Task_Load_Requirements" name="Load Required Documents From PM">
<bpmn:incoming>SequenceFlow_1ees8ka</bpmn:incoming> <bpmn:incoming>SequenceFlow_1ees8ka</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_17ct47v</bpmn:outgoing> <bpmn:outgoing>SequenceFlow_17ct47v</bpmn:outgoing>
<bpmn:script>RequiredDocs</bpmn:script> <bpmn:script>Documents</bpmn:script>
</bpmn:scriptTask> </bpmn:scriptTask>
<bpmn:businessRuleTask id="Activity_1yqy50i" name="Enter Core Info&#10;" camunda:decisionRef="enter_core_info"> <bpmn:businessRuleTask id="Activity_1yqy50i" name="Enter Core Info&#10;" camunda:decisionRef="enter_core_info">
<bpmn:incoming>Flow_1m8285h</bpmn:incoming> <bpmn:incoming>Flow_1m8285h</bpmn:incoming>

View File

@ -15,7 +15,7 @@
OGC will upload the Non-Funded Executed Agreement after it has been negotiated by OSP contract negotiator.</bpmn:documentation> OGC will upload the Non-Funded Executed Agreement after it has been negotiated by OSP contract negotiator.</bpmn:documentation>
<bpmn:extensionElements> <bpmn:extensionElements>
<camunda:formData> <camunda:formData>
<camunda:formField id="UVACompliance.PRCApproval" label="Non-Funded Executed Agreement" type="file"> <camunda:formField id="UVACompl_PRCAppr" label="Non-Funded Executed Agreement" type="file">
<camunda:properties> <camunda:properties>
<camunda:property id="group" value="upload" /> <camunda:property id="group" value="upload" />
<camunda:property id="repeat" value="upload" /> <camunda:property id="repeat" value="upload" />

View File

@ -5,21 +5,29 @@
<biodi:bounds x="190" y="80" width="180" height="80" /> <biodi:bounds x="190" y="80" width="180" height="80" />
</extensionElements> </extensionElements>
<decisionTable id="DecisionTable_1mjqwlv"> <decisionTable id="DecisionTable_1mjqwlv">
<input id="InputClause_18pwfqu" label="Required Doc Keys"> <input id="InputClause_18pwfqu" label="Data Plan Required in PB?">
<inputExpression id="LiteralExpression_1y84stb" typeRef="string" expressionLanguage="feel"> <inputExpression id="LiteralExpression_1y84stb" typeRef="boolean" expressionLanguage="feel">
<text>required_docs.keys()</text> <text>documents['Study_DataSecurityPlan']['required']</text>
</inputExpression> </inputExpression>
</input> </input>
<output id="OutputClause_05y0j7c" label="data_security_plan" name="data_security_plan" typeRef="string" /> <output id="OutputClause_05y0j7c" label="data_security_plan" name="data_security_plan" typeRef="string" />
<rule id="DecisionRule_17xsr74"> <rule id="DecisionRule_17xsr74">
<description></description> <description></description>
<inputEntry id="UnaryTests_05ldcq4"> <inputEntry id="UnaryTests_05ldcq4">
<text>contains(6)</text> <text>true</text>
</inputEntry> </inputEntry>
<outputEntry id="LiteralExpression_09oao3s"> <outputEntry id="LiteralExpression_09oao3s">
<text>"required"</text> <text>"required"</text>
</outputEntry> </outputEntry>
</rule> </rule>
<rule id="DecisionRule_1ucfx7k">
<inputEntry id="UnaryTests_0t09cyd">
<text>false</text>
</inputEntry>
<outputEntry id="LiteralExpression_1fk77fu">
<text>"disabled"</text>
</outputEntry>
</rule>
</decisionTable> </decisionTable>
</decision> </decision>
</definitions> </definitions>

View File

@ -6,8 +6,8 @@
</extensionElements> </extensionElements>
<decisionTable id="decisionTable_1"> <decisionTable id="decisionTable_1">
<input id="InputClause_1ki80j6" label="required doc ids"> <input id="InputClause_1ki80j6" label="required doc ids">
<inputExpression id="LiteralExpression_10mfcy7" typeRef="string" expressionLanguage="Python"> <inputExpression id="LiteralExpression_10mfcy7" typeRef="boolean" expressionLanguage="Python">
<text>required_docs.keys()</text> <text>documents['UVACompl_PRCAppr']['required']</text>
</inputExpression> </inputExpression>
</input> </input>
<output id="output_1" label="enter_core_info" name="enter_core_info" typeRef="string" /> <output id="output_1" label="enter_core_info" name="enter_core_info" typeRef="string" />

View File

@ -5,15 +5,15 @@
<biodi:bounds x="190" y="70" width="180" height="80" /> <biodi:bounds x="190" y="70" width="180" height="80" />
</extensionElements> </extensionElements>
<decisionTable id="DecisionTable_00zdxg0"> <decisionTable id="DecisionTable_00zdxg0">
<input id="InputClause_02n3ccs" label="Required Doc Ids"> <input id="InputClause_02n3ccs" label="Sponsor Document Required in PB?">
<inputExpression id="LiteralExpression_1ju4o1o" typeRef="string" expressionLanguage="feel"> <inputExpression id="LiteralExpression_1ju4o1o" typeRef="boolean" expressionLanguage="feel">
<text>required_docs.keys()</text> <text>documents['AD_LabManual']['required']</text>
</inputExpression> </inputExpression>
</input> </input>
<output id="OutputClause_1ybi1ud" label="sponsor_funding_source" name="eat_my_shorts" typeRef="string" /> <output id="OutputClause_1ybi1ud" label="sponsor_funding_source" name="eat_my_shorts" typeRef="string" />
<rule id="DecisionRule_1t97mw4"> <rule id="DecisionRule_1t97mw4">
<inputEntry id="UnaryTests_0ym4ln2"> <inputEntry id="UnaryTests_0ym4ln2">
<text>contains(12)</text> <text>true</text>
</inputEntry> </inputEntry>
<outputEntry id="LiteralExpression_1pweuqc"> <outputEntry id="LiteralExpression_1pweuqc">
<text>"required"</text> <text>"required"</text>
@ -21,7 +21,7 @@
</rule> </rule>
<rule id="DecisionRule_1q965wz"> <rule id="DecisionRule_1q965wz">
<inputEntry id="UnaryTests_1mlhh3t"> <inputEntry id="UnaryTests_1mlhh3t">
<text>not contains(12)</text> <text>false</text>
</inputEntry> </inputEntry>
<outputEntry id="LiteralExpression_073vd6i"> <outputEntry id="LiteralExpression_073vd6i">
<text>"disabled"</text> <text>"disabled"</text>

View File

@ -11,7 +11,7 @@
<bpmn:scriptTask id="Task_Load_Requirements" name="Load Required Documents From PM"> <bpmn:scriptTask id="Task_Load_Requirements" name="Load Required Documents From PM">
<bpmn:incoming>SequenceFlow_1ees8ka</bpmn:incoming> <bpmn:incoming>SequenceFlow_1ees8ka</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_17ct47v</bpmn:outgoing> <bpmn:outgoing>SequenceFlow_17ct47v</bpmn:outgoing>
<bpmn:script>RequiredDocs</bpmn:script> <bpmn:script>Documents</bpmn:script>
</bpmn:scriptTask> </bpmn:scriptTask>
<bpmn:businessRuleTask id="Activity_1yqy50i" name="Enter Core Info&#10;" camunda:decisionRef="enter_core_info"> <bpmn:businessRuleTask id="Activity_1yqy50i" name="Enter Core Info&#10;" camunda:decisionRef="enter_core_info">
<bpmn:incoming>Flow_1m8285h</bpmn:incoming> <bpmn:incoming>Flow_1m8285h</bpmn:incoming>

View File

@ -4,7 +4,7 @@ from unittest.mock import patch
from crc import db from crc import db
from crc.models.file import FileDataModel, FileModel from crc.models.file import FileDataModel, FileModel
from crc.models.protocol_builder import ProtocolBuilderRequiredDocumentSchema from crc.models.protocol_builder import ProtocolBuilderRequiredDocumentSchema
from crc.scripts.required_docs import RequiredDocs from crc.scripts.documents import Documents
from crc.services.file_service import FileService from crc.services.file_service import FileService
from tests.base_test import BaseTest from tests.base_test import BaseTest
@ -29,13 +29,13 @@ class TestRequiredDocsScript(BaseTest):
db.session.query(FileModel).filter(FileModel.id == file_model.id).delete() db.session.query(FileModel).filter(FileModel.id == file_model.id).delete()
db.session.commit() db.session.commit()
db.session.flush() db.session.flush()
errors = RequiredDocs.validate() errors = Documents.validate()
self.assertTrue(len(errors) > 0) self.assertTrue(len(errors) > 0)
self.assertEquals("file_not_found", errors[0].code) self.assertEquals("file_not_found", errors[0].code)
def test_no_validation_error_when_correct_file_exists(self): def test_no_validation_error_when_correct_file_exists(self):
self.create_reference_document() self.create_reference_document()
errors = RequiredDocs.validate() errors = Documents.validate()
self.assertTrue(len(errors) == 0) self.assertTrue(len(errors) == 0)
def test_load_lookup_data(self): def test_load_lookup_data(self):
@ -50,31 +50,31 @@ class TestRequiredDocsScript(BaseTest):
def test_get_required_docs(self): def test_get_required_docs(self):
pb_docs = self.get_required_docs() pb_docs = self.get_required_docs()
self.create_reference_document() self.create_reference_document()
script = RequiredDocs() script = Documents()
required_docs = script.get_required_docs(12, pb_docs) # Mocked out, any random study id works. documents = script.get_documents(12, pb_docs) # Mocked out, any random study id works.
self.assertIsNotNone(required_docs) self.assertIsNotNone(documents)
self.assertTrue(6 in required_docs.keys()) self.assertTrue("UVACompl_PRCAppr" in documents.keys())
self.assertEquals("Cancer Center's PRC Approval Form", required_docs[6]['name']) self.assertEquals("Cancer Center's PRC Approval Form", documents["UVACompl_PRCAppr"]['Name'])
self.assertEquals("UVA Compliance", required_docs[6]['category1']) self.assertEquals("UVA Compliance", documents["UVACompl_PRCAppr"]['category1'])
self.assertEquals("PRC Approval", required_docs[6]['category2']) self.assertEquals("PRC Approval", documents["UVACompl_PRCAppr"]['category2'])
self.assertEquals("CRC", required_docs[6]['Who Uploads?']) self.assertEquals("CRC", documents["UVACompl_PRCAppr"]['Who Uploads?'])
self.assertEquals(0, required_docs[6]['count']) self.assertEquals(0, documents["UVACompl_PRCAppr"]['count'])
def test_get_required_docs_has_correct_count_when_a_file_exists(self): def test_get_required_docs_has_correct_count_when_a_file_exists(self):
self.load_example_data() self.load_example_data()
pb_docs = self.get_required_docs() pb_docs = self.get_required_docs()
# Make sure the xslt reference document is in place. # Make sure the xslt reference document is in place.
self.create_reference_document() self.create_reference_document()
script = RequiredDocs() script = Documents()
# Add a document to the study with the correct code. # Add a document to the study with the correct code.
workflow = self.create_workflow('docx') workflow = self.create_workflow('docx')
irb_code = "UVACompliance.PRCApproval" # The first file referenced in pb required docs. irb_code = "UVACompl_PRCAppr" # The first file referenced in pb required docs.
FileService.add_task_file(study_id=workflow.study_id, workflow_id=workflow.id, FileService.add_task_file(study_id=workflow.study_id, workflow_id=workflow.id,
task_id="fakingthisout", task_id="fakingthisout",
name="anything.png", content_type="text", name="anything.png", content_type="text",
binary_data=b'1234', irb_doc_code=irb_code) binary_data=b'1234', irb_doc_code=irb_code)
required_docs = script.get_required_docs(workflow.study_id, pb_docs) docs = script.get_documents(workflow.study_id, pb_docs)
self.assertIsNotNone(required_docs) self.assertIsNotNone(docs)
self.assertEquals(1, required_docs[6]['count']) self.assertEquals(1, docs["UVACompl_PRCAppr"]['count'])

View File

@ -368,13 +368,13 @@ class TestWorkflowProcessor(BaseTest):
# It should mark Enter Core Data as required, because it is always required. # It should mark Enter Core Data as required, because it is always required.
self.assertTrue("enter_core_info" in data) self.assertTrue("enter_core_info" in data)
self.assertEquals("required", data["enter_core_info"]) self.assertEqual("required", data["enter_core_info"])
# It should mark the Data Security Plan as required, because InfoSec Approval (24) is included in required docs. # It should mark the Data Security Plan as required, because InfoSec Approval (24) is included in required docs.
self.assertTrue("data_security_plan" in data) self.assertTrue("data_security_plan" in data)
self.assertEquals("required", data["data_security_plan"]) self.assertEqual("required", data["data_security_plan"])
# It should mark the sponsor funding source as disabled since the funding required (12) is not included in the required docs. # It should mark the sponsor funding source as disabled since the funding required (12) is not included in the required docs.
self.assertTrue("sponsor_funding_source" in data) self.assertTrue("sponsor_funding_source" in data)
self.assertEquals("disabled", data["sponsor_funding_source"]) self.assertEqual("disabled", data["sponsor_funding_source"])