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
|
|
|
|
|
m_redcap_readUI <- function(id, include_title = TRUE) {
|
|
|
|
|
ns <- shiny::NS(id)
|
|
|
|
|
|
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",
|
|
|
|
|
value = "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."),
|
|
|
|
|
shiny::actionButton(
|
|
|
|
|
inputId = ns("data_connect"),
|
|
|
|
|
label = "Connect",
|
|
|
|
|
icon = shiny::icon("link", lib = "glyphicon"),
|
|
|
|
|
# width = NULL,
|
|
|
|
|
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
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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"),
|
|
|
|
|
shiny::uiOutput(outputId = ns("fields")),
|
2025-03-11 13:42:57 +01:00
|
|
|
|
shiny::uiOutput(outputId = ns("data_type")),
|
|
|
|
|
shiny::uiOutput(outputId = ns("fill")),
|
2024-12-19 11:32:09 +01:00
|
|
|
|
shinyWidgets::switchInput(
|
|
|
|
|
inputId = "do_filter",
|
|
|
|
|
label = "Apply filter?",
|
|
|
|
|
value = FALSE,
|
|
|
|
|
inline = FALSE,
|
|
|
|
|
onLabel = "YES",
|
|
|
|
|
offLabel = "NO"
|
|
|
|
|
),
|
|
|
|
|
shiny::conditionalPanel(
|
|
|
|
|
condition = "input.do_filter",
|
|
|
|
|
shiny::uiOutput(outputId = ns("arms")),
|
|
|
|
|
shiny::textInput(
|
|
|
|
|
inputId = ns("filter"),
|
|
|
|
|
label = "Optional filter logic (e.g., [gender] = 'female')"
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
shiny::fluidPage(
|
|
|
|
|
if (include_title) shiny::tags$h3("Import data from REDCap"),
|
2025-01-15 16:21:38 +01:00
|
|
|
|
bslib::layout_columns(
|
2024-12-19 11:32:09 +01:00
|
|
|
|
server_ui,
|
2025-01-15 16:21:38 +01:00
|
|
|
|
params_ui,
|
|
|
|
|
col_widths = bslib::breakpoints(
|
|
|
|
|
sm = c(12, 12),
|
|
|
|
|
md = c(12, 12)
|
|
|
|
|
)
|
|
|
|
|
),
|
2024-12-19 11:32:09 +01:00
|
|
|
|
shiny::column(
|
|
|
|
|
width = 12,
|
|
|
|
|
# shiny::actionButton(inputId = ns("import"), label = "Import"),
|
2025-02-26 21:09:08 +01:00
|
|
|
|
## TODO: Use busy indicator like on download to have button activate/deactivate
|
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-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"
|
|
|
|
|
# ),
|
2024-12-19 11:32:09 +01:00
|
|
|
|
shiny::br(),
|
|
|
|
|
shiny::br(),
|
2025-02-27 13:34:45 +01:00
|
|
|
|
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
|
|
|
|
shiny::br(),
|
2025-02-26 21:09:08 +01:00
|
|
|
|
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,
|
|
|
|
|
imported = 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), {
|
|
|
|
|
uri <- paste0(ifelse(endsWith(input$uri, "/"), input$uri, paste0(input$uri, "/")), "api/")
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
imported <- try(rlang::exec(REDCapR::redcap_metadata_read, !!!parameters), silent = TRUE)
|
|
|
|
|
|
|
|
|
|
## 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-03-11 13:42:57 +01:00
|
|
|
|
include_data_alert(see_data_text = "Click to see data dictionary",
|
2025-02-26 21:09:08 +01:00
|
|
|
|
dataIdName = "see_data",
|
2025-03-11 13:42:57 +01:00
|
|
|
|
extra = tags$p(tags$b(phosphoricons::ph("check", weight = "bold"), "Connected to server!"), tags$p(paste0(data_rv$info$project_title, " 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-02-26 21:09:08 +01:00
|
|
|
|
shiny::observeEvent(input$see_data, {
|
|
|
|
|
datamods::show_data(
|
|
|
|
|
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
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
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"
|
|
|
|
|
),
|
|
|
|
|
selected = "yes",
|
|
|
|
|
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-03-07 14:53:22 +01:00
|
|
|
|
vectorSelectInput(
|
2024-12-19 11:32:09 +01:00
|
|
|
|
inputId = ns("arms"),
|
|
|
|
|
selected = NULL,
|
|
|
|
|
label = "Filter by events/arms",
|
2025-03-11 13:42:57 +01:00
|
|
|
|
choices = stats::setNames(arms()[[3]], arms()[[1]]),
|
2024-12-19 11:32:09 +01:00
|
|
|
|
multiple = TRUE
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
|
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-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,
|
|
|
|
|
split_forms = if (input$data_type == "long") "none" else "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-02-26 21:09:08 +01:00
|
|
|
|
code <- rlang::call2(REDCapCAST::read_redcap_tables, !!!parameters)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (inherits(imported, "try-error") || NROW(imported) < 1) {
|
|
|
|
|
data_rv$data_status <- "error"
|
|
|
|
|
data_rv$data_list <- NULL
|
2024-12-19 11:32:09 +01:00
|
|
|
|
} else {
|
2025-02-26 21:09:08 +01:00
|
|
|
|
data_rv$data_status <- "success"
|
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
|
|
|
|
|
if (input$data_type != "long") {
|
|
|
|
|
# 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()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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-03-11 13:42:57 +01:00
|
|
|
|
# shiny::observe({
|
|
|
|
|
# shiny::req(data_rv$imported)
|
|
|
|
|
#
|
|
|
|
|
# imported <- data_rv$imported
|
|
|
|
|
#
|
|
|
|
|
#
|
|
|
|
|
# })
|
|
|
|
|
|
2025-02-26 21:09:08 +01:00
|
|
|
|
return(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(
|
|
|
|
|
m_redcap_readUI("data"),
|
2025-02-26 21:09:08 +01:00
|
|
|
|
DT::DTOutput("data_summary")
|
2024-12-19 11:32:09 +01:00
|
|
|
|
)
|
|
|
|
|
server <- function(input, output, session) {
|
2025-01-15 16:21:38 +01:00
|
|
|
|
data_val <- shiny::reactiveValues(data = NULL)
|
2024-12-19 11:32:09 +01:00
|
|
|
|
|
|
|
|
|
|
2025-02-26 21:09:08 +01:00
|
|
|
|
data_val$data <- m_redcap_readServer(id = "data")
|
2024-12-19 11:32:09 +01:00
|
|
|
|
|
|
|
|
|
output$data_summary <- DT::renderDataTable(
|
|
|
|
|
{
|
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
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
shiny::shinyApp(ui, server)
|
|
|
|
|
}
|