Skip to content

Commit

Permalink
Adds support for multiple sequences, longitudinal projects
Browse files Browse the repository at this point in the history
  • Loading branch information
jangari committed Nov 20, 2022
1 parent a2d1f42 commit 8c80adf
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 79 deletions.
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# REDCap Survey Shuffle

This REDCap External Module allows users to shuffle surveys such that they are displayed to respondents in a random order.
This REDCap External Module allows users to shuffle surveys such that they are displayed to respondents in a random order, and the order in which they were displayed can be stored in a text variable..

## Installation

Expand All @@ -14,23 +14,23 @@ Clone the repository and rename the directory to include a version number, e.g.,

## Usage

In the module configuration, add instruments to be shuffled from the drop-down menu. The surveys should be contiguous or weird things may happen. If the `prior-survey` option is set in the configuration, randomisation will commence on completion of this prior survey, otherwise randomisation commences upon completion of the first instrument. Since this module makes use of the `redcap_survey_complete` hook, randomisation of instruments cannot take place until at least one survey is completed.
In the module configuration, add instruments to be shuffled from the drop-down menu. The surveys should be contiguous or weird things may happen. If the `entry-survey` option is set in the configuration, randomisation will commence on completion of this entry survey, otherwise randomisation commences upon completion of the first instrument. Since this module makes use of the `redcap_survey_complete` hook, randomisation of instruments cannot take place until at least one survey is completed, and as such, the first survey cannot be shuffled..

An `exit-survey` configuration option allows users to direct respondents to a single survey upon completion of the battery of shuffled surveys. If this is not set, then the survey termination option of the final displayed instrument takes effect.

Users can set a number of instruments to be displayed. For example, in a battery of ten survey instruments, respondents can be administered a random 5 before being directed to the `exit-survey` (or, the survey termination option of the fifth displayed survey). In another example, randomly displaying one of two instruments can allow for A/B testing.

Just which survey instruments were administered can be seen by looking at the instruments' complete status fields, but the order in which they were administered can be stored in a text field using the `sequence-field` configuration option in the format `form_3, form_1, form_2, `. Typically, this field would be `@HIDDEN` and `@READONLY`.
Just which survey instruments were administered can be seen by looking at the instruments' complete status fields, but the order in which they were administered can be stored in a text field using the `sequence-field` configuration option in the format `form_3, form_1, form_2`. Typically, this field would be `@HIDDEN` and `@READONLY`. Using `@SETVALUE` on a checkbox field with options coded as the unique names can effectively store the displayed instruments as an enumerated field that is then available for data quality checking and reporting.

## Limitations
Multiple sequences can be configured and sequences can be limited to specific longitudinal events. This allows for a different battery of surveys in different events, as required. It also allows for a single battery of surveys comprising separately shuffled sections. Users may need to configure a bridge instrument to act as the `exit_survey` of one shuffled sequence and the `entry_survey` of the next, otherwise respondents may not be directed through the instruments correctly.

Currently this module is not event-aware and has not been tested in a longitudinal context.
## Limitations

Similarly, there is no support for repeating instruments, and I'm not even sure how that would even work.
There is no support for repeating instruments. If one of the shuffled instruments is repeating and configured to allow the respondent to re-take on submission, the respondent is not directed to the next shuffled survey, but instead is directed to a new instance of the repeating instrument.

The method used to randomly select a survey does not allow for balancing the administration of surveys across a population; each survey page is only aware of which surveys are not yet completed for the current record, and not how many times each survey has been taken.

## TODO

- Investigate support for longitudinal projects and repeating instruments.
- Investigate support for repeating instruments.
- Allow for surveys to be administered in a pre-determined order by reading the sequence field, that is, administering surveys in an order defined elsewhere. Or maybe that's a different module entirely.
94 changes: 50 additions & 44 deletions SurveyShuffle.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,58 +3,64 @@
namespace INTERSECT\SurveyShuffle;

use ExternalModules\AbstractExternalModule;
use Project;
use REDCap;

class SurveyShuffle extends \ExternalModules\AbstractExternalModule {

function redcap_survey_complete($project_id, $record, $instrument, $event_id, $group_id, $survey_hash, $response_id, $repeat_instance)
/* function redcap_survey_page($project_id, $record, $instrument, $event_id, $group_id, $survey_hash, $response_id, $repeat_instance) // For debugging */
{
//Retrieve module configuration settings
$shuffle_instruments = $this -> getProjectSetting('shuffle-instruments');
$shuffle_number = $this -> getProjectSetting('shuffle-number');
$exit_survey = $this -> getProjectSetting('exit-survey');
$prior_survey = $this -> getProjectSetting('prior-survey');
$sequence_field = $this -> getProjectSetting('sequence-field');

// Set the number of instruments to shuffle to the number requested, or else the number of instruments being shuffled
$shuffle_number = (!is_null($shuffle_number) && is_numeric($shuffle_number) && $shuffle_number > 0) ? $shuffle_number : count($shuffle_instruments);

// Since this module fires on the survey_complete hook, we have to know if the survey that was just completed was one that should then go to a random next one. Those are: the list of shuffled surveys, the prior survey is it is set, or the first survey if it is not.
$trigger_instruments = $shuffle_instruments;
$first_instrument = array_key_first(REDCap::getInstrumentNames());
(is_null($prior_survey)) ? array_push($trigger_instruments,$first_instrument) : array_push($trigger_instruments,$prior_survey);

if (in_array($instrument,$trigger_instruments)) { // Stop if the current instrument should *not* lead to a shuffled instrument

$completed_shuffle_instruments = array(); // Instantiate an array for completed instruments

// Loop through shuffle instruments and remove those that are complete, adding them to the completed instruments array
foreach($shuffle_instruments as $inst) {
if (REDCap::getData('array',$record,$inst.'_complete',$event_id)[$record][$event_id][$inst.'_complete'] == 2) {
unset($shuffle_instruments[array_search($inst,$shuffle_instruments)]);
array_push($completed_shuffle_instruments, $inst);
$configs = $this -> getProjectSetting('configs');

for ($i = 0; $i < count($configs); $i++){ // Loop over each new configuration
//Retrieve module configuration settings
$shuffle_instruments = $this -> getProjectSetting('shuffle-instruments')[$i];
$shuffle_number = $this -> getProjectSetting('shuffle-number')[$i];
$exit_survey = $this -> getProjectSetting('exit-survey')[$i];
$entry_survey = $this -> getProjectSetting('entry-survey')[$i];
$sequence_field = $this -> getProjectSetting('sequence-field')[$i];
$shuffle_event = $this -> getProjectSetting('shuffle-event')[$i];

if (is_null($shuffle_event) || $event_id == $shuffle_event ) { // proceed if the current event is the config event, or if no event is specified
// Set the number of instruments to shuffle to the number requested, or else the number of instruments being shuffled
$shuffle_number = (!is_null($shuffle_number) && is_numeric($shuffle_number) && $shuffle_number > 0) ? $shuffle_number : count($shuffle_instruments);

// Since this module fires on the survey_complete hook, we have to know if the survey that was just completed was one that should then go to a random next one. Those are: the list of shuffled surveys, the entry survey is it is set, or the first survey if it is not.
$trigger_instruments = $shuffle_instruments;
$first_instrument = array_key_first(REDCap::getInstrumentNames());
(is_null($entry_survey)) ? array_push($trigger_instruments,$first_instrument) : array_push($trigger_instruments,$entry_survey);

if (in_array($instrument,$trigger_instruments)) { // Stop if the current instrument should *not* lead to a shuffled instrument

$completed_shuffle_instruments = array(); // Instantiate an array for completed instruments

// Loop through shuffle instruments and remove those that are complete, adding them to the completed instruments array
foreach($shuffle_instruments as $inst) {
if (REDCap::getData('array',$record,$inst.'_complete',$event_id)[$record][$event_id][$inst.'_complete'] == 2) {
unset($shuffle_instruments[array_search($inst,$shuffle_instruments)]);
array_push($completed_shuffle_instruments, $inst);
};
}
if (count($completed_shuffle_instruments) < $shuffle_number) { // If we're not done displaying surveys
shuffle($shuffle_instruments);
$next_survey = $shuffle_instruments[0]; // Pick a remaining instrument at random
if (!is_null($sequence_field)) { // If we're planning on storing the sequence in a field, let's do so.
$sequence_data_curr = REDCap::getData('array',$record,$sequence_field,$event_id)[$record][$event_id][$sequence_field];
$sequence_new_data = (strlen($sequence_data_curr) == 0) ? $next_survey : $sequence_data_curr.", ".$next_survey;
$this->setData($record,$sequence_field,$sequence_new_data);
};
$next_survey_link = REDCap::getSurveyLink($record, $next_survey, $event_id); // Get the next survey's link
header('Location: '.$next_survey_link); // And direct the respondent there
$this->exitAfterHook();
} else { // If we're done
if (!is_null($exit_survey)) { // Test if there's a configured end-survey
$exit_survey_link = REDCap::getSurveyLink($record, $exit_survey, $event_id);
header('Location: '.$exit_survey_link); // Go there
$this->exitAfterHook();
};
}; // Otherwise do whatever the survey termination options have configured.
};
}
if (count($completed_shuffle_instruments) < $shuffle_number) { // If we're not done displaying surveys
shuffle($shuffle_instruments);
$next_survey = $shuffle_instruments[0]; // Pick a remaining instrument at random
if (!is_null($sequence_field)) { // If we're planning on storing the sequence in a field, let's do so.
$sequence_data_curr = REDCap::getData('array',$record,$sequence_field,$event_id)[$record][$event_id][$sequence_field];
$sequence_new_data = (strlen($sequence_data_curr) == 0) ? $next_survey : $sequence_data_curr.", ".$next_survey;
$this->setData($record,$sequence_field,$sequence_new_data);
};
$next_survey_link = REDCap::getSurveyLink($record, $next_survey); // Get the next survey's link
header('Location: '.$next_survey_link); // And direct the respondent there
$this->exitAfterHook();
} else { // If we're done
if (!is_null($exit_survey)) { // Test if there's a configured end-survey
$exit_survey_link = REDCap::getSurveyLink($record, $exit_survey);
header('Location: '.$exit_survey_link); // Go there
$this->exitAfterHook();
};
}; // Otherwise do whatever the survey termination options have configured.
};
}
}
}
79 changes: 51 additions & 28 deletions config.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
{
"name": "Survey Shuffle",
"namespace": "INTERSECT\\SurveyShuffle",
"description": "Shuffles Surveys; Displays selected surveys to respondents in a random order. Displayed sequence can be stored in a text field. The number of displayed surveys can be configured to be less than the number of survey instruments being shuffled.",
"description": "Shuffles Surveys; Displays selected surveys to respondents in a random order.",
"framework-version": 7,
"compatibility": {
"php-version-min": "7.3.0",
"redcap-version-min": "11.4.0"
},
"permissions": [
"redcap_survey_complete"
"redcap_survey_complete",
"redcap_survey_page"
],
"authors": [
{
Expand All @@ -14,32 +19,50 @@
}
],
"project-settings": [
{
"key": "shuffle-instruments",
"name": "Instruments to be shuffled (initial instrument cannot be shuffled)",
"type": "form-list",
"repeatable": true
},
{
"key": "shuffle-number",
"name": "Number of instruments to display (if empty, less than 1, more than the number of selected instruments, or NaN, then all instruments will be displayed)",
"type": "text"
},
{
"key": "prior-survey",
"name": "Survey whose submission triggers the random display of shuffle instruments (if unset, shuffle instruments will begin displaying after the submission of the initial survey, which itself cannot be shuffled).",
"type": "form-list"
},
{
"key": "exit-survey",
"name": "Survey to which to direct respondents when all shuffled surveys have been displayed (if unset, survey terminations option of last displayed survey will take effect)",
"type": "form-list"
},
{
"key": "sequence-field",
"name": "Field to be used to store survey display order (typically hidden and readonly)",
"type": "field-list",
"field-type": "text"
{
"key": "configs",
"name": "Sequence configuration",
"type": "sub_settings",
"repeatable": true,
"sub_settings": [
{
"key": "shuffle-event",
"name": "Event<br><em>Leave blank if not longitudinal or to apply to all events</em>",
"type": "event-list"
},
{
"key": "entry-survey",
"name": "Entry survey; survey whose submission triggers the random display of shuffle instruments<br/><em>If unset, shuffled instruments will begin displaying after the submission of the initial survey, which itself cannot be shuffled</em>.",
"type": "form-list"
},
{
"key": "descriptive-text",
"name": "Instruments to be shuffled<br/> <em>initial instrument cannot be shuffled</em>",
"type": "descriptive"
},
{
"key": "shuffle-instruments",
"name": "Instrument",
"type": "form-list",
"repeatable": true
},
{
"key": "shuffle-number",
"name": "Number of instruments to display<br/><em>if empty, less than 1, more than the number of selected instruments, or NaN, then all instruments will be displayed</em>",
"type": "text"
},
{
"key": "exit-survey",
"name": "Exit survey; survey to which to direct respondents when all shuffled surveys have been displayed<br/><em>if unset, survey terminations option of last displayed survey will take effect</em>",
"type": "form-list"
},
{
"key": "sequence-field",
"name": "Field to be used to store survey sequence<br/><em>typically hidden and readonly; must be designated on the current event if longitudinal</em>",
"type": "field-list",
"field-type": "text"
}
]
}
]
}

0 comments on commit 8c80adf

Please sign in to comment.