2024-12-19 11:32:09 +01:00
#' Shiny module to browser and export REDCap data
#'
#' @param id Namespace id
#' @param include_title logical to include title
#'
#' @rdname redcap_read_shiny_module
#'
#' @return shiny ui element
#' @export
2025-04-03 13:11:02 +02:00
m_redcap_readUI <- function ( id , title = TRUE , url = NULL ) {
2024-12-19 11:32:09 +01:00
ns <- shiny :: NS ( id )
2025-03-13 12:41:50 +01:00
if ( isTRUE ( title ) ) {
title <- shiny :: tags $ h4 (
" Import data from REDCap" ,
class = " redcap-module-title"
)
}
2025-01-15 16:21:38 +01:00
server_ui <- shiny :: tagList (
# width = 6,
2025-02-27 13:34:45 +01:00
shiny :: tags $ h4 ( " REDCap server" ) ,
2024-12-19 11:32:09 +01:00
shiny :: textInput (
inputId = ns ( " uri" ) ,
2025-02-27 13:34:45 +01:00
label = " Web address" ,
2025-04-03 13:11:02 +02:00
value = if_not_missing ( url , " https://redcap.your.institution/" )
2024-12-19 11:32:09 +01:00
) ,
2025-02-27 13:34:45 +01:00
shiny :: helpText ( " Format should be either 'https://redcap.your.institution/' or 'https://your.institution/redcap/'" ) ,
2024-12-19 11:32:09 +01:00
shiny :: textInput (
inputId = ns ( " api" ) ,
label = " API token" ,
value = " "
2025-02-26 21:09:08 +01:00
) ,
2025-02-27 13:34:45 +01:00
shiny :: helpText ( " The token is a string of 32 numbers and letters." ) ,
2025-04-03 13:11:02 +02:00
shiny :: br ( ) ,
shiny :: br ( ) ,
2025-02-27 13:34:45 +01:00
shiny :: actionButton (
inputId = ns ( " data_connect" ) ,
label = " Connect" ,
icon = shiny :: icon ( " link" , lib = " glyphicon" ) ,
2025-04-03 13:11:02 +02:00
width = " 100%" ,
2025-02-27 13:34:45 +01:00
disabled = TRUE
) ,
shiny :: br ( ) ,
shiny :: br ( ) ,
2025-02-26 21:09:08 +01:00
tags $ div (
id = ns ( " connect-placeholder" ) ,
shinyWidgets :: alert (
id = ns ( " connect-result" ) ,
status = " info" ,
2025-02-27 13:34:45 +01:00
tags $ p ( phosphoricons :: ph ( " info" , weight = " bold" ) , " Please fill in server address (URI) and API token, then press 'Connect'." )
2025-02-26 21:09:08 +01:00
) ,
dismissible = TRUE
2025-02-27 13:34:45 +01:00
) ,
shiny :: br ( )
2024-12-19 11:32:09 +01:00
)
2025-04-03 13:11:02 +02:00
filter_ui <-
shiny :: tagList (
# width = 6,
shiny :: uiOutput ( outputId = ns ( " arms" ) ) ,
shiny :: textInput (
inputId = ns ( " filter" ) ,
label = " Optional filter logic (e.g., [gender] = 'female')"
)
)
2024-12-19 11:32:09 +01:00
params_ui <-
2025-01-15 16:21:38 +01:00
shiny :: tagList (
# width = 6,
2024-12-19 11:32:09 +01:00
shiny :: tags $ h4 ( " Data import parameters" ) ,
shiny :: helpText ( " Options here will show, when API and uri are typed" ) ,
2025-04-09 12:31:08 +02:00
shiny :: tags $ br ( ) ,
2024-12-19 11:32:09 +01:00
shiny :: uiOutput ( outputId = ns ( " fields" ) ) ,
2025-04-03 13:11:02 +02:00
shiny :: tags $ div (
class = " shiny-input-container" ,
shiny :: tags $ label (
class = " control-label" ,
`for` = ns ( " dropdown_params" ) ,
" ..." ,
style = htmltools :: css ( visibility = " hidden" )
) ,
shinyWidgets :: dropMenu (
shiny :: actionButton (
inputId = ns ( " dropdown_params" ) ,
label = " Add data filters" ,
icon = shiny :: icon ( " filter" ) ,
width = " 100%" ,
class = " px-1"
) ,
filter_ui
) ,
shiny :: helpText ( " Optionally filter project arms if logitudinal or apply server side data filters" )
) ,
2025-04-09 12:31:08 +02:00
shiny :: tags $ br ( ) ,
2025-03-11 13:42:57 +01:00
shiny :: uiOutput ( outputId = ns ( " data_type" ) ) ,
shiny :: uiOutput ( outputId = ns ( " fill" ) ) ,
2025-02-27 13:34:45 +01:00
shiny :: actionButton (
inputId = ns ( " data_import" ) ,
2024-12-19 11:32:09 +01:00
label = " Import" ,
icon = shiny :: icon ( " download" , lib = " glyphicon" ) ,
2025-02-27 13:34:45 +01:00
width = " 100%" ,
disabled = TRUE
2024-12-19 11:32:09 +01:00
) ,
2025-04-03 13:11:02 +02:00
shiny :: tags $ br ( ) ,
shiny :: tags $ br ( ) ,
tags $ div (
id = ns ( " retrieved-placeholder" ) ,
shinyWidgets :: alert (
id = ns ( " retrieved-result" ) ,
status = " info" ,
tags $ p ( phosphoricons :: ph ( " info" , weight = " bold" ) , " Please specify data to download, then press 'Import'." )
) ,
dismissible = TRUE
2025-05-05 14:44:18 +02:00
) # ,
2025-04-03 13:11:02 +02:00
## TODO: Use busy indicator like on download to have button activate/deactivate
2025-02-27 13:34:45 +01:00
# bslib::input_task_button(
# id = ns("data_import"),
# label = "Import",
# icon = shiny::icon("download", lib = "glyphicon"),
# label_busy = "Just a minute...",
# icon_busy = fontawesome::fa_i("arrows-rotate",
# class = "fa-spin",
# "aria-hidden" = "true"
# ),
# type = "primary",
# auto_reset = TRUE#,state="busy"
# ),
2025-04-03 13:11:02 +02:00
# shiny::br(),
# shiny::helpText("Press 'Import' to get data from the REDCap server. Check the preview below before proceeding.")
2024-12-19 11:32:09 +01:00
)
2025-04-03 13:11:02 +02:00
shiny :: fluidPage (
title = title ,
server_ui ,
shiny :: conditionalPanel (
condition = " output.connect_success == true" ,
params_ui ,
ns = ns
) ,
shiny :: br ( )
2024-12-19 11:32:09 +01:00
)
}
2025-02-27 13:34:45 +01:00
2024-12-19 11:32:09 +01:00
#' @rdname redcap_read_shiny_module
#'
#' @return shiny server module
#' @export
#'
2025-02-26 21:09:08 +01:00
m_redcap_readServer <- function ( id ) {
2024-12-19 11:32:09 +01:00
module <- function ( input , output , session ) {
ns <- session $ ns
2025-02-26 21:09:08 +01:00
data_rv <- shiny :: reactiveValues (
dd_status = NULL ,
data_status = NULL ,
2025-02-27 13:34:45 +01:00
uri = NULL ,
project_name = NULL ,
2025-02-26 21:09:08 +01:00
info = NULL ,
arms = NULL ,
dd_list = NULL ,
2025-03-11 13:42:57 +01:00
data = NULL ,
rep_fields = NULL ,
2025-03-17 15:00:13 +01:00
code = NULL
2025-02-26 21:09:08 +01:00
)
2024-12-19 11:32:09 +01:00
2025-02-27 13:34:45 +01:00
shiny :: observeEvent ( list ( input $ api , input $ uri ) , {
2025-03-13 12:41:50 +01:00
shiny :: req ( input $ api )
shiny :: req ( input $ uri )
2025-04-03 13:11:02 +02:00
if ( ! is.null ( input $ uri ) ) {
uri <- paste0 ( ifelse ( endsWith ( input $ uri , " /" ) , input $ uri , paste0 ( input $ uri , " /" ) ) , " api/" )
2025-03-13 12:41:50 +01:00
} else {
uri <- input $ uri
}
2025-02-27 13:34:45 +01:00
if ( is_valid_redcap_url ( uri ) & is_valid_token ( input $ api ) ) {
data_rv $ uri <- uri
shiny :: updateActionButton ( inputId = " data_connect" , disabled = FALSE )
} else {
shiny :: updateActionButton ( inputId = " data_connect" , disabled = TRUE )
}
} )
tryCatch (
{
2025-02-26 21:09:08 +01:00
shiny :: observeEvent (
list (
2025-02-27 13:34:45 +01:00
input $ data_connect
2025-02-26 21:09:08 +01:00
) ,
{
shiny :: req ( input $ api )
2025-02-27 13:34:45 +01:00
shiny :: req ( data_rv $ uri )
2025-02-26 21:09:08 +01:00
parameters <- list (
2025-02-27 13:34:45 +01:00
redcap_uri = data_rv $ uri ,
2025-02-26 21:09:08 +01:00
token = input $ api
)
# browser()
2025-05-10 11:30:36 +02:00
shiny :: withProgress (
{
imported <- try ( rlang :: exec ( REDCapR :: redcap_metadata_read , ! ! ! parameters ) , silent = TRUE )
} ,
message = paste ( " Connecting to" , data_rv $ uri )
)
2025-02-26 21:09:08 +01:00
## TODO: Simplify error messages
if ( inherits ( imported , " try-error" ) || NROW ( imported ) < 1 || ifelse ( is.list ( imported ) , ! isTRUE ( imported $ success ) , FALSE ) ) {
if ( ifelse ( is.list ( imported ) , ! isTRUE ( imported $ success ) , FALSE ) ) {
mssg <- imported $ raw_text
} else {
mssg <- attr ( imported , " condition" ) $ message
}
2025-02-27 13:34:45 +01:00
datamods ::: insert_error ( mssg = mssg , selector = " connect" )
2025-02-26 21:09:08 +01:00
data_rv $ dd_status <- " error"
data_rv $ dd_list <- NULL
} else if ( isTRUE ( imported $ success ) ) {
2025-02-27 13:34:45 +01:00
data_rv $ dd_status <- " success"
2025-03-11 13:42:57 +01:00
data_rv $ info <- REDCapR :: redcap_project_info_read (
2025-02-27 13:34:45 +01:00
redcap_uri = data_rv $ uri ,
token = input $ api
2025-03-11 13:42:57 +01:00
) $ data
2025-02-26 21:09:08 +01:00
datamods ::: insert_alert (
selector = ns ( " connect" ) ,
status = " success" ,
2025-04-03 13:11:02 +02:00
include_data_alert (
see_data_text = " Click to see data dictionary" ,
2025-05-10 11:30:36 +02:00
dataIdName = " see_dd" ,
2025-05-05 14:44:18 +02:00
extra = tags $ p (
tags $ b ( phosphoricons :: ph ( " check" , weight = " bold" ) , " Connected to server!" ) ,
glue :: glue ( " The {data_rv$info$project_title} project is loaded." )
) ,
2025-02-26 21:09:08 +01:00
btn_show_data = TRUE
)
)
data_rv $ dd_list <- imported
}
} ,
ignoreInit = TRUE
)
2025-02-27 13:34:45 +01:00
} ,
warning = function ( warn ) {
showNotification ( paste0 ( warn ) , type = " warning" )
} ,
error = function ( err ) {
showNotification ( paste0 ( err ) , type = " err" )
}
)
2024-12-19 11:32:09 +01:00
2025-04-03 13:11:02 +02:00
output $ connect_success <- shiny :: reactive ( identical ( data_rv $ dd_status , " success" ) )
shiny :: outputOptions ( output , " connect_success" , suspendWhenHidden = FALSE )
2025-05-10 11:30:36 +02:00
shiny :: observeEvent ( input $ see_dd , {
show_data (
2025-02-26 21:09:08 +01:00
purrr :: pluck ( data_rv $ dd_list , " data" ) ,
title = " Data dictionary" ,
type = " modal" ,
show_classes = FALSE ,
tags $ b ( " Preview:" )
2025-02-27 13:34:45 +01:00
)
2024-12-19 11:32:09 +01:00
} )
2025-05-10 11:30:36 +02:00
shiny :: observeEvent ( input $ see_data , {
show_data (
# purrr::pluck(data_rv$dd_list, "data"),
data_rv $ data ,
title = " Imported data set" ,
type = " modal" ,
show_classes = FALSE ,
tags $ b ( " Preview:" )
)
} )
2024-12-19 11:32:09 +01:00
arms <- shiny :: reactive ( {
shiny :: req ( input $ api )
2025-02-27 13:34:45 +01:00
shiny :: req ( data_rv $ uri )
2024-12-19 11:32:09 +01:00
REDCapR :: redcap_event_read (
2025-02-27 13:34:45 +01:00
redcap_uri = data_rv $ uri ,
2024-12-19 11:32:09 +01:00
token = input $ api
) $ data
} )
output $ fields <- shiny :: renderUI ( {
2025-02-26 21:09:08 +01:00
shiny :: req ( data_rv $ dd_list )
2024-12-19 11:32:09 +01:00
shinyWidgets :: virtualSelectInput (
inputId = ns ( " fields" ) ,
2025-02-27 13:34:45 +01:00
label = " Select variables to import:" ,
2025-02-26 21:09:08 +01:00
choices = purrr :: pluck ( data_rv $ dd_list , " data" ) | >
2024-12-19 11:32:09 +01:00
dplyr :: select ( field_name , form_name ) | >
( \ ( .x ) {
2025-03-11 13:42:57 +01:00
split ( .x $ field_name , REDCapCAST :: as_factor ( .x $ form_name ) )
2025-02-27 13:34:45 +01:00
} ) ( ) ,
updateOn = " change" ,
2024-12-19 11:32:09 +01:00
multiple = TRUE ,
search = TRUE ,
showValueAsTags = TRUE
)
} )
2025-03-11 13:42:57 +01:00
output $ data_type <- shiny :: renderUI ( {
shiny :: req ( data_rv $ info )
if ( isTRUE ( data_rv $ info $ has_repeating_instruments_or_events ) ) {
vectorSelectInput (
inputId = ns ( " data_type" ) ,
label = " Select the data format to import" ,
choices = c (
" Wide data (One row for each subject)" = " wide" ,
" Long data for project with repeating instruments (default REDCap)" = " long"
) ,
selected = " wide" ,
multiple = FALSE
)
}
} )
output $ fill <- shiny :: renderUI ( {
shiny :: req ( data_rv $ info )
shiny :: req ( input $ data_type )
## Get repeated field
data_rv $ rep_fields <- data_rv $ dd_list $ data $ field_name [
data_rv $ dd_list $ data $ form_name %in% repeated_instruments (
uri = data_rv $ uri ,
token = input $ api
)
]
if ( input $ data_type == " long" && isTRUE ( any ( input $ fields %in% data_rv $ rep_fields ) ) ) {
vectorSelectInput (
inputId = ns ( " fill" ) ,
label = " Fill missing values?" ,
choices = c (
" Yes, fill missing, non-repeated values" = " yes" ,
" No, leave the data as is" = " no"
) ,
2025-04-08 13:45:07 +02:00
selected = " no" ,
2025-03-11 13:42:57 +01:00
multiple = FALSE
)
}
} )
2025-02-27 13:34:45 +01:00
shiny :: observeEvent ( input $ fields , {
if ( is.null ( input $ fields ) | length ( input $ fields ) == 0 ) {
shiny :: updateActionButton ( inputId = " data_import" , disabled = TRUE )
} else {
shiny :: updateActionButton ( inputId = " data_import" , disabled = FALSE )
}
} )
2025-02-26 21:09:08 +01:00
2024-12-19 11:32:09 +01:00
output $ arms <- shiny :: renderUI ( {
2025-04-03 13:11:02 +02:00
if ( NROW ( arms ( ) ) > 0 ) {
vectorSelectInput (
inputId = ns ( " arms" ) ,
selected = NULL ,
label = " Filter by events/arms" ,
choices = stats :: setNames ( arms ( ) [ [3 ] ] , arms ( ) [ [1 ] ] ) ,
multiple = TRUE
)
}
2024-12-19 11:32:09 +01:00
} )
2025-02-26 21:09:08 +01:00
shiny :: observeEvent ( input $ data_import , {
2024-12-19 11:32:09 +01:00
shiny :: req ( input $ fields )
2025-04-03 13:11:02 +02:00
# browser()
2025-02-26 21:09:08 +01:00
record_id <- purrr :: pluck ( data_rv $ dd_list , " data" ) [ [1 ] ] [1 ]
2024-12-19 11:32:09 +01:00
2025-03-11 13:42:57 +01:00
2025-02-26 21:09:08 +01:00
parameters <- list (
2025-02-27 13:34:45 +01:00
uri = data_rv $ uri ,
2024-12-19 11:32:09 +01:00
token = input $ api ,
fields = unique ( c ( record_id , input $ fields ) ) ,
events = input $ arms ,
raw_or_label = " both" ,
2025-03-11 13:42:57 +01:00
filter_logic = input $ filter ,
2025-04-03 13:11:02 +02:00
split_forms = ifelse (
input $ data_type == " long" && ! is.null ( input $ data_type ) ,
" none" ,
" all"
)
2024-12-19 11:32:09 +01:00
)
2025-02-27 13:34:45 +01:00
shiny :: withProgress ( message = " Downloading REDCap data. Hold on for a moment.." , {
imported <- try ( rlang :: exec ( REDCapCAST :: read_redcap_tables , ! ! ! parameters ) , silent = TRUE )
} )
2025-03-17 15:00:13 +01:00
2025-05-10 11:30:36 +02:00
parameters_code <- parameters [c ( " uri" , " fields" , " events" , " raw_or_label" , " filter_logic" ) ]
code <- rlang :: call2 (
" easy_redcap" ,
! ! ! utils :: modifyList (
parameters_code ,
list (
data_format = ifelse (
input $ data_type == " long" && ! is.null ( input $ data_type ) ,
" long" ,
" wide"
) ,
project.name = simple_snake ( data_rv $ info $ project_title )
)
) ,
2025-04-03 13:11:02 +02:00
.ns = " REDCapCAST"
)
2025-02-26 21:09:08 +01:00
if ( inherits ( imported , " try-error" ) || NROW ( imported ) < 1 ) {
data_rv $ data_status <- " error"
data_rv $ data_list <- NULL
2025-04-03 13:11:02 +02:00
data_rv $ data_message <- imported $ raw_text
2024-12-19 11:32:09 +01:00
} else {
2025-02-26 21:09:08 +01:00
data_rv $ data_status <- " success"
2025-04-03 13:11:02 +02:00
data_rv $ data_message <- " Requested data was retrieved!"
2025-03-11 13:42:57 +01:00
## The data management below should be separated to allow for changing
## "wide"/"long" without re-importing data
2025-04-03 13:11:02 +02:00
if ( parameters $ split_form == " all" ) {
2025-03-11 13:42:57 +01:00
# browser()
out <- imported | >
# redcap_wider()
REDCapCAST :: redcap_wider ( )
} else {
if ( input $ fill == " yes" ) {
## Repeated fields
## Non-repeated fields in current dataset
inc_non_rep <- names ( imported ) [ ! names ( imported ) %in% data_rv $ rep_fields ]
out <- imported | >
drop_empty_event ( ) | >
dplyr :: group_by ( ! ! dplyr :: sym ( names ( imported ) [1 ] ) ) | >
tidyr :: fill ( inc_non_rep ) | >
dplyr :: ungroup ( )
} else {
out <- imported | >
drop_empty_event ( )
}
}
2025-04-03 13:11:02 +02:00
# browser()
in_data_check <- parameters $ fields %in% names ( out ) |
sapply ( names ( out ) , \ ( .x ) any ( sapply ( parameters $ fields , \ ( .y ) startsWith ( .x , .y ) ) ) )
if ( ! any ( in_data_check [ -1 ] ) ) {
data_rv $ data_status <- " warning"
data_rv $ data_message <- " Data retrieved, but it looks like only the ID was retrieved from the server. Please check with your REDCap administrator that you have required permissions for data access."
}
if ( ! all ( in_data_check ) ) {
data_rv $ data_status <- " warning"
data_rv $ data_message <- " Data retrieved, but it looks like not all requested fields were retrieved from the server. Please check with your REDCap administrator that you have required permissions for data access."
}
2025-03-17 15:00:13 +01:00
data_rv $ code <- code
2025-03-11 13:42:57 +01:00
data_rv $ data <- out | >
2025-02-26 21:09:08 +01:00
dplyr :: select ( - dplyr :: ends_with ( " _complete" ) ) | >
2025-03-11 13:42:57 +01:00
# dplyr::select(-dplyr::any_of(record_id)) |>
2025-02-26 21:09:08 +01:00
REDCapCAST :: suffix2label ( )
2024-12-19 11:32:09 +01:00
}
} )
2025-02-26 21:09:08 +01:00
2025-04-03 13:11:02 +02:00
shiny :: observeEvent (
data_rv $ data_status ,
{
# browser()
if ( identical ( data_rv $ data_status , " error" ) ) {
datamods ::: insert_error ( mssg = data_rv $ data_message , selector = ns ( " retrieved" ) )
} else if ( identical ( data_rv $ data_status , " success" ) ) {
datamods ::: insert_alert (
selector = ns ( " retrieved" ) ,
status = data_rv $ data_status ,
2025-05-10 11:30:36 +02:00
# tags$p(
# tags$b(phosphoricons::ph("check", weight = "bold"), "Success!"),
# data_rv$data_message
# ),
include_data_alert (
see_data_text = " Click to see the imported data" ,
dataIdName = " see_data" ,
extra = tags $ p (
tags $ b ( phosphoricons :: ph ( " check" , weight = " bold" ) , data_rv $ data_message )
) ,
btn_show_data = TRUE
2025-04-03 13:11:02 +02:00
)
)
} else {
datamods ::: insert_alert (
selector = ns ( " retrieved" ) ,
status = data_rv $ data_status ,
tags $ p (
tags $ b ( phosphoricons :: ph ( " warning" , weight = " bold" ) , " Warning!" ) ,
data_rv $ data_message
)
)
}
}
)
2025-03-11 13:42:57 +01:00
2025-03-17 15:00:13 +01:00
return ( list (
status = shiny :: reactive ( data_rv $ data_status ) ,
name = shiny :: reactive ( data_rv $ info $ project_title ) ,
info = shiny :: reactive ( data_rv $ info ) ,
code = shiny :: reactive ( data_rv $ code ) ,
data = shiny :: reactive ( data_rv $ data )
) )
2024-12-19 11:32:09 +01:00
}
shiny :: moduleServer (
id = id ,
module = module
)
}
2025-02-26 21:09:08 +01:00
#' @importFrom htmltools tagList tags
#' @importFrom shiny icon getDefaultReactiveDomain
2025-03-03 08:44:46 +01:00
include_data_alert <- function ( dataIdName = " see_data" ,
2025-02-26 21:09:08 +01:00
btn_show_data ,
2025-02-27 13:34:45 +01:00
see_data_text = " Click to see data" ,
2025-02-26 21:09:08 +01:00
extra = NULL ,
session = shiny :: getDefaultReactiveDomain ( ) ) {
if ( isTRUE ( btn_show_data ) ) {
success_message <- tagList (
extra ,
tags $ br ( ) ,
shiny :: actionLink (
inputId = session $ ns ( dataIdName ) ,
2025-03-11 13:42:57 +01:00
label = tagList ( phosphoricons :: ph ( " book-open-text" ) , see_data_text )
2025-02-26 21:09:08 +01:00
)
)
}
return ( success_message )
}
2025-02-25 09:51:42 +01:00
# #' REDCap import teal data module
# #'
# #' @rdname redcap_read_shiny_module
# tdm_redcap_read <- teal::teal_data_module(
# ui <- function(id) {
# shiny::fluidPage(
# m_redcap_readUI(id)
# )
# },
# server = function(id) {
# m_redcap_readServer(id, output.format = "teal")
# }
# )
2024-12-19 11:32:09 +01:00
2025-03-11 13:42:57 +01:00
#' Test if url is valid format for REDCap API
2025-02-27 13:34:45 +01:00
#'
2025-03-11 13:42:57 +01:00
#' @param url url
2025-02-27 13:34:45 +01:00
#'
2025-03-11 13:42:57 +01:00
#' @returns logical
2025-02-27 13:34:45 +01:00
#' @export
#'
#' @examples
#' url <- c(
#' "www.example.com",
2025-03-11 13:42:57 +01:00
#' "redcap.your.inst/api/",
#' "https://redcap.your.inst/api/",
#' "https://your.inst/redcap/api/",
#' "https://www.your.inst/redcap/api/"
2025-02-27 13:34:45 +01:00
#' )
#' is_valid_redcap_url(url)
is_valid_redcap_url <- function ( url ) {
pattern <- " https://[^ /$.?#].[^\\s]*/api/$"
stringr :: str_detect ( url , pattern )
}
#' Validate REDCap token
#'
#' @param token token
#' @param pattern_env pattern
#'
2025-03-11 13:42:57 +01:00
#' @returns logical
2025-02-27 13:34:45 +01:00
#' @export
#'
#' @examples
#' token <- paste(sample(c(1:9, LETTERS[1:6]), 32, TRUE), collapse = "")
#' is_valid_token(token)
is_valid_token <- function ( token , pattern_env = NULL , nchar = 32 ) {
checkmate :: assert_character ( token , any.missing = TRUE , len = 1 )
if ( ! is.null ( pattern_env ) ) {
checkmate :: assert_character ( pattern_env ,
any.missing = FALSE ,
len = 1
)
pattern <- pattern_env
} else {
pattern <- glue :: glue ( " ^([0-9A-Fa-f]{<nchar>})(?:\\n)?$" ,
.open = " <" ,
.close = " >"
)
}
if ( is.na ( token ) ) {
out <- FALSE
} else if ( is.null ( token ) ) {
out <- FALSE
} else if ( nchar ( token ) == 0L ) {
out <- FALSE
} else if ( ! grepl ( pattern , token , perl = TRUE ) ) {
out <- FALSE
} else {
out <- TRUE
}
out
}
2025-03-11 13:42:57 +01:00
#' Get names of repeated instruments
#'
#' @param uri REDCap database uri
#' @param token database token
#'
#' @returns vector
#' @export
#'
repeated_instruments <- function ( uri , token ) {
instruments <- REDCapR :: redcap_event_instruments ( redcap_uri = uri , token = token )
unique ( instruments $ data $ form [duplicated ( instruments $ data $ form ) ] )
}
#' Drop empty events from REDCap export
#'
#' @param data data
#' @param event "redcap_event_name", "redcap_repeat_instrument" or
#' "redcap_repeat_instance"
#'
#' @returns data.frame
#' @export
#'
drop_empty_event <- function ( data , event = " redcap_event_name" ) {
generics <- c ( names ( data ) [1 ] , " redcap_event_name" , " redcap_repeat_instrument" , " redcap_repeat_instance" )
filt <- split ( data , data [ [event ] ] ) | >
lapply ( \ ( .x ) {
dplyr :: select ( .x , - tidyselect :: all_of ( generics ) ) | >
REDCapCAST :: all_na ( )
} ) | >
unlist ( )
data [data [ [event ] ] %in% names ( filt ) [ ! filt ] , ]
}
2025-02-27 13:34:45 +01:00
2024-12-19 11:32:09 +01:00
#' Test app for the redcap_read_shiny_module
#'
#' @rdname redcap_read_shiny_module
#'
#' @examples
#' \dontrun{
2025-02-26 21:09:08 +01:00
#' redcap_demo_app()
2024-12-19 11:32:09 +01:00
#' }
2025-02-26 21:09:08 +01:00
redcap_demo_app <- function ( ) {
2024-12-19 11:32:09 +01:00
ui <- shiny :: fluidPage (
2025-04-03 13:11:02 +02:00
m_redcap_readUI ( " data" , url = NULL ) ,
2025-03-17 15:00:13 +01:00
DT :: DTOutput ( " data" ) ,
shiny :: tags $ b ( " Code:" ) ,
shiny :: verbatimTextOutput ( outputId = " code" )
2024-12-19 11:32:09 +01:00
)
server <- function ( input , output , session ) {
2025-03-17 15:00:13 +01:00
data_val <- m_redcap_readServer ( id = " data" )
2024-12-19 11:32:09 +01:00
2025-03-17 15:00:13 +01:00
output $ data <- DT :: renderDataTable (
2024-12-19 11:32:09 +01:00
{
2025-02-26 21:09:08 +01:00
shiny :: req ( data_val $ data )
data_val $ data ( )
2024-12-19 11:32:09 +01:00
} ,
options = list (
scrollX = TRUE ,
pageLength = 5
2025-02-26 21:09:08 +01:00
) ,
2024-12-19 11:32:09 +01:00
)
2025-03-17 15:00:13 +01:00
output $ code <- shiny :: renderPrint ( {
shiny :: req ( data_val $ code )
data_val $ code ( )
} )
2024-12-19 11:32:09 +01:00
}
shiny :: shinyApp ( ui , server )
}