diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..df39227 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.un~ +*.sw? diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000..66aa656 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,10 @@ +cff-version: 1.2.0 +message: "If you use this software, please cite it as below." +authors: +- family-names: "Wilson" + given-names: "Aidan" + orcid: "https://orcid.org/0000-0001-9858-5470" +title: "Requested and Required Fields" +doi: +date-released: 2024-06-17 +url: "https://github.com/jangari/redcap_requested_and_required_fields" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c34f796 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Aidan Wilson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..454b8c3 --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# Requested and Required Fields + +This REDCap External Module provides functionality for requesting that respondents provide an answer to a field, and displays a warning to survey respondents if any requested fields are missing a value when they try to submit. For completeness, this module also allows required fields to be treated in the same way. Fields can be annotated with @REQUESTED or @REQUIRED, both of which take an optional description, which is shown to the user when they try to submit. If the description is omitted, the field label is shown instead. + +![screencast](screencast.gif) + +This module respects fields marked @HIDDEN(-SURVEY) and also those that are hidden due to branching logic. + +## Limitations + +This module does not play nicely with embedded fields. Perhaps this could be fixed, but at the moment it's just a limitation. + +Unlike traditional required fields, wherein submitting the page commits other values to the database and sets the survey as partially complete, with this module enabled, clicking 'submit' does _not_ save any other entered values. Again, perhaps this could be fixed by running an AJAX call to save the data. + +This module only considers fields to be required if they are annotated with @REQUIRED. It might be an idea in future to take fields marked Required in the designer and treat them in the same way. + +## Installation + +Install the module from the REDCap module repository and enable in the Control Center, then enable on projects as needed. + +## Usage + +This module adds two action tags: + +| Action Tag | Description | +| --- | --- | +| @REQUESTED | Displays a modal window if the annotated field is empty when the respondent attempts to submit, but allows the respondent to submit regardless (unless there are @REQUIRED fields). With a description provided by @REQUESTED="description", the description is shown in the modal. Otherwise, the field label is shown. | +| @REQUIRED | Displays a modal window if the annotated field is empty when the respondent attempts to submit, and prevents submission. With a description provided by @REQUIRED="description", the description is shown in the modal. Otherwise, the field label is shown. | + +## Configuration + +This module can be configured with the following project settings: + +| Setting | Default Value | Description | +| --- | --- | --- | +| Modal title | "Action Required!" | Title of the popup window showing any requested or required fields. | +| Requested text | "The following fields are requested, although you may submit without completing them:" | Text displayed in the modal window above listed requested fields. | +| Required text | "The following fields are required:" | Text displayed in the modal window above listed requested fields. | +| Footer text (no required fields) | "The following fields are requested, although you may submit without completing them:" | Text displayed at the end of the modal, above the buttons, where no required fields are missing. | +| Footer text (required fields) | "The following fields are required:" | Text displayed at the end of the modal, above the buttons, where required fields are missing. | +| Cancel button text | "Review Response" | Text for cancel button. | +| Submit button text | "Submit Now" | Text for submit button. | +| Highlight fields after displaying warning? | false | If true, highlights required and requested fields after cancelling the modal window. | +| Highlight colour for requested fields | "#d2e0ff" (light blue) | Colour used to highlight requested fields. | +| Highlight colour for required fields | "#ffd2e0" (light red) | Colour used to highlight required fields. | +| Disable green highlight | false | Disable the default green highlighting on all fields, as this will visually conflict with the highlighting added by this module. | +| Label requested fields | false | Display a label on requested fields. | +| Requested field label text | "* response requested" | Text for requested field label. | +| Requested field label colour | "#0000ff" (blue) | Colour for requested field label. | diff --git a/RequestedAndRequiredFields.php b/RequestedAndRequiredFields.php new file mode 100644 index 0000000..d2f118b --- /dev/null +++ b/RequestedAndRequiredFields.php @@ -0,0 +1,271 @@ +getTags($tags, $fields=NULL, $instruments=$instrument); + + // Extract field names and build the new structure, with fieldType and description (or label if unset in the tag) + $requestedFields = []; + foreach (array_keys($annotatedFields[$requestedTag]) as $fieldName) { + $fieldType = REDCap::getFieldType($fieldName); + $description = trim($annotatedFields[$requestedTag][$fieldName][0], '"'); + if (strlen($description) == 0) $description = $this->getFieldLabel($fieldName); + $requestedFields[$fieldName] = [ + 'type' => $fieldType, + 'description' => $this->escape($description) + ]; + }; + $requiredFields = []; + foreach (array_keys($annotatedFields[$requiredTag]) as $fieldName) { + $fieldType = REDCap::getFieldType($fieldName); + $description = trim($annotatedFields[$requiredTag][$fieldName][0], '"'); + if (strlen($description) == 0) $description = $this->getFieldLabel($fieldName); + $requiredFields[$fieldName] = [ + 'type' => $fieldType, + 'description' => $this->escape($description) + ]; + }; + + // Collect project settings + $settings = $this->getProjectSettings(); + + // Language defaults + $settings['modal-title'] = $settings['modal-title'] ?? $this->tt('modal-title-text'); + $settings['modal-requested-header'] = $settings['modal-requested-header'] ?? $this->tt('modal-requested-header-text'); + $settings['modal-required-header'] = $settings['modal-required-header'] ?? $this->tt('modal-required-header-text'); + $settings['modal-footer-norequired'] = $settings['modal-footer-norequired'] ?? $this->tt('modal-footer-norequired-text'); + $settings['modal-footer-required'] = $settings['modal-footer-required'] ?? $this->tt('modal-footer-required-text'); + $settings['requested-label'] = $settings['requested-label'] ?? $this->tt('requested-label-text'); + $settings['modal-cancel'] = $settings['modal-cancel'] ?? $this->tt('modal-cancel-text'); + $settings['modal-submit'] = $settings['modal-submit'] ?? $this->tt('modal-submit-text'); + + // Colour defaults + $settings['requested-hlcolour'] = $settings['requested-hlcolour'] ?? $this->tt('requested-hlcolour-hex'); + $settings['required-hlcolour'] = $settings['required-hlcolour'] ?? $this->tt('required-hlcolour-hex'); + $settings['requested-label-colour'] = $settings['requested-label-colour'] ?? $this->tt('requested-label-colour-hex'); + + echo ""; + + if ($settings['highlight']) { + echo ""; + } + + echo ""; + if ($settings['show-requested']) { + echo " + "; + } + if ($settings['disable-greenhl']) { + echo ""; + } + } +} diff --git a/classes/ActionTagHelper.php b/classes/ActionTagHelper.php new file mode 100644 index 0000000..db0852e --- /dev/null +++ b/classes/ActionTagHelper.php @@ -0,0 +1,149 @@ + array( + * "field_name" => array( + * "params" => any parameters next to tag (supports string, list, or json + * ) + * ) + */ + static function getActionTags($tags = NULL, $fields = NULL, $instruments = NULL) { + + // Check to see if this search has been cached + $arg_key = md5(json_encode(func_get_args())); + if (isset(self::$cache[$arg_key])) { + // \Plugin::log($arg_key, "DEBUG", "Using Cache"); + return self::$cache[$arg_key]; + } + + // Convert tag_filter into uppercase array + if (!empty($tags)) { + if (!is_array($tags)) $tags = array($tags); + $tags = array_map('strtoupper', $tags); + } + // \Plugin::log($tag_filter,"DEBUG","tag filter "); + + // Get the metadata with applied filters + $q = REDCap::getDataDictionary('json', false, $fields, $instruments); + $metadata = json_decode($q,true); + // \Plugin::log($metadata,"DEBUG","Metadata"); + + // Build a backwards-compatible action_tag array + $action_tags = array(); + foreach ($metadata as $field) { + $field_name = $field['field_name']; + $field_annotation = $field['field_annotation']; + $parsed_tags = self::parseActionTags($field_annotation); + // \Plugin::log($tags, "DEBUG", "TAGS for $field_name"); + foreach ($parsed_tags as $tag) { + // All action-tags should be parsed as uppercase + $action_tag = strtoupper($tag['actiontag']); + + // If we are filtering, skip non-specified tags + if ($tags AND !in_array($action_tag, $tags)) continue; + + // Initialize the action_tag node + if (!isset($action_tags[$action_tag])) $action_tags[$action_tag] = array(); + + $action_tags[$action_tag][$field_name][] = isset($tag['params']) ? $tag['params'] : ""; + } + } + + // Cache this search + self::$cache[$arg_key] = $action_tags; + + return $action_tags; + } + /** + * Parses a string for arrays of actiontags (optionally filtering by the supplied tag) + * Examples of valid action tags are: + * @TAG1 + * @TAG2=1,2,3 + * @TAG3={ + * "key":"value", + * "key2","value2" + * } + * + * The results are an array where each match is an actiontag with a key of + * [0] => [ + * "actiontag" => "@TAG1" + * ], + * [1] => [ + * "actiontag" => "@TAG2", + * "params" => "1,2,3" + * ], + * [2] => [ + * "actiontag" => "@TAG3", + * "params" => "{"key":"value","key2","value2"} + * + * https://regex101.com/r/fL2rM8/5 + * + * @param $string The string to be parsed for actiontags (in the format of @FOO=BAR or @FOO={"param":"bar"} + * @param null $tag_only If you wish to select a single tag + * @return array|bool returns the match array with the key equal to the tag and an array containing keys of 'params, params_json and params_text' + */ + static function parseActionTags($string, $tag_only = null) { + $re = "/(?(DEFINE) + (? -? (?= [1-9]|0(?!\d) ) \d+ (\.\d+)? ([eE] [+-]? \d+)? ) + (? true | false | null ) + (? \" ([^\"\\\\]* | \\\\ [\"\\\\bfnrt\/] | \\\\ u [0-9a-f]{4} )* \" ) + (? \[ (?: (?&json) (?: , (?&json) )* )? \s* \] ) + (? \s* (?&string) \s* : (?&json) ) + (? \{ (?: (?&pair) (?: , (?&pair) )* )? \s* \} ) + (? [a-zA-Z0-9\_\-]+ ) + (? \s* (?: (?&number) | (?&boolean) | (?&string) | (?&array) | (?&object) ) ) + (? (?: (?&fieldname) (?: , (?&fieldname) )+ )+ ) + ) + (?'actiontag' + \@(?&fieldname) + ) + (?:\= + (?'params' + (?: + (?'match_list'(?&fieldlist)) + | + (?'match_json'(?&json)) + | + (?'match_string'(?:[[:alnum:]\_\-]+)) + ) + ) + )?/ixm"; + + preg_match_all($re, $string, $matches); + + // Return false if none are found + if (count($matches['actiontag']) == 0) return false; + + $results = array(); + + foreach ($matches['actiontag'] as $i => $tag) { + $tag = strtoupper($tag); + if ($tag_only && ($tag != strtoupper($tag_only))) continue; + $results[] = array( + 'actiontag' => $tag, + 'params' => $matches['params'][$i] + ); + } + return $results; + } +} diff --git a/config.json b/config.json new file mode 100644 index 0000000..47fdeb4 --- /dev/null +++ b/config.json @@ -0,0 +1,131 @@ +{ + "name": "Requested and Required Fields", + "tt_name": "name", + "namespace": "INTERSECT\\RequestedAndRequiredFields", + "description": "Provides functionality for requesting a response to a field, and displays a warning to survey respondents if any requested fields are missing a value. For completeness, this module also allows required fields to be treated in the same way. Fields can be annotated with @REQUESTED or @REQUIRED, both of which take an optional description, which is shown to the user. If the description is omitted, the field label is shown instead.", + "tt_description": "description", + "framework-version": 14, + "authors": [ + { + "name": "Aidan Wilson", + "email": "aidan.wilson@intersect.org.au", + "institution": "Intersect Australia" + } + ], + "action-tags": [ + { + "tag": "@REQUESTED", + "description": "Displays a modal window if the annotated field is empty when the respondent attempts to submit, but allows the respondent to submit regardless (unless there are @REQUIRED fields). With a description provided by @REQUESTED=\"description\", the description is shown in the modal. Otherwise, the field label is shown." + }, + { + "tag": "@REQUIRED", + "description": "Displays a modal window if the annotated field is empty when the respondent attempts to submit, and prevents submission. With a description provided by @REQUIRED=\"description\", the description is shown in the modal. Otherwise, the field label is shown." + } + ], + "project-settings": [ + { + "key": "modal-title", + "tt_name": "modal-title", + "name": "Modal title
Title of window that pops up alerting the user to missing fields.
Default: Action Required!", + "type": "text" + }, + { + "key": "modal-requested-header", + "tt_name": "modal-requested-header", + "name": "Requested text
Default: The following fields are requested, although you may submit without completing them:", + "type": "text" + }, + { + "key": "modal-required-header", + "tt_name": "modal-required-header", + "name": "Required text
Default: The following fields are required:", + "type": "text" + }, + { + "key": "modal-footer-norequired", + "tt_name": "modal-footer-norequired", + "name": "Footer text (no required fields)
Default: Do you want to proceed with the submission?", + "type": "text" + }, + { + "key": "modal-footer-required", + "tt_name": "modal-footer-required", + "name": "Footer text (required fields)
Default: You must complete all required fields before continuing.", + "type": "text" + }, + { + "key": "modal-cancel", + "tt_name": "modal-cancel", + "name": "Cancel button text
Default: Review Response", + "type": "text" + }, + { + "key": "modal-submit", + "tt_name": "modal-submit", + "name": "Submit button text
Default: Submit Now", + "type": "text" + }, + { + "key": "highlight", + "tt_name": "highlight", + "name": "Highlight fields after displaying warning?", + "type": "checkbox" + }, + { + "key": "requested-hlcolour", + "tt_name": "requested-hlcolour", + "name": "Highlight colour for requested fields
Default: light blue", + "branchingLogic": { + "field": "highlight", + "value": true + }, + "type": "color-picker" + }, + { + "key": "required-hlcolour", + "tt_name": "required-hlcolour", + "name": "Highlight colour for required fields
Default: light red", + "branchingLogic": { + "field": "highlight", + "value": true + }, + "type": "color-picker" + }, + { + "key": "disable-greenhl", + "tt_name": "disable-greenhl", + "name": "Disable green highlight
This can conflict visually with requested and required field highlighting", + "branchingLogic": { + "field": "highlight", + "value": true + }, + "type": "checkbox" + }, + { + "key": "show-requested", + "tt-name": "show-requested", + "name": "Label requested fields", + "type": "checkbox" + }, + { + "key": "requested-label", + "tt_name": "requested-label", + "name": "Requested field label text
Default: * response requested", + "branchingLogic": { + "field": "show-requested", + "value": true + }, + "type": "text" + }, + { + "key": "requested-label-colour", + "tt_name": "requested-label-colour", + "name": "Requested field label colour
Default: blue", + "branchingLogic": { + "field": "show-requested", + "value": true + }, + "type": "color-picker" + } + ] +} diff --git a/lang/English.ini b/lang/English.ini new file mode 100644 index 0000000..a4f9e2a --- /dev/null +++ b/lang/English.ini @@ -0,0 +1,34 @@ +; Module name and description +name = "Requested and Required Fields" +description = "Provides functionality for requesting a response to a field, and displays a warning to survey respondents if any requested fields are missing a value. For completeness, this module also allows required fields to be treated in the same way. Fields can be annotated with @REQUESTED or @REQUIRED, both of which take an optional description, which is shown to the user. If the description is omitted, the field label is shown instead." + +; Configuration settings +modal-title = "Modal title
Title of window that pops up alerting the user to missing fields.
Default: Action Required!" +modal-requested-header = "Modal header text for requested fields
Default: The following fields are requested, although you may submit without completing them:" +modal-required-header = "Modal header text for required fields
Default: The following fields are required:" +modal-footer-norequired = "Modal footer text
Default: Do you want to proceed with the submission?" +modal-footer-required = "Modal footer text
Default: You must complete all required fields before continuing." +modal-cancel = "Modal cancel button text
Default: Review Response" +modal-submit = "Modal submit button text
Default: Submit Now" +highlight = "Highlight fields after displaying warning?" +disable-greenhl = "Disable green highlight
This can conflict visually with requested and required field highlighting" +show-requested = "Indicate which fields are requested on the survey page" +requested-label = "Requested field label text
Default: * response requested" +requested-label-colour =Requested field label colour
Default: blue +requested-hlcolour = "Highlight colour for requested fields
Default: light blue" +required-hlcolour = "Highlight colour for required fields
Default: light red" + +; Default text +modal-title-text = "Action Required!" +modal-requested-header-text = "The following fields are requested, although you may submit without completing them:" +modal-required-header-text = "The following fields are required:" +modal-footer-norequired-text = "Do you want to proceed with the submission?" +modal-footer-required-text = "You must complete all required fields before continuing." +modal-cancel-text = "Review Response" +modal-submit-text = "Submit Now" +requested-label-text = "* response requested" + +; Default colours (if this works) +requested-hlcolour-hex = "#d2e0ff" +required-hlcolour-hex = "#ffd2e0" +requested-label-colour-hex = "#0000bb" diff --git a/screencast.gif b/screencast.gif new file mode 100644 index 0000000..8e38ab7 Binary files /dev/null and b/screencast.gif differ