Skip to content

Commit 0041e3f

Browse files
authored
Merge pull request #11 from redcapuzgent/issue_10
Add method to change randomisation target
2 parents 663d0a5 + 880be49 commit 0041e3f

File tree

5 files changed

+272
-5
lines changed

5 files changed

+272
-5
lines changed

Randapi.php

Lines changed: 201 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ function getFreeAllocationRecordForTarget($criteriaFields,$fields=array(),$group
200200
*/
201201
function changeSources(string $recordId,int $projectId,$fields=array(),array $allocations,$group_id='',string $arm_name='Arm 1', string $event_name='Event 1'){
202202

203-
error_log("init randomize record");
203+
error_log("init change sources");
204204
$this->initRandomizeRecord($recordId, $projectId);
205205

206206
global $status;
@@ -248,7 +248,7 @@ function changeSources(string $recordId,int $projectId,$fields=array(),array $al
248248
error_log("Found a new aid $newAid");
249249
// No need to change the target_field value in redcap_data. This remains the same.
250250
if(!$this->query("update redcap_randomization_allocation set is_used_by = null where aid = '$currentAid' and is_used_by = '$recordId'")){
251-
throw new RandapiException("Could not unsed is_used_by for aid $currentAid");
251+
throw new RandapiException("Could not unset is_used_by for aid $currentAid");
252252
}
253253
error_log("updated is_used_by from old aid $currentAid to null");
254254
if(!$this->query("update redcap_randomization_allocation set is_used_by = '$recordId' where aid = $newAid;")){
@@ -292,6 +292,142 @@ function changeSources(string $recordId,int $projectId,$fields=array(),array $al
292292
}
293293
}
294294

295+
/**
296+
* In limited cases it might be necessary to change the outcome of the randomization. E.g. in an automated process a record was assigned to a certain target group.
297+
* Due to some manual changes, the record is unrandomized. Correcting the error and randomizing the record again, results in a different target value.
298+
* This can be done using the following steps
299+
* 1) Check if an allocation record is available for the given recorid
300+
* 2) Collect the current source_field value.
301+
* 3) Look for a new allocation record (aid) for the new source_field values but for the same target_field.
302+
* 4) Select the first free allocation. If none available, add new ones as defined in $newAllocations and select first (aid)
303+
* 5) Update is_used_by field for the current allocation to null, update the new allocation to the new found aid.
304+
* 6) Update the source field values in the record
305+
*
306+
* @param string $recordId
307+
* @param int $projectId
308+
* @param string $new_target The new target value
309+
* @param string $group_id
310+
* @param RandomizationAllocation[] $allocations These allocations will be added if no allocations are available for the given combination of source fields and the target field.
311+
* @param string $arm_name (optional) The name of the arm. default = 'Arm 1'
312+
* @param string $event_name (optional) The name of the event. default = 'Event 1'
313+
* @return int
314+
* @throws Exception
315+
*/
316+
function changeTarget(string $recordId,int $projectId,string $new_target,array $allocations,$group_id='',string $arm_name='Arm 1', string $event_name='Event 1'){
317+
318+
error_log("init change target");
319+
$this->initRandomizeRecord($recordId, $projectId);
320+
321+
global $status;
322+
323+
$recordId = db_real_escape_string($recordId);
324+
325+
/**
326+
* @var $source_fields array key value array with key source_fieldx and value the source_field's name
327+
*/
328+
$source_fields = Randomization::getRandomizationFields(false,true);
329+
330+
$source_field_columns = array_keys($source_fields);
331+
for($i = 0; $i < sizeof($source_field_columns); $i++){
332+
$source_field_columns[$i]="ra.".$source_field_columns[$i];
333+
}
334+
335+
// What is the current target_field value?
336+
$currentTargetFieldQuery = $this->query("SELECT ra.aid, ".implode(',',$source_field_columns)."
337+
FROM redcap_randomization r
338+
join redcap_randomization_allocation ra on
339+
ra.rid = r.rid and
340+
ra.is_used_by = '$recordId' and
341+
ra.project_status = $status
342+
where r.project_id = $projectId");
343+
if($row = $currentTargetFieldQuery->fetch_assoc()){
344+
345+
// copy of Randomization::randomizeRecord
346+
// Ensure that fields have all correct criteria fields. If not, return false to throw AJAX error msg.
347+
$criteriaFieldsOk = true;
348+
$criteriaFields = Randomization::getRandomizationFields(false,true);
349+
350+
//create RandomizationField objects
351+
/**
352+
* @var $source_fields_rf RandomizationField[]
353+
*/
354+
$source_fields_rf = array();
355+
foreach(array_keys($source_fields) as $source_field){
356+
$source_fields_rf[] = new RandomizationField($criteriaFields[$source_field], strval($row[$source_field]));
357+
}
358+
$currentAid = $row["aid"];
359+
360+
$source_fields_str = array();
361+
foreach($source_fields_rf as $rf){
362+
$source_fields_str[] = $rf->getKey()." => ".$rf->getValue();
363+
}
364+
365+
error_log("Received current source fields ".implode(",",$source_fields_str)." and current aid $currentAid");
366+
if (count($source_fields_rf) != count($criteriaFields)) $criteriaFieldsOk = false;
367+
foreach (array_keys($source_fields_rf) as $field) {
368+
if (!in_array($field, $criteriaFields)) $criteriaFieldsOk = false;
369+
}
370+
371+
if(!$criteriaFieldsOk){
372+
throw new Exception("The given criteria fields are not valid");
373+
}
374+
error_log("criteria fields are oké.");
375+
$newAid = $this->getFreeAllocationRecordForTarget($criteriaFields, $source_fields_rf, $group_id, $new_target);
376+
if($newAid == 0) {
377+
error_log("No allocations are present");
378+
if (is_array($allocations) && sizeof($allocations) > 0) {
379+
error_log("Adding new allocations");
380+
$this->addRecordsToAllocationTable($projectId, $status, $allocations);
381+
$newAid = $this->getFreeAllocationRecordForTarget($criteriaFields, $source_fields_rf, $group_id, $new_target);
382+
} else {
383+
throw new RandapiException('No free allocation was available and no allocations were passed through the $allocations argument');
384+
}
385+
}
386+
if($newAid != 0){
387+
error_log("Found a new aid $newAid");
388+
// No need to change the target_field value in redcap_data. This remains the same.
389+
if(!$this->query("update redcap_randomization_allocation set is_used_by = null where aid = '$currentAid' and is_used_by = '$recordId'")){
390+
throw new RandapiException("Could not unset is_used_by for aid $currentAid");
391+
}
392+
error_log("updated is_used_by from old aid $currentAid to null");
393+
if(!$this->query("update redcap_randomization_allocation set is_used_by = '$recordId' where aid = $newAid;")){
394+
throw new RandapiException("Could not set is_used_by for aid $newAid");
395+
}
396+
error_log("updated is_used_by from new aid $newAid to $recordId");
397+
// update the target field in the record
398+
399+
$randomization_fields = Randomization::getRandomizationFields(false,false);
400+
401+
$updateQuery = "
402+
update redcap_data rd
403+
join redcap_events_metadata md on
404+
md.event_id = rd.event_id and
405+
md.descrip = '$event_name'
406+
join redcap_events_arms a on
407+
a.arm_id = md.arm_id and
408+
a.arm_name = '$arm_name'
409+
join redcap_randomization_allocation newa on newa.aid = $newAid
410+
set rd.value = newa.target_field
411+
where rd.project_id = $projectId and
412+
rd.record = '$recordId' and
413+
rd.field_name = '".$randomization_fields["target_field"]."'
414+
";
415+
error_log("Executing query $updateQuery");
416+
if(!$this->query($updateQuery)){
417+
throw new RandapiException("Could not update target_field to value '".$new_target."' for project $projectId, record $recordId, event $event_name and arm $arm_name from aid $currentAid to aid $newAid");
418+
}else{
419+
error_log("updated target_field to value '".$new_target."' for project $projectId, record $recordId, event $event_name and arm $arm_name from aid $currentAid to aid $newAid");
420+
}
421+
return $newAid;
422+
}else{
423+
throw new RandapiException("Could not find an empty allocation record for the given target and source fields");
424+
}
425+
426+
}else{
427+
throw new RandapiException("Record $recordId has not yet been randomized");
428+
}
429+
}
430+
295431
/**
296432
* @param int $projectId The project id
297433
* @param int $project_status (0 = development, 1 = production)
@@ -509,6 +645,10 @@ public function handleRequest(stdClass $jsonObject, string $jsonText):void{
509645
$newAid = $this->handleChangeSources($jsonObject);
510646
echo json_encode($newAid);
511647
break;
648+
case "changeTarget":
649+
$newAid = $this->handleChangeTarget($jsonObject);
650+
echo json_encode($newAid);
651+
break;
512652
default:
513653
throw new RandapiException("Invalid Action was specified");
514654
}
@@ -709,4 +849,63 @@ private function handleChangeSources(stdClass $jsonObject){
709849
$eventName);
710850
}
711851

852+
/**
853+
* @param stdClass $jsonObject
854+
* @return int
855+
* @throws RandapiException
856+
*/
857+
private function handleChangeTarget(stdClass $jsonObject){
858+
859+
error_log("Received parameters in changeTarget: ".print_r($jsonObject,true));
860+
861+
if(!property_exists($jsonObject,"parameters")){
862+
error_log("parameters property not found.");
863+
throw new RandapiException("parameters property not found.");
864+
}
865+
if(!property_exists($jsonObject->parameters, "recordId")){
866+
error_log("parameters->recordId property not found.");
867+
throw new RandapiException("parameters->recordId property not found.");
868+
}
869+
if(!property_exists($jsonObject->parameters, "target")){
870+
error_log("parameters->fields property not found.");
871+
throw new RandapiException("parameters->fields property not found.");
872+
}
873+
if(!property_exists($jsonObject->parameters, "allocations")){
874+
error_log("parameters->allocations property not found.");
875+
throw new RandapiException("parameters->allocations property not found.");
876+
}
877+
if(!is_array($jsonObject->parameters->allocations)){
878+
error_log("parameters->allocations is not an array.");
879+
throw new RandapiException("parameters->allocations is not an array.");
880+
}
881+
882+
// optional
883+
$groupId = "";
884+
if(property_exists($jsonObject->parameters, "groupId")){
885+
$groupId = $jsonObject->parameters->groupId;
886+
}
887+
$armName = "Arm 1";
888+
if(property_exists($jsonObject->parameters, "armName")){
889+
$groupId = $jsonObject->parameters->armName;
890+
}
891+
$eventName = "Event 1";
892+
if(property_exists($jsonObject->parameters, "eventName")){
893+
$eventName = $jsonObject->parameters->eventName;
894+
}
895+
896+
$allocations = array();
897+
foreach($jsonObject->parameters->allocations as $allocation){
898+
array_push($allocations,RandomizationAllocation::fromstdClass($allocation));
899+
}
900+
901+
error_log("executing changeTarget");
902+
return $this->changeTarget($jsonObject->parameters->recordId,
903+
$this->getProjectId(),
904+
$jsonObject->parameters->target,
905+
$allocations,
906+
$groupId,
907+
$armName,
908+
$eventName);
909+
}
910+
712911
}

help.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,42 @@
167167
}
168168
</code>
169169
</pre>
170+
171+
<h2>changeTarget</h2>
172+
<p>In limited cases it might be necessary to change the outcome of the randomization. E.g. in an automated process a record was assigned to a certain target group. Due to some manual changes, the record is unrandomized. Correcting the error and randomizing the record again, results in a different target value.</p>
173+
<p>If possible, another allocation record for the preferred target will be used that maintains the current assigned sources.</p>
174+
<p>If no such records exist, there is a possibility to provide additional allocation records</p>
175+
176+
<h3>parameters:</h3>
177+
<ul>
178+
<li><b>recordId</b>: The record that we want to change source fields for</li>
179+
<li><b>target</b>: The new target value</li>
180+
<li><b>allocations</b>: A list of new allocation records, in case there is no record available anymore.
181+
<li><b>groupId</b>: (optional) The DAG identifier. default = '' (none)</li>
182+
<li><b>armName</b>: (optional) The name of the arm. default = 'Arm 1'</li>
183+
<li><b>eventName</b>: (optional) The name of the event. default = 'Event 1'</li>
184+
</ul>
185+
<h3>Example:</h3>
186+
<p>This examples changes the target for record 1 to assignedto A. In case there is no allocation record available anymore, we allow the algorithm to add these 4 new allocations.</p>
187+
188+
<pre>
189+
<code>
190+
{
191+
"action":"changeTarget",
192+
"token":"F33F6876ADC5EC63CE79EBFF88FF0092",
193+
"parameters":{
194+
"recordId":1,
195+
"target":'A',
196+
"allocations":[
197+
{"source_fields":["2"],"target_field":"1"},
198+
{"source_fields":["2"],"target_field":"2"},
199+
{"source_fields":["2"],"target_field":"1"},
200+
{"source_fields":["2"],"target_field":"2"},
201+
]
202+
}
203+
}
204+
</code>
205+
</pre>
170206
</div>
171207
</body>
172208
</html>

typescript/ChangeSourcesParameters.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,16 @@ export class ChangeSourcesParameters{
77
*
88
* @param recordId The record that we want to randomize
99
* @param fields An array of RandomizationFields
10-
* @param resultFieldName The field where the randomization result can be stored.
10+
* @param allocations New allocations in case none are available for the given fields and current target
1111
* @param groupId (optional) The DAG identifier. default = '' (none)
1212
* @param armName (optional) The name of the arm. default = 'Arm 1'
1313
* @param eventName (optional) The name of the event. default = 'Event 1'
1414
*/
15-
constructor(public recordId: string, public fields: RandomizationField[],
15+
constructor(public recordId: string,
16+
public fields: RandomizationField[],
1617
public allocations: RandomizationAllocation[],
1718
public groupId: string = '',
18-
public armName: string= 'Arm 1', public eventName: string = 'Event 1') {
19+
public armName: string= 'Arm 1',
20+
public eventName: string = 'Event 1') {
1921
}
2022
}

typescript/ChangeTargetAction.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import {RandApiAction} from "./RandApiAction";
2+
import {ChangeTargetParameters} from "./ChangeTargetParameters";
3+
4+
export class ChangeTargetAction extends RandApiAction{
5+
6+
constructor(public parameters:ChangeTargetParameters, public token: string){
7+
super("changeTarget",token, parameters);
8+
}
9+
}

typescript/ChangeTargetParameters.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import {RandomizationAllocation} from "./RandomizationAllocation";
2+
3+
export class ChangeTargetParameters{
4+
5+
/**
6+
*
7+
* @param recordId The record that we want to randomize
8+
* @param target The new target
9+
* @param allocations allocations New allocations in case none are available for the given target and current sources
10+
* @param groupId (optional) The DAG identifier. default = '' (none)
11+
* @param armName (optional) The name of the arm. default = 'Arm 1'
12+
* @param eventName (optional) The name of the event. default = 'Event 1'
13+
*/
14+
constructor(public recordId: string,
15+
public target: string,
16+
public allocations: RandomizationAllocation[],
17+
public groupId: string = '',
18+
public armName: string= 'Arm 1',
19+
public eventName: string = 'Event 1') {
20+
}
21+
}

0 commit comments

Comments
 (0)