Documents
Documents are an important part of submission SmartForm and workflow data. Since documents can be numerous and potentially large in size, they require special handling in the integration functionality with IRB Exchange. Data is included with submission JSON data that specifies the documents.
Handling documents in the integration functionality involves three keys components:
- Creating JSON to represent documents metadata whenever it's needed for generating JSON for a submission or workflow item to upload to the IRB Exchange.
- Parsing JSON documents metadata into entities on a store when downloading information from IRB Exchange.
Downloading the document content into respective document entities.
Creating Document JSON Metadata for Upload
Other methods (which will be covered in later tutorials) need a common method to call that will generate JSON for a document set. The following is an example of such a method from the Huron IRB implementation:
_IRBSubmission.getDocSetPoRefs Method Example
/** Converts the relevant data for an Attachment or Document set into JSON for upload to the IRB Exchange.
@param docSet (Set of Entity) The _Attachment or Document set to get JSON for
@return docSetObj (JSON) - representation of the docSet data; an array of objects representing _Attachments or Documents
**/
function getDocSetPoRefs(docSet) {
var docSetObj = [], memberObj, member, docModel, finalDocument;
if (docSet != null) {
var docSetElements = docSet.elements();
var docSetCount = docSetElements.count();
for (var i = 1; i <= docSetCount; i++) {
member = docSetElements.item(i);
docModel = {};
if(member.getType() == "_Attachment") {
// Draft document will always be set
docModel.category = member.getQualifiedAttribute("customAttributes.category.ID");
docModel.draftVersion = member.getQualifiedAttribute("customAttributes.draftVersion.version");
docModel.draftPoRef = member.getQualifiedAttribute("customAttributes.draftVersion") + "";
docModel.draftDateModified = member.getQualifiedAttribute("customAttributes.draftVersion.dateModified");
docModel.attachmentPoRef = member + "";
// Get the final document if set
finalDocument = member.getQualifiedAttribute("customAttributes.finalVersion");
if (finalDocument != null) {
docModel.finalVersion = finalDocument.getQualifiedAttribute("version");
docModel.finalPoRef = finalDocument + "";
}
// If this is a site mod, we'll need the poRef of the initial site version of this Attachment
var metaCloneId = member.getQualifiedAttribute("metaCloneId");
var owningEntity = member.getQualifiedAttribute("owningEntity");
var memberOwningEntitySubmissionType = owningEntity.getQualifiedAttribute("customAttributes.submissionType.ID");
if(metaCloneId != null && memberOwningEntitySubmissionType == "DRAFT") {
var initialSubmission = owningEntity.getQualifiedAttribute("customAttributes.parentStudy");
var initialSiteAttachmentMatches = ApplicationEntity.getResultSet("_Attachment").query("ID != '" + member.ID + "' and metaCloneId = '" + metaCloneId + "' and dateCreated is not null and owningEntity = " + initialSubmission).elements();
var initialSiteAttachmentMatchesCount = initialSiteAttachmentMatches.count();
if(initialSiteAttachmentMatchesCount != 1) {
throw new Error("Unexpected number of matches for _Attachment with metaCloneId " + metaCloneId + "; count: " + initialSiteAttachmentMatchesCount);
}
docModel.attachmentInitialSitePoRef = initialSiteAttachmentMatches.item(1) + "";
}
} else {
docModel.draftPoRef = member + "";
docModel.draftVersion = member.getQualifiedAttribute("version");
}
docSetObj.push(docModel);
}
}
return docSetObj;
}
Technical notes about this method example:
- If the set in question is a set of _Attachment, then there are some additional items of metadata to set. Attachments have a draft document and final document. Final document might not be set, but draft always will be. Sets of Document use the draft attributes in the JSON to store its metadata for simpler code.
- For site modifications, we use metaCloneId to link original and modification documents so that other features like View Differences work properly. metaCloneId is a relatively newer addition in Portal, so if your version of Portal is older you should check if the attribute exists and if not you may need to use an appropriate proxy such as dateCreated to tie original and modification versions of documents together.
Setting Attributes and Checking for Changes
When setting data onto attributes, we want to know whether the value has changed or is the same. This check aids in providing feedback about whether changes occurred. The following is an example of such a method from the Huron IRB implementation:
_IRBSubmission.setAttributeAndChangeCheck Method Example
/** Set a single value attribute on the submission that was downloaded from Exchange and check if the value is different. If different, set a flag on the activity that downloaded the data.
*
* @param entity (Entity) The entity to set the value on
* @param newValue (Expression) The new value for the attribute
* @param attributePath (string) The attribute path to the attribute from the submission
* @param activity (Activity) The Update from IRB Exchange or Update from IRB Exchange (Admin) activity that downloaded the data
* @param isDate (Boolean) Flag indicating whether the data type is date, which requires special handling
* @return {Boolean} True if the value has changed and not initial download, false otherwise
*/
function setAttributeAndChangeCheck(entity, newValue, attributePath, activity, isDate) {
var hasChanged = false;
var oldValue = entity.getQualifiedAttribute(attributePath);
if(isDate == true && oldValue != null) {
oldValue = oldValue.getTime();
}
if(isDate == true && newValue != null && newValue != undefined) {
entity.setQualifiedAttribute(attributePath, newValue);
newValue = entity.getQualifiedAttribute(attributePath);
newValue = newValue.getTime();
}
if(newValue != oldValue && newValue != undefined) {
if(isDate == false) {
entity.setQualifiedAttribute(attributePath, newValue);
}
if(activity != null) {
activity.setQualifiedAttribute("customAttributes.hasDataChanged", true);
hasChanged = true;
}
}
return hasChanged;
}
Technical note about this method example:
- New data fields added in subsequent releases of IRB Exchange will show as 'undefined' in older data. In the case that 'undefined' data is encountered, we don't want to overwrite user entered data.
Parsing JSON Documents Metadata
Upon downloading data from the IRB Exchange, other methods (which will be covered in later tutorials) need a method to call that will create or synchronize a document set's entity data. Note that this method in most cases should probably not download the document content at this point, since this code has to run within a standard Portal transaction that is subject to time out. Large sets of documents or large documents could cause time outs if their document content is downloaded in this method. Information about downloading the document content is contained in a later section of this tutorial and the Updates tutorials. The following is an example of such a method from the Huron IRB implementation:
_IRBSubmission.downloadDocSet Method Example
/** Takes JSON from the IRB Exchange for a multi-site study and sets the data onto the appropriate external study.
@param exchangeClient (IrbExchange) Handle to the etype to run operations on against the Exchange
@param study (_IRBSubmission) Study the container is related to
@param entity (Entity) Entity to download the doc set to
@param attributePath (string) Attribute path to the doc set on the entity
@param docListJson (JSON) JSON array of document item Exchange IDs
@param isAttachment (Boolean) Flag indicating whether the document set is a set of Attachment
@param documentsToDownloadJson (JSON) The JSON array so far of documents to download for other document sets for this download or update operation
@param submission (_IRBSubmission) The submission the doc set lives on
@param activity (Activity) The Update from IRB Exchange or Update from IRB Exchange (Admin) activity that triggered the call here, or null if initial download
@return documentsToDownloadJson (JSON) - The JSON array of documents to download
**/
function downloadDocSet(exchangeClient, study, entity, attributePath, docListJson, isAttachment, documentsToDownloadJson, submission, attachmentSetAttrDisplayName, eSetAttrDisplayName, eSetAttrInternalName, eSetMemberDisplayAttrPath, activity){
// Loop through the JSON array of document info to download and download data for them in calls to helper method
if(docListJson != null) {
for(var i = 0; i < docListJson.length; i++) {
documentsToDownloadJson = downloadDoc(exchangeClient, study, entity, attributePath, docListJson[i], isAttachment, documentsToDownloadJson, submission, attachmentSetAttrDisplayName, eSetAttrDisplayName, eSetAttrInternalName, eSetMemberDisplayAttrPath, "draft", activity);
if(docListJson[i].finalExchangeRef != null) {
documentsToDownloadJson = downloadDoc(exchangeClient, study, entity, attributePath, docListJson[i], isAttachment, documentsToDownloadJson, submission, attachmentSetAttrDisplayName, eSetAttrDisplayName, eSetAttrInternalName, eSetMemberDisplayAttrPath, "final", activity);
}
}
}
// Handle removals
var documentsToKeep = Document.createEntitySet();
var doc;
for(var i = 0; i < documentsToDownloadJson.length; i++) {
doc = wom.getEntityFromString(documentsToDownloadJson[i].document);
documentsToKeep.addElement(doc);
}
var docSet = entity.getQualifiedAttribute(attributePath);
if(docSet == null) {
return documentsToDownloadJson;
}
var docSetElements = docSet.elements();
var docSetCount = docSet.count();
var currentExchangeId, currentFile, doRemoval, draftDoc, finalDoc, containsDraft, containsFinal;
for(var i = 1; i <= docSetCount; i++) {
doRemoval = false;
currentFile = docSetElements.item(i);
if(currentFile.getType() == "_Attachment") {
draftDoc = currentFile.getQualifiedAttribute("customAttributes.draftVersion");
finalDoc = currentFile.getQualifiedAttribute("customAttributes.finalVersion");
containsDraft = documentsToKeep.contains(draftDoc);
if(finalDoc != null) {
containsFinal = documentsToKeep.contains(finalDoc);
} else {
containsFinal = false;
}
if(!containsDraft && !containsFinal) {
doRemoval = true;
}
if(!containsDraft) {
currentExchangeId = IrbExchangeReference.GetExchangeId(draftDoc + "", exchangeClient + "", exchangeClient.GetEndpoint() + "");
if(currentExchangeId != null) {
// Remove no longer needed reference entity
IrbExchangeReference.LinkExchangeIdToPoRef(currentExchangeId, draftDoc + "", exchangeClient + "", null, true, true, true, false);
}
}
if(!containsFinal) {
currentExchangeId = IrbExchangeReference.GetExchangeId(finalDoc + "", exchangeClient + "", exchangeClient.GetEndpoint() + "");
if(currentExchangeId != null) {
// Remove no longer needed reference entity
IrbExchangeReference.LinkExchangeIdToPoRef(currentExchangeId, finalDoc + "", exchangeClient + "", null, true, true, true, false);
}
}
} else if(!documentsToKeep.contains(currentFile)) {
doRemoval = true;
currentExchangeId = IrbExchangeReference.GetExchangeId(currentFile + "", exchangeClient + "", exchangeClient.GetEndpoint() + "");
if(currentExchangeId != null) {
// Remove no longer needed reference entity
IrbExchangeReference.LinkExchangeIdToPoRef(currentExchangeId, currentFile + "", exchangeClient + "", null, true, true, true, false);
}
}
if(doRemoval == true) {
docSet.removeElement(currentFile);
currentFile.unregisterEntity();
if(activity != null) {
activity.setQualifiedAttribute("customAttributes.hasDataChanged", true);
}
}
}
return documentsToDownloadJson;
}
/** Helper method to set data for a document in this store from the JSON from the IRB Exchange.
@param docJson (JSON) JSON representation of the document or attachment
@param draftOrFinal (string) Denotes whether this is a draft or final version of an Attachment; if Document type, will be equal to 'draft'
@param isAttachment (Boolean) Flag indicating whether this is an Attachment or Document type
@param entity (Entity) The entity to store the document or attachment on
@param documentsToDownloadJson (JSON) Array of documents storing metadata to support downloading the document content in a later operation
@param attributePath (string) Fully qualified attribute path to the attribute on the entity to store the documents or attachments on
@param exchangeClient (IrbExchange) The entity used to make calls to the IRB Exchange
@param study (_IRBSubmission) The study
@param submission (_IRBSubmission) The submission the doc set lives on
@param activity (Activity) The Update from IRB Exchange or Update from IRB Exchange (Admin) activity that triggered the call here, or null if initial download
@return {documentsToDownloadJson} (JSON) Array of documents storing metadata to support downloading the document content in a later operation
**/
function downloadDoc(exchangeClient, study, entity, attributePath, docJson, isAttachment, documentsToDownloadJson, submission, attachmentSetAttrDisplayName, eSetAttrDisplayName, eSetAttrInternalName, eSetMemberDisplayAttrPath, draftOrFinal, activity) {
var documentToDownloadJson = {};
// Attempt to get the document to see if it exists already
var doc = null, oldDoc = null, changeTrackingInfoAddition = "";
if(docJson[draftOrFinal + "ExchangeRef"] != null) {
var docPoRef = IrbExchangeReference.GetPoRef(docJson[draftOrFinal + "ExchangeRef"] + "", exchangeClient + "", exchangeClient.GetEndpoint() + "");
try {
doc = wom.getEntityFromString(docPoRef);
} catch(e) {
// Intentionally eat the exception; for _Attachment type, if wom.getEntityFromString is called for an entity that no longer exists, it will throw an exception, but this scenario is expected
}
if(doc != null) {
var docSet;
if (isAttachment == true) {
var attachmentSet = entity.getQualifiedAttribute(attributePath);
if (attachmentSet != null) {
docSet = attachmentSet.dereference("customAttributes." + draftOrFinal + "Version");
if (docSet.query("ID = '" + doc.ID + "'").count() == 0) {
doc = null;
}
}
}
}
}
// Check if this is a site mod attachment, if so it may already exist on the draft
if(doc == null && docJson[draftOrFinal + "InitialSitePoRef"] != null) {
var docInitialSite = null;
try {
docInitialSite = wom.getEntityFromString(docJson[draftOrFinal + "InitialSitePoRef"]);
} catch(e) {
// Intentionally eat the exception; for _Attachment type, if wom.getEntityFromString is called for an entity that no longer exists, it will throw an exception, but this scenario is expected
}
if(docInitialSite != null) {
var initialSiteAttachmentMatches = ApplicationEntity.getResultSet("_Attachment").query("customAttributes." + draftOrFinal + "Version.ID = '" + docInitialSite.ID + "'").elements();
if (initialSiteAttachmentMatches.count() == 1) {
var initialAttachment = initialSiteAttachmentMatches.item(1);
var initialAttachmentMetaCloneId = initialAttachment.getQualifiedAttribute("metaCloneId");
var draftSiteAttachmentMatches = ApplicationEntity.getResultSet("_Attachment").query("ID != '" + initialAttachment.ID + "' and metaCloneId = '" + initialAttachmentMetaCloneId + "'").elements();
if (draftSiteAttachmentMatches.count() == 1) {
doc = draftSiteAttachmentMatches.item(1).getQualifiedAttribute("customAttributes." + draftOrFinal + "Version");
IrbExchangeReference.LinkExchangeIdToPoRef(docJson[draftOrFinal + "ExchangeRef"], doc + "", exchangeClient + "", null, true, false, true, false);
}
}
}
}
// Document doesn't exist in store, create it
var currentUser = Person.getCurrentUser();
var attachment = null;
if(doc == null) {
documentToDownloadJson.oldDocumentPoRef = null;
documentToDownloadJson.oldName = "";
documentToDownloadJson.oldVersion = "";
// Create the document and set some initial data
doc = Document.createEntity();
var modifier = ApplicationEntity.getResultSet("Modifier").query("ID = 'Document'").elements();
if(modifier.count() == 0) {
throw new Error("Document modifier not found");
}
doc.setQualifiedAttribute("modifiers", modifier.item(1), "add");
doc.setQualifiedAttribute("owner", currentUser);
// Add a reference between this document (OID) and the Exchange ID for tracking for future updates
var entityType = entity.getType();
if (entityType != "_IRBSubmission_ReportContinuingReviewData") {
IrbExchangeReference.LinkExchangeIdToPoRef(docJson[draftOrFinal + "ExchangeRef"], doc + "", exchangeClient + "", null, true, false, true, false);
}
// Handle Attachment type
if (isAttachment == true) {
if (draftOrFinal == "draft") {
attachment = _Attachment.createEntity();
attachment.setQualifiedAttribute("dateCreated", new Date());
attachment.setQualifiedAttribute("owningEntity", submission);
entity.setQualifiedAttribute(attributePath, attachment, "add");
documentToDownloadJson.oldCategoryPoRef = null;
} else {
var draftPoRef = IrbExchangeReference.GetPoRef(docJson.draftExchangeRef, exchangeClient + "", null);
var draftDoc = wom.getEntityFromString(draftPoRef);
var attachments = ApplicationEntity.getResultSet("_Attachment").query("customAttributes.draftVersion = " + draftDoc).elements();
if (attachments.count != 1) {
throw new Error("Unexpected number of _Attachment with draftVersion = " + draftDoc);
}
attachment = attachments.item(1);
}
attachment.setQualifiedAttribute("dateModified", new Date());
attachment.setQualifiedAttribute("customAttributes." + draftOrFinal + "Version", doc);
if (docJson.category != null && draftOrFinal == "draft") {
var attachmentCategory = getEntityForAttribute(docJson, "ID = '" + docJson.category.replace("'", "''") + "'", "_AttachmentCategory", null, null);
attachment.setQualifiedAttribute("customAttributes.category", attachmentCategory);
}
}
// Handle Document type only
else {
if (entityType == "_IRBSubmission_ReportContinuingReviewData") {
// Due to technical reasons, for site continuing review documents we download the document content now
entity.setQualifiedAttribute(attributePath, doc, "add");
var targetUrl = exchangeClient.GetDocument(study, doc + "", docJson.draftExchangeRef);
doc.setQualifiedAttribute("targetURL", targetUrl);
targetUrl.addAccessValidationMethod(doc, "userCanRead");
doc.AddHistoryEntry("Downloaded from IRB Exchange", "", true);
} else {
documentToDownloadJson.entity = entity + "";
documentToDownloadJson.attributePath = attributePath;
}
}
} else if (isAttachment == true) {
var attachments = ApplicationEntity.getResultSet("_Attachment").query("customAttributes." + draftOrFinal + "Version = " + doc).elements();
if (attachments.count != 1) {
throw new Error("Unexpected number of _Attachment with " + draftOrFinal + "Version = " + doc);
}
attachment = attachments.item(1);
documentToDownloadJson.oldDocumentPoRef = doc + "";
documentToDownloadJson.oldName = doc.getQualifiedAttribute("name");
documentToDownloadJson.oldVersion = doc.getQualifiedAttribute("version");
// Attachment specific, so only do once, for draft
if (draftOrFinal == "draft") {
documentToDownloadJson.oldCategoryPoRef = attachment.getQualifiedAttribute("customAttributes.category") + "";
}
}
// Set document entity data that needs to be set for creation and update
setAttributeAndChangeCheck(doc, docJson[draftOrFinal + "Name"], "name", activity, false);
setAttributeAndChangeCheck(doc, docJson[draftOrFinal + "Version"], "version", activity, false);
if(draftOrFinal == "draft") {
doc.setQualifiedAttribute("dateModified", docJson["draftDateModified"]);
}
documentToDownloadJson.document = doc + "";
if(activity != null) {
documentToDownloadJson.updateFromExchangeActivity = activity + "";
} else {
documentToDownloadJson.updateFromExchangeActivity = null;
}
// Add metadata to JSON for change tracking info additions later on for attachments
if(isAttachment == true) {
// eSet member attachment set
if (eSetAttrDisplayName != null) {
documentToDownloadJson.eSetAttrDisplayName = eSetAttrDisplayName;
documentToDownloadJson.eSetAttrInternalName = eSetAttrInternalName;
documentToDownloadJson.eSetMemberPoRef = entity + "";
documentToDownloadJson.eSetMemberDisplayValue = entity.getQualifiedAttribute(eSetMemberDisplayAttrPath);
} else {
documentToDownloadJson.eSetMemberPoRef = null;
}
// All attachments
documentToDownloadJson.attachmentSetAttrDisplayName = attachmentSetAttrDisplayName;
documentToDownloadJson.attachmentSetAttrInternalName = attributePath;
documentToDownloadJson.attachmentPoRef = attachment + "";
documentToDownloadJson.draftOrFinal = draftOrFinal;
var oldTargetUrlPoRef = doc.getQualifiedAttribute("targetURL");
if(oldTargetUrlPoRef == null) {
documentToDownloadJson.oldTargetUrlPoRef = "";
} else {
documentToDownloadJson.oldTargetUrlPoRef = EntityUtils.GetDocumentProxyPOREF(oldTargetUrlPoRef);
}
}
documentsToDownloadJson[documentsToDownloadJson.length] = documentToDownloadJson;
return documentsToDownloadJson;
}
Technical notes about this method example:
- This method creates and updates the Document and Attachment entities needed to represent the documents' data from the IRB Exchange. It has algorithms to effectively synchronize the set, which includes deleting documents that have been deleted from the IRB Exchange.
- A JSON array is used to track documents whose document content needs to be downloaded in a later operation. The exception is site continuing review supporting documents. Due to technical reasons, the standard approach we use to download potentially large sets of large documents without triggering a transaction timeout could not be implemented for the Report Continuing Review Data activity on sites. Given that these documents are not expected to be large sets or necessarily large documents, we chose to accept downloading the document content for these in this transaction.
- If documents still in the IRB Exchange were previously deleted, then we re-create them.
- Documents and attachments are linked to their IRB Exchange counterparts via the IrbExchangeReference type and it's associated methods. These links should be maintained for efficient updates.
- For _Attachment type, if wom.getEntityFromString is called for an entity that no longer exists, it will throw an exception, but this scenario is expected so we 'eat' the exception.
- Metadata on data changes is provided to aid in View Differences accuracy.
Downloading the Document Content
In most cases, we want to download document content in a way that reduces the chance the operation will cause a transaction timeout. This is why you will want to create a remote method that receives some JSON arguments and downloads the document content onto the appropriate Document entity.
ClickIRBRemoteMethods.downloadDocument Method Example
/** Downloads a document's document content from the IRB Exchange and sets it on the appropriate Document entity.
*
@param argSet (JSON) Contains the document and study poRefs
@return {string} Upon success, return the string "success"
**/
function downloadDocument(argSet)
{
// Parse the JSON string into JSON for our data
var args = JSON.parse(argSet, null);
// Get the document we're downloading the file onto
var document = wom.getEntityFromString(args.documentPoRef);
// Get the study whose container in the IRB Exchange has the file
var study = wom.getEntityFromString(args.study);
// Get an IRB Exchange account client connected using the account assigned on the admin office
var adminOffice = study.getQualifiedAttribute("customAttributes.IRB");
var accountName = adminOffice.getQualifiedAttribute("customAttributes.iRBExchangeAccountName");
if(accountName == null) {
throw new Error("Account group not set for admin office with ID " + adminOffice.ID);
}
var exchangeClient = ClickIRBUtils.getExchangeClient(accountName);
var documentExchangeId;
if(args.exchangeRef == null) {
// The IRB Exchange ID of the document wasn't provided here, so look it up
documentExchangeId = IrbExchangeReference.GetExchangeId(args.documentPoRef, exchangeClient + "", exchangeClient.GetEndpoint() + "");
} else {
// The IRB Exchange ID of the document was provided, so set it to our local variable
documentExchangeId = args.exchangeRef;
}
// Get the targetUrl (docProxy) from the IRB Exchange Client Libraries
var targetUrl = exchangeClient.GetDocument(study, document + "", documentExchangeId);
// The targetUrl was retrieved from the IRB Exchange
if(targetUrl != null) {
// Set the targetUrl onto the Document entity
document.setQualifiedAttribute("targetURL", targetUrl);
// Set the read access method on the targetUrl
targetUrl.addAccessValidationMethod(document, "userCanRead");
// Add history entry for document history feature
document.AddHistoryEntry("Downloaded from IRB Exchange", "", true);
// For setting targetUrl on Document (not Attachment) sets
if (args.entity != null) {
var entity = wom.getEntityFromString(args.entity);
entity.setQualifiedAttribute(args.attributePath, document, "add");
}
// If this is an update to existing submission, then note on the activity that data has changed
var updateFromExchangeActivity = args.updateFromExchangeActivity;
if(updateFromExchangeActivity != null) {
updateFromExchangeActivity = wom.getEntityFromString(updateFromExchangeActivity);
if(updateFromExchangeActivity.getQualifiedAttribute("customAttributes.hasDataChanged") == false) {
updateFromExchangeActivity.setQualifiedAttribute("customAttributes.hasDataChanged", true);
updateFromExchangeActivity.name = "Updated from IRB Exchange";
updateFromExchangeActivity.notesAsStr = null;
var incrementVersionActivity = ActivityType.getActivityType("_IRBSubmission_IncrementMinorVersion", "_IRBSubmission");
var sch = ShadowSCH.getRealOrShadowSCH();
var versionIncrement = updateFromExchangeActivity.loggedFor.logActivity(sch, incrementVersionActivity, Person.getCurrentUser());
versionIncrement.versionDescription = "Updated from IRB Exchange";
}
}
}
return "success";
}
Technical notes about the method example:
- The method takes a JSON argument that contains all the metadata it needs to do its job.
- This method doesn't do Attachment handling; it receives information about a draft or final Attachment or standalone Document and downloads the document content for that Document.
- In order to download the document content using the appropriate IRB Exchange account, it looks up the IRB office on the study and gets the IRB Exchange account based on the IRB Exchange account name selected on the IRB office and the store's default IRB Exchange endpoint.
- The IRB Exchange method GetDocument is called to get the document content (targetUrl attribute on Document).
- Since document operations with the IRB Exchange all happen in code, we need to set some requisite attributes on Document entities to make them whole, including access validation method and history entry.
- If data changed here that wasn't previously noted on the activity, then we note it on the activity now.