From bdccf8d2f925a5ad733d2dc4564683b17b9bfd96 Mon Sep 17 00:00:00 2001 From: Will Beasley Date: Sun, 9 Jul 2023 16:32:03 -0500 Subject: [PATCH 01/23] error if `events` are passed to a nonlongitudinal project ref #492 --- R/redcap-read.R | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/R/redcap-read.R b/R/redcap-read.R index 72f1222c..46c3ebc5 100644 --- a/R/redcap-read.R +++ b/R/redcap-read.R @@ -300,10 +300,12 @@ redcap_read <- function( handle_httr = handle_httr ) - # browser() - if (!is.null(fields) || !is.null(forms)) + if (!is.null(events) && !metadata$longitudinal) { + stop("This project is NOT longitudinal, so do not pass a value to the `event` argument.") + } else if (!is.null(fields) || !is.null(forms)) { fields <- base::union(metadata$record_id_name, fields) # fields <- base::union(metadata$plumbing_variables, fields) + } # Retrieve list of record ids -------------------------------------- initial_call <- REDCapR::redcap_read_oneshot( @@ -405,6 +407,7 @@ redcap_read <- function( " (ie, ", length(selected_ids), " unique subject records)." ) } + read_result <- REDCapR::redcap_read_oneshot( redcap_uri = redcap_uri, token = token, From ce4f4df64f8a8cb091a085eab0edb0d52b74f5a8 Mon Sep 17 00:00:00 2001 From: Will Beasley Date: Sun, 9 Jul 2023 17:23:00 -0500 Subject: [PATCH 02/23] check for bad event names ref #492 --- R/redcap-read.R | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/R/redcap-read.R b/R/redcap-read.R index 46c3ebc5..85ff774e 100644 --- a/R/redcap-read.R +++ b/R/redcap-read.R @@ -300,9 +300,31 @@ redcap_read <- function( handle_httr = handle_httr ) - if (!is.null(events) && !metadata$longitudinal) { - stop("This project is NOT longitudinal, so do not pass a value to the `event` argument.") - } else if (!is.null(fields) || !is.null(forms)) { + if (!is.null(events)) { + if (!metadata$longitudinal) { + "This project is NOT longitudinal, so do not pass a value to the `event` argument." %>% + stop(call. = FALSE) + } else { + events_in_project <- + redcap_event_read( + redcap_uri, + token, + verbose = verbose, + config_options = config_options, + handle_httr = handle_httr + )$data %>% + dplyr::pull(unique_event_name) + + events_not_recognized <- setdiff(events, events_in_project) + if(0L < length(events_not_recognized)) { + "The following events are not recognized for this project: {%s}.\nMake sure you're using internal `event-name` (lowercase letters & underscores)\ninstead of the user-facing `event-label` (that can have spaces and uppercase letters)." %>% + sprintf(paste(events_not_recognized, collapse = ", ")) %>% + stop(call. = FALSE) + } + } # end of else + } # end of !is.null(events) + + if (!is.null(fields) || !is.null(forms)) { fields <- base::union(metadata$record_id_name, fields) # fields <- base::union(metadata$plumbing_variables, fields) } From 784fded9c1b7266f40eb0ed3954cb39dd983afb1 Mon Sep 17 00:00:00 2001 From: Will Beasley Date: Sun, 9 Jul 2023 17:34:57 -0500 Subject: [PATCH 03/23] sections for `redcap_read()` doc ref #493 --- R/redcap-read.R | 7 ++++++- man/redcap_read.Rd | 13 ++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/R/redcap-read.R b/R/redcap-read.R index 85ff774e..10235412 100644 --- a/R/redcap-read.R +++ b/R/redcap-read.R @@ -118,7 +118,8 @@ #' a zero-row tibble is returned. #' Currently the empty tibble has zero columns, but that may change in the future. #' -#' @details +#' @section Batching subsets of data: +#' #' [redcap_read()] internally uses multiple calls to [redcap_read_oneshot()] #' to select and return data. Initially, only the primary key is queried #' through the REDCap API. The long list is then subsetted into batches, @@ -148,6 +149,8 @@ #' 1. `redcap_repeat_instrument` and `redcap_repeat_instance` will be returned #' for projects with repeating instruments #' +#' @section Export permissions: +#' #' For [redcap_read_oneshot()] to function properly, the user must have Export #' permissions for the 'Full Data Set'. Users with only 'De-Identified' #' export privileges can still use `redcap_read_oneshot`. To grant the @@ -156,6 +159,8 @@ #' * select the desired user, and then select 'Edit User Privileges', #' * in the 'Data Exports' radio buttons, select 'Full Data Set'. #' +#' @section Pseudofields: +#' #' The REDCap project may contain "pseudofields", depending on its structure. #' Pseudofields are exported for certain project structures, but are not #' defined by users and do not appear in the codebook. diff --git a/man/redcap_read.Rd b/man/redcap_read.Rd index 37b19ce8..f256cc42 100644 --- a/man/redcap_read.Rd +++ b/man/redcap_read.Rd @@ -179,7 +179,9 @@ retrieves subsets of the data, and then combines them before returning be more appropriate than \code{\link[=redcap_read_oneshot]{redcap_read_oneshot()}} when returning large datasets that could tie up the server. } -\details{ +\section{Batching subsets of data}{ + + \code{\link[=redcap_read]{redcap_read()}} internally uses multiple calls to \code{\link[=redcap_read_oneshot]{redcap_read_oneshot()}} to select and return data. Initially, only the primary key is queried through the REDCap API. The long list is then subsetted into batches, @@ -209,6 +211,10 @@ included, even if not explicitly requested. As a result: \item \code{redcap_repeat_instrument} and \code{redcap_repeat_instance} will be returned for projects with repeating instruments } +} + +\section{Export permissions}{ + For \code{\link[=redcap_read_oneshot]{redcap_read_oneshot()}} to function properly, the user must have Export permissions for the 'Full Data Set'. Users with only 'De-Identified' @@ -219,6 +225,10 @@ appropriate permissions: \item select the desired user, and then select 'Edit User Privileges', \item in the 'Data Exports' radio buttons, select 'Full Data Set'. } +} + +\section{Pseudofields}{ + The REDCap project may contain "pseudofields", depending on its structure. Pseudofields are exported for certain project structures, but are not @@ -247,6 +257,7 @@ throw an error like }\if{html}{\out{}} } } + \examples{ \dontrun{ uri <- "https://bbmc.ouhsc.edu/redcap/api/" From 97ab050a032725d3b751a33d381412e462aab57e Mon Sep 17 00:00:00 2001 From: Will Beasley Date: Sun, 9 Jul 2023 17:46:02 -0500 Subject: [PATCH 04/23] documentation for event error checking ref #492 --- R/redcap-read.R | 16 ++++++++++++++++ man/redcap_read.Rd | 18 ++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/R/redcap-read.R b/R/redcap-read.R index 10235412..cb5cfac3 100644 --- a/R/redcap-read.R +++ b/R/redcap-read.R @@ -187,6 +187,22 @@ #' ERROR: The following values in the parameter fields are not valid: 'demographics_timestamp' #' ``` #' +#' @section Events: +#' The `event` argument is a vector of characters passed to the server. +#' It is the "event-name", not the "event-label". +#' The event-label is the value presented to the users, +#' which contains uppercase letters and spaces, +#' while the event-name can contain only lowercase letters, digits, +#' and underscores. +#' +#' If `event` is nonnull and the project is not longitudinal, +#' [redcap_read()] will throw an error. +#' Similarly, if a value in the `event` vector is not a current +#' event-name, [redcap_read()] will throw an error. +#' +#' The simpler [redcap_read_oneshot()] function does not +#' check for invalid event values, and will not throw errors. +#' #' @author #' Will Beasley #' diff --git a/man/redcap_read.Rd b/man/redcap_read.Rd index f256cc42..b666a6f8 100644 --- a/man/redcap_read.Rd +++ b/man/redcap_read.Rd @@ -258,6 +258,24 @@ throw an error like } } +\section{Events}{ + +The \code{event} argument is a vector of characters passed to the server. +It is the "event-name", not the "event-label". +The event-label is the value presented to the users, +which contains uppercase letters and spaces, +while the event-name can contain only lowercase letters, digits, +and underscores. + +If \code{event} is nonnull and the project is not longitudinal, +\code{\link[=redcap_read]{redcap_read()}} will throw an error. +Similarly, if a value in the \code{event} vector is not a current +event-name, \code{\link[=redcap_read]{redcap_read()}} will throw an error. + +The simpler \code{\link[=redcap_read_oneshot]{redcap_read_oneshot()}} function does not +check for invalid event values, and will not throw errors. +} + \examples{ \dontrun{ uri <- "https://bbmc.ouhsc.edu/redcap/api/" From 526d7f9f53a6bcc5132721bbf97d3c74e6d15b84 Mon Sep 17 00:00:00 2001 From: Will Beasley Date: Sun, 9 Jul 2023 17:48:43 -0500 Subject: [PATCH 05/23] news for checking events close #492 --- NEWS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/NEWS.md b/NEWS.md index e1e8b41a..a40315cd 100644 --- a/NEWS.md +++ b/NEWS.md @@ -82,6 +82,7 @@ This will help extract forms from longitudinal & repeating projects. * `redcap_read()` and `redcap_read_oneshot()` now remove "pseudofields" (e.g., `redcap_event_name`, `redcap_repeat_instrument`, & `redcap_repeat_instance`) from the `fields` parameter. Starting with REDCap v13.4.10, an error is thrown by the server. REDCap will return a message if a common pseudofield is requested explicitly by the user. (#477) * `redcap_event_instruments()` now can return mappings for all arms, instead of one arm per call.(Suggested by @januz, #482) * `validate_for_write()` contains a few more checks, such as `validate_repeat_instance()` and `validate_data_frame_inherits()` (#485) +* `redcap_read()` checks the `event` parameter and throws an error if a value is not recognized, or the project is not longitudinal (#493) Version 1.1.0 (released 2022-08-10) ========================================================== From e9222d00028e3b91da7a6dc649f62b2f3b795012 Mon Sep 17 00:00:00 2001 From: Will Beasley Date: Sun, 9 Jul 2023 19:10:44 -0500 Subject: [PATCH 06/23] don't require tricks with a global variable ref #492 --- R/redcap-read.R | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/R/redcap-read.R b/R/redcap-read.R index cb5cfac3..470bba90 100644 --- a/R/redcap-read.R +++ b/R/redcap-read.R @@ -333,8 +333,7 @@ redcap_read <- function( verbose = verbose, config_options = config_options, handle_httr = handle_httr - )$data %>% - dplyr::pull(unique_event_name) + )$data[["unique_event_name"]] events_not_recognized <- setdiff(events, events_in_project) if(0L < length(events_not_recognized)) { From 5f5603ad04570e0496107e76f7fe3bc45f193878 Mon Sep 17 00:00:00 2001 From: Will Beasley Date: Mon, 10 Jul 2023 09:03:51 -0500 Subject: [PATCH 07/23] update spelling --- inst/WORDLIST | 1 + 1 file changed, 1 insertion(+) diff --git a/inst/WORDLIST b/inst/WORDLIST index 311009b0..bf2f42b6 100644 --- a/inst/WORDLIST +++ b/inst/WORDLIST @@ -121,6 +121,7 @@ misspecified nano nolint nonmissing +nonnull odbc ouhscbbmc packageVersion From e9702e4f858cfd16ea532fdf8341d0f2fcda6354 Mon Sep 17 00:00:00 2001 From: Will Beasley Date: Mon, 10 Jul 2023 09:56:04 -0500 Subject: [PATCH 08/23] lintr ref #485 --- R/redcap-read.R | 2 +- R/validate.R | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/R/redcap-read.R b/R/redcap-read.R index 470bba90..0439f039 100644 --- a/R/redcap-read.R +++ b/R/redcap-read.R @@ -336,7 +336,7 @@ redcap_read <- function( )$data[["unique_event_name"]] events_not_recognized <- setdiff(events, events_in_project) - if(0L < length(events_not_recognized)) { + if (0L < length(events_not_recognized)) { "The following events are not recognized for this project: {%s}.\nMake sure you're using internal `event-name` (lowercase letters & underscores)\ninstead of the user-facing `event-label` (that can have spaces and uppercase letters)." %>% sprintf(paste(events_not_recognized, collapse = ", ")) %>% stop(call. = FALSE) diff --git a/R/validate.R b/R/validate.R index 6168e6d5..e82d7dec 100644 --- a/R/validate.R +++ b/R/validate.R @@ -133,7 +133,7 @@ #' @export validate_data_frame_inherits <- function(d) { - if(!base::inherits(d, "data.frame")) { + if (!base::inherits(d, "data.frame")) { stop( "The `d` object is not a valid `data.frame`. ", "Make sure it is a data.frame ", @@ -214,7 +214,7 @@ validate_repeat_instance <- function(d, stop_on_error = FALSE) { checkmate::assert_logical(stop_on_error, any.missing = FALSE, len = 1) column_name <- "redcap_repeat_instance" - if(!any(colnames(d) == column_name)) { + if (!any(colnames(d) == column_name)) { tibble::tibble( field_name = character(0), field_index = integer(0), @@ -263,7 +263,7 @@ validate_uniqueness <- function(d, record_id_name = "record_id", stop_on_error = ) |> dplyr::filter(1L < count_of_records) - if(nrow(d_replicates) == 0L) { + if (nrow(d_replicates) == 0L) { tibble::tibble( field_name = character(0), field_index = integer(0), @@ -272,7 +272,7 @@ validate_uniqueness <- function(d, record_id_name = "record_id", stop_on_error = ) } else if (stop_on_error) { m <- - if(requireNamespace("knitr", quietly = TRUE)) { + if (requireNamespace("knitr", quietly = TRUE)) { d_replicates %>% knitr::kable() %>% paste(collapse = "\n") From fd1c71a347f65e6699e75fecda07f04c8c314a7e Mon Sep 17 00:00:00 2001 From: Will Beasley Date: Mon, 10 Jul 2023 10:50:47 -0500 Subject: [PATCH 09/23] return character of field indices ref #485 --- R/validate.R | 19 ++++++++++--------- tests/testthat/test-validate-field-names.R | 2 +- tests/testthat/test-validate-no-logical.R | 2 +- tests/testthat/test-validate-repeat.R | 2 +- utility/refresh.R | 2 +- 5 files changed, 14 insertions(+), 13 deletions(-) diff --git a/R/validate.R b/R/validate.R index e82d7dec..c2df4efb 100644 --- a/R/validate.R +++ b/R/validate.R @@ -155,7 +155,7 @@ validate_no_logical <- function(data_types, stop_on_error = FALSE) { if (length(indices) == 0L) { tibble::tibble( field_name = character(0), - field_index = integer(0), + field_index = character(0), concern = character(0), suggestion = character(0) ) @@ -169,7 +169,7 @@ validate_no_logical <- function(data_types, stop_on_error = FALSE) { } else { tibble::tibble( field_name = names(data_types)[indices], - field_index = indices, + field_index = as.character(indices), concern = "The REDCap API does not automatically convert boolean values to 0/1 values.", suggestion = "Convert the variable with the `as.integer()` function." ) @@ -187,7 +187,7 @@ validate_field_names <- function(field_names, stop_on_error = FALSE) { if (length(indices) == 0L) { tibble::tibble( field_name = character(0), - field_index = integer(0), + field_index = character(0), concern = character(0), suggestion = character(0) ) @@ -201,7 +201,7 @@ validate_field_names <- function(field_names, stop_on_error = FALSE) { } else { tibble::tibble( field_name = field_names[indices], - field_index = indices, + field_index = as.character(indices), concern = "A REDCap project does not allow field names with an uppercase letter.", suggestion = "Change the uppercase letters to lowercase, potentially with `base::tolower()`." ) @@ -217,14 +217,14 @@ validate_repeat_instance <- function(d, stop_on_error = FALSE) { if (!any(colnames(d) == column_name)) { tibble::tibble( field_name = character(0), - field_index = integer(0), + field_index = character(0), concern = character(0), suggestion = character(0) ) } else if (inherits(d[[column_name]], "integer")) { tibble::tibble( field_name = character(0), - field_index = integer(0), + field_index = character(0), concern = character(0), suggestion = character(0) ) @@ -239,7 +239,7 @@ validate_repeat_instance <- function(d, stop_on_error = FALSE) { tibble::tibble( field_name = column_name, - field_index = indices, + field_index = as.character(indices), concern = "The `redcap_repeat_instance` column should be an integer.", suggestion = "Use `as.integer()` to cast it. Make sure no 'NAs introduced by coercion' warnings appears." ) @@ -266,7 +266,7 @@ validate_uniqueness <- function(d, record_id_name = "record_id", stop_on_error = if (nrow(d_replicates) == 0L) { tibble::tibble( field_name = character(0), - field_index = integer(0), + field_index = character(0), concern = character(0), suggestion = character(0) ) @@ -288,7 +288,7 @@ validate_uniqueness <- function(d, record_id_name = "record_id", stop_on_error = tibble::tibble( field_name = paste(variables, collapse = ", "), - field_index = indices, + field_index = as.character(indices), concern = "The values in these variables were not unique.", suggestion = "Run `validate_uniqueness()` with `stop_on_error = TRUE` to see the specific values that are duplicated." ) @@ -327,6 +327,7 @@ validate_for_write <- function( lst_concerns[[length(lst_concerns) + 1L]] <- validate_no_logical(vapply(d, class, character(1))) } + # browser() # Vertically stack all the data.frames into a single data frame dplyr::bind_rows(lst_concerns) } diff --git a/tests/testthat/test-validate-field-names.R b/tests/testthat/test-validate-field-names.R index 1176195b..6541dadc 100644 --- a/tests/testthat/test-validate-field-names.R +++ b/tests/testthat/test-validate-field-names.R @@ -40,5 +40,5 @@ test_that("validate_field_names -concern dataset", { ds <- validate_field_names(colnames(ds_bad)) expect_equal(object=nrow(ds), expected=1, info="One uppercase field should be flagged") expect_equal(object=ds$field_name, expected="bad_Uppercase") - expect_equal(object=ds$field_index, expected=3) + expect_equal(object=ds$field_index, expected="3") }) diff --git a/tests/testthat/test-validate-no-logical.R b/tests/testthat/test-validate-no-logical.R index 00fd5aa0..83ce46fc 100644 --- a/tests/testthat/test-validate-no-logical.R +++ b/tests/testthat/test-validate-no-logical.R @@ -27,7 +27,7 @@ test_that("validate_no_logical -concern dataset", { ds <- validate_no_logical(vapply(ds_bad, class, character(1))) expect_equal(object=nrow(ds), expected=1, info="One logical field should be flagged") expect_equal(object=ds$field_name, expected="bad_logical") - expect_equal(object=unname(ds$field_index), expected=2) + expect_equal(object=unname(ds$field_index), expected="2") }) # ---- redcap-repeat-instance -------------------------------------------------- diff --git a/tests/testthat/test-validate-repeat.R b/tests/testthat/test-validate-repeat.R index 70e96da4..36f7b96e 100644 --- a/tests/testthat/test-validate-repeat.R +++ b/tests/testthat/test-validate-repeat.R @@ -29,5 +29,5 @@ test_that("validate_repeat_instance -double", { ds <- validate_repeat_instance(d) expect_equal(object=nrow(ds), expected=1) expect_equal(object=ds$field_name, expected="redcap_repeat_instance") - expect_equal(object=ds$field_index, expected=1) + expect_equal(object=ds$field_index, expected="1") }) diff --git a/utility/refresh.R b/utility/refresh.R index d4204e87..0de60422 100644 --- a/utility/refresh.R +++ b/utility/refresh.R @@ -40,7 +40,7 @@ devtools::run_examples(); #dev.off() #This overwrites the NAMESPACE file too # pkgload::load_all() test_results_checked <- devtools::test() test_results_checked <- devtools::test(filter = "read-batch-survey") -test_results_checked <- devtools::test(filter = "^metadata-coltypes") +test_results_checked <- devtools::test(filter = "^validate") withr::local_envvar(ONLYREADTESTS = "true") test_results_checked <- devtools::test(filter = "write-batch") From 1b9cc852d24407fccb69a5a7e116c9f6b6d028dd Mon Sep 17 00:00:00 2001 From: Will Beasley Date: Mon, 10 Jul 2023 14:26:33 -0500 Subject: [PATCH 10/23] better design for `validate_no_logical()` accept a data.frame instead of a vector of data types ref #485 --- R/validate.R | 14 ++++++-------- man/validate.Rd | 5 +---- tests/testthat/test-validate-no-logical.R | 6 +++--- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/R/validate.R b/R/validate.R index c2df4efb..1218ebfb 100644 --- a/R/validate.R +++ b/R/validate.R @@ -14,7 +14,7 @@ #' #' validate_data_frame_inherits( d ) #' -#' validate_no_logical( data_types, stop_on_error ) +#' validate_no_logical( d, stop_on_error ) #' #' validate_field_names( field_names, stop_on_error = FALSE ) #' @@ -33,8 +33,6 @@ #' @param d The [base::data.frame()] or [tibble::tibble()] #' containing the dataset used to update #' the REDCap project. -#' @param data_types The data types of the data frame corresponding -#' to the REDCap project. #' @param field_names The names of the fields/variables in the REDCap project. #' Each field is an individual element in the character vector. #' @param record_id_name The name of the field that represents one record. @@ -146,11 +144,11 @@ validate_data_frame_inherits <- function(d) { } #' @export -validate_no_logical <- function(data_types, stop_on_error = FALSE) { - checkmate::assert_character(data_types, any.missing=FALSE, min.len=1, min.chars=2) +validate_no_logical <- function(d, stop_on_error = FALSE) { + checkmate::assert_data_frame(d) checkmate::assert_logical(stop_on_error, any.missing=FALSE, len=1) - indices <- which(data_types == "logical") + indices <- which(vapply(d, \(x) inherits(x, "logical"), logical(1))) if (length(indices) == 0L) { tibble::tibble( @@ -168,7 +166,7 @@ validate_no_logical <- function(data_types, stop_on_error = FALSE) { ) } else { tibble::tibble( - field_name = names(data_types)[indices], + field_name = colnames(d)[indices], field_index = as.character(indices), concern = "The REDCap API does not automatically convert boolean values to 0/1 values.", suggestion = "Convert the variable with the `as.integer()` function." @@ -324,7 +322,7 @@ validate_for_write <- function( # lst_concerns, # validate_no_logical(vapply(d, class, character(1))) # ) - lst_concerns[[length(lst_concerns) + 1L]] <- validate_no_logical(vapply(d, class, character(1))) + lst_concerns[[length(lst_concerns) + 1L]] <- validate_no_logical(d) } # browser() diff --git a/man/validate.Rd b/man/validate.Rd index 2a9ed8ff..3034bd1f 100644 --- a/man/validate.Rd +++ b/man/validate.Rd @@ -15,7 +15,7 @@ validate_for_write( d, convert_logical_to_integer ) validate_data_frame_inherits( d ) -validate_no_logical( data_types, stop_on_error ) +validate_no_logical( d, stop_on_error ) validate_field_names( field_names, stop_on_error = FALSE ) @@ -28,9 +28,6 @@ validate_uniqueness(d, record_id_name, stop_on_error) containing the dataset used to update the REDCap project.} -\item{data_types}{The data types of the data frame corresponding -to the REDCap project.} - \item{field_names}{The names of the fields/variables in the REDCap project. Each field is an individual element in the character vector.} diff --git a/tests/testthat/test-validate-no-logical.R b/tests/testthat/test-validate-no-logical.R index 83ce46fc..8fbcd1e5 100644 --- a/tests/testthat/test-validate-no-logical.R +++ b/tests/testthat/test-validate-no-logical.R @@ -12,19 +12,19 @@ ds_good <- data.frame( ) test_that("validate_no_logical -good", { - ds <- validate_no_logical(vapply(ds_good, class, character(1)), stop_on_error = TRUE) + ds <- validate_no_logical(ds_good, stop_on_error = TRUE) expect_equal(nrow(ds), 0) }) test_that("validate_no_logical -stop on error", { expect_error( - validate_no_logical(vapply(ds_bad, class, character(1)), stop_on_error = TRUE), + validate_no_logical(ds_bad, stop_on_error = TRUE), "1 field\\(s\\) were logical/boolean. The REDCap API does not automatically convert boolean values to 0/1 values. Convert the variable with the `as.integer\\(\\)` function." ) }) test_that("validate_no_logical -concern dataset", { - ds <- validate_no_logical(vapply(ds_bad, class, character(1))) + ds <- validate_no_logical(ds_bad) expect_equal(object=nrow(ds), expected=1, info="One logical field should be flagged") expect_equal(object=ds$field_name, expected="bad_logical") expect_equal(object=unname(ds$field_index), expected="2") From db11a8efb6d4ee2edcb08e543f7ef8c11c04ec9a Mon Sep 17 00:00:00 2001 From: Will Beasley Date: Tue, 11 Jul 2023 11:33:09 -0500 Subject: [PATCH 11/23] updating to data.frame I guess this was from when it used data.table to bind rows? Or maybe just a brain fart writing the documentation --- R/validate.R | 2 +- man/validate.Rd | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/R/validate.R b/R/validate.R index 1218ebfb..3244aa07 100644 --- a/R/validate.R +++ b/R/validate.R @@ -61,7 +61,7 @@ #' with one call. #' #' Currently it verifies that the dataset -#' * inherits from [data.table::data.table()]. +#' * inherits from [base::data.frame()]. #' * does not contain #' [logical](https://stat.ethz.ch/R-manual/R-devel/library/base/html/logical.html) #' values (because REDCap typically wants `0`/`1` values instead of diff --git a/man/validate.Rd b/man/validate.Rd index 3034bd1f..6391df53 100644 --- a/man/validate.Rd +++ b/man/validate.Rd @@ -65,7 +65,7 @@ with one call. Currently it verifies that the dataset \itemize{ -\item inherits from \code{\link[data.table:data.table]{data.table::data.table()}}. +\item inherits from \code{\link[base:data.frame]{base::data.frame()}}. \item does not contain \href{https://stat.ethz.ch/R-manual/R-devel/library/base/html/logical.html}{logical} values (because REDCap typically wants \code{0}/\code{1} values instead of From a7a39c8ba48035a7d92d74ce3b8c17e4f6c9c451 Mon Sep 17 00:00:00 2001 From: Will Beasley Date: Tue, 11 Jul 2023 11:43:23 -0500 Subject: [PATCH 12/23] better interface for `validate_field_names()` ref #485 --- R/validate.R | 14 +++++++------- man/validate.Rd | 5 +---- tests/testthat/test-validate-field-names.R | 15 +++++---------- 3 files changed, 13 insertions(+), 21 deletions(-) diff --git a/R/validate.R b/R/validate.R index 3244aa07..5813cb5b 100644 --- a/R/validate.R +++ b/R/validate.R @@ -16,7 +16,7 @@ #' #' validate_no_logical( d, stop_on_error ) #' -#' validate_field_names( field_names, stop_on_error = FALSE ) +#' validate_field_names( d, stop_on_error = FALSE ) #' #' validate_repeat_instance( d, stop_on_error ) #' @@ -33,8 +33,6 @@ #' @param d The [base::data.frame()] or [tibble::tibble()] #' containing the dataset used to update #' the REDCap project. -#' @param field_names The names of the fields/variables in the REDCap project. -#' Each field is an individual element in the character vector. #' @param record_id_name The name of the field that represents one record. #' The default name in REDCap is "record_id". #' @param stop_on_error If `TRUE`, an error is thrown for violations. @@ -52,7 +50,7 @@ #' * `field_index`: The position of the field. (For example, a value of #' '1' indicates the first column, while a '3' indicates the third column.) #' * `concern`: A description of the problem potentially caused by the `field`. -#' * `suggestion`: A *potential* solution to the concern. +#' * `suggestion`: A _potential_ solution to the concern. #' #' @details #' All functions listed in the Usage section above inspect a specific aspect @@ -175,11 +173,13 @@ validate_no_logical <- function(d, stop_on_error = FALSE) { } #' @export -validate_field_names <- function(field_names, stop_on_error = FALSE) { - checkmate::assert_character(field_names, any.missing=FALSE, null.ok=TRUE, min.len=1, min.chars=1) +validate_field_names <- function(d, stop_on_error = FALSE) { + checkmate::assert_data_frame(d) + # checkmate::assert_character(field_names, any.missing=FALSE, null.ok=TRUE, min.len=1, min.chars=1) checkmate::assert_logical(stop_on_error, any.missing=FALSE, len=1) pattern <- "^[a-z][0-9a-z_]*$" + field_names <- colnames(d) indices <- which(!grepl(pattern, x = field_names, perl = TRUE)) if (length(indices) == 0L) { @@ -311,7 +311,7 @@ validate_for_write <- function( lst_concerns <- list( validate_data_frame_inherits(d), - validate_field_names(colnames(d)), + validate_field_names(d), validate_uniqueness(d), validate_repeat_instance(d) ) diff --git a/man/validate.Rd b/man/validate.Rd index 6391df53..6606bd6c 100644 --- a/man/validate.Rd +++ b/man/validate.Rd @@ -17,7 +17,7 @@ validate_data_frame_inherits( d ) validate_no_logical( d, stop_on_error ) -validate_field_names( field_names, stop_on_error = FALSE ) +validate_field_names( d, stop_on_error = FALSE ) validate_repeat_instance( d, stop_on_error ) @@ -28,9 +28,6 @@ validate_uniqueness(d, record_id_name, stop_on_error) containing the dataset used to update the REDCap project.} -\item{field_names}{The names of the fields/variables in the REDCap project. -Each field is an individual element in the character vector.} - \item{record_id_name}{The name of the field that represents one record. The default name in REDCap is "record_id".} diff --git a/tests/testthat/test-validate-field-names.R b/tests/testthat/test-validate-field-names.R index 6541dadc..97694238 100644 --- a/tests/testthat/test-validate-field-names.R +++ b/tests/testthat/test-validate-field-names.R @@ -12,32 +12,27 @@ ds_good <- data.frame( ) test_that("validate_field_names -good", { - ds <- validate_field_names(colnames(ds_good)) + ds <- validate_field_names(ds_good) expect_equal(nrow(ds), 0) }) test_that("validate_field_names -stop on error", { expect_error( - validate_field_names(colnames(ds_bad), stop_on_error = TRUE), + validate_field_names(ds_bad, stop_on_error = TRUE), "1 field name\\(s\\) violated the naming rules. Only digits, lowercase letters, and underscores are allowed." ) }) test_that("validate_field_names -uppercase", { + d_bad <- data.frame(record_ID = 1:4) expect_error( - validate_field_names("record_ID", stop_on_error = TRUE), - "1 field name\\(s\\) violated the naming rules. Only digits, lowercase letters, and underscores are allowed." - ) -}) -test_that("validate_field_names -start underscore", { - expect_error( - validate_field_names("_record_id", stop_on_error = TRUE), + validate_field_names(d_bad, stop_on_error = TRUE), "1 field name\\(s\\) violated the naming rules. Only digits, lowercase letters, and underscores are allowed." ) }) test_that("validate_field_names -concern dataset", { - ds <- validate_field_names(colnames(ds_bad)) + ds <- validate_field_names(ds_bad) expect_equal(object=nrow(ds), expected=1, info="One uppercase field should be flagged") expect_equal(object=ds$field_name, expected="bad_Uppercase") expect_equal(object=ds$field_index, expected="3") From a9083a459a39b4b583cc64985a8815d8baccac06 Mon Sep 17 00:00:00 2001 From: Will Beasley Date: Tue, 11 Jul 2023 12:16:55 -0500 Subject: [PATCH 13/23] `validate_record_id_name()` ref #485 --- NAMESPACE | 1 + NEWS.md | 8 +++- R/validate.R | 45 +++++++++++++++++-- man/validate.Rd | 4 +- tests/testthat/test-validate-record-id-name.R | 39 ++++++++++++++++ 5 files changed, 92 insertions(+), 5 deletions(-) create mode 100644 tests/testthat/test-validate-record-id-name.R diff --git a/NAMESPACE b/NAMESPACE index 0307c200..37196c95 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -45,6 +45,7 @@ export(validate_data_frame_inherits) export(validate_field_names) export(validate_for_write) export(validate_no_logical) +export(validate_record_id_name) export(validate_repeat_instance) export(validate_uniqueness) importFrom(magrittr,"%>%") diff --git a/NEWS.md b/NEWS.md index a40315cd..4b52a6c4 100644 --- a/NEWS.md +++ b/NEWS.md @@ -81,7 +81,13 @@ This will help extract forms from longitudinal & repeating projects. * `redcap_log_read()` now returns a new column reflecting the affected record id value (ref #478) * `redcap_read()` and `redcap_read_oneshot()` now remove "pseudofields" (e.g., `redcap_event_name`, `redcap_repeat_instrument`, & `redcap_repeat_instance`) from the `fields` parameter. Starting with REDCap v13.4.10, an error is thrown by the server. REDCap will return a message if a common pseudofield is requested explicitly by the user. (#477) * `redcap_event_instruments()` now can return mappings for all arms, instead of one arm per call.(Suggested by @januz, #482) -* `validate_for_write()` contains a few more checks, such as `validate_repeat_instance()` and `validate_data_frame_inherits()` (#485) +* `validate_for_write()` contains a few more checks. (#485) The complete list is now: + * `validate_data_frame_inherits()` + * `validate_field_names()` + * `validate_record_id_name()` + * `validate_uniqueness()` + * `validate_repeat_instance()` + * `validate_no_logical()` * `redcap_read()` checks the `event` parameter and throws an error if a value is not recognized, or the project is not longitudinal (#493) Version 1.1.0 (released 2022-08-10) diff --git a/R/validate.R b/R/validate.R index 5813cb5b..d499d15e 100644 --- a/R/validate.R +++ b/R/validate.R @@ -10,7 +10,7 @@ #' validate_uniqueness #' #' @usage -#' validate_for_write( d, convert_logical_to_integer ) +#' validate_for_write( d, convert_logical_to_integer, record_id_name ) #' #' validate_data_frame_inherits( d ) #' @@ -18,6 +18,8 @@ #' #' validate_field_names( d, stop_on_error = FALSE ) #' +#' validate_record_id_name( d, record_id_name = "record_id", stop_on_error = FALSE ) +#' #' validate_repeat_instance( d, stop_on_error ) #' #' validate_uniqueness(d, record_id_name, stop_on_error) @@ -206,6 +208,39 @@ validate_field_names <- function(d, stop_on_error = FALSE) { } } +#' @export +validate_record_id_name <- function ( + d, + record_id_name = "record_id", + stop_on_error = FALSE +) { + checkmate::assert_data_frame(d) + checkmate::assert_character(record_id_name, len = 1L, any.missing = FALSE, min.chars = 1L) + checkmate::assert_logical(stop_on_error, any.missing = FALSE, len = 1) + + record_id_found <- (record_id_name %in% colnames(d)) + + if (record_id_found) { + tibble::tibble( + field_name = character(0), + field_index = character(0), + concern = character(0), + suggestion = character(0) + ) + } else if (stop_on_error) { + "The field called `%s` is not found in the dataset.\nAdjust the value passed to the `record_id_name` if this isn't the correct named used by your specific REDCap project." |> + sprintf(record_id_name) |> + stop() + } else { + tibble::tibble( + field_name = record_id_name, + field_index = NA_character_, + concern = "The field is not found in the dataset.", + suggestion = "Adjust the value passed to the `record_id_name` if this isn't the correct named used by your specific REDCap project." + ) + } +} + #' @export validate_repeat_instance <- function(d, stop_on_error = FALSE) { checkmate::assert_data_frame(d) @@ -248,6 +283,7 @@ validate_repeat_instance <- function(d, stop_on_error = FALSE) { #' @export validate_uniqueness <- function(d, record_id_name = "record_id", stop_on_error = FALSE) { checkmate::assert_data_frame(d) + checkmate::assert_character(record_id_name, len = 1L, any.missing = FALSE, min.chars = 1L) count_of_records <- NULL plumbing <- c(record_id_name, "redcap_event_name", "redcap_repeat_instrument", "redcap_repeat_instance") @@ -304,15 +340,18 @@ validate_uniqueness <- function(d, record_id_name = "record_id", stop_on_error = #' @export validate_for_write <- function( d, - convert_logical_to_integer = FALSE + convert_logical_to_integer = FALSE, + record_id_name = "record_id" ) { # checkmate::assert_data_frame(d, any.missing = TRUE, null.ok = FALSE) checkmate::assert_logical(convert_logical_to_integer, any.missing = FALSE, len = 1) + checkmate::assert_character(record_id_name, len = 1L, any.missing = FALSE, min.chars = 1L) lst_concerns <- list( validate_data_frame_inherits(d), validate_field_names(d), - validate_uniqueness(d), + validate_record_id_name(d), + validate_uniqueness(d, record_id_name = record_id_name), validate_repeat_instance(d) ) diff --git a/man/validate.Rd b/man/validate.Rd index 6606bd6c..7c6a1220 100644 --- a/man/validate.Rd +++ b/man/validate.Rd @@ -11,7 +11,7 @@ \title{Inspect a dataset to anticipate problems before writing to a REDCap project} \usage{ -validate_for_write( d, convert_logical_to_integer ) +validate_for_write( d, convert_logical_to_integer, record_id_name ) validate_data_frame_inherits( d ) @@ -19,6 +19,8 @@ validate_no_logical( d, stop_on_error ) validate_field_names( d, stop_on_error = FALSE ) +validate_record_id_name( d, record_id_name = "record_id", stop_on_error = FALSE ) + validate_repeat_instance( d, stop_on_error ) validate_uniqueness(d, record_id_name, stop_on_error) diff --git a/tests/testthat/test-validate-record-id-name.R b/tests/testthat/test-validate-record-id-name.R new file mode 100644 index 00000000..7f6c477d --- /dev/null +++ b/tests/testthat/test-validate-record-id-name.R @@ -0,0 +1,39 @@ +library(testthat) + +test_that("validate_record_id_name: default", { + d1 <- data.frame( + record_id = 1:4, + flag_logical = c(TRUE, TRUE, FALSE, TRUE), + flag_Uppercase = c(4, 6, 8, 2) + ) + + ds <- validate_record_id_name(d1) + expect_equal(object = nrow(ds), expected = 0) +}) + +test_that("validate_record_id_name: nondefault", { + d1 <- data.frame( + pt_id = 1:4, + flag_logical = c(TRUE, TRUE, FALSE, TRUE), + flag_Uppercase = c(4, 6, 8, 2) + ) + + ds <- validate_record_id_name(d1, record_id_name = "pt_id") + expect_equal(object = nrow(ds), expected = 0) +}) + + +test_that("validate_repeat_instance -stopping", { + expect_error( + validate_record_id_name(mtcars, stop_on_error = TRUE), + "The field called `record_id` is not found in the dataset\\." + ) +}) + +test_that("validate_repeat_instance -not stopping", { + ds <- validate_record_id_name(mtcars, stop_on_error = FALSE) + + expect_equal(object=nrow(ds), expected=1) + expect_equal(object=ds$field_name, expected="record_id") + expect_true(is.na(ds$field_index)) +}) From 85456f0732f79bb82683d2d92c92a6727e757d85 Mon Sep 17 00:00:00 2001 From: Will Beasley Date: Tue, 11 Jul 2023 12:23:27 -0500 Subject: [PATCH 14/23] consistent `stop_on_error` checking ref #485 --- R/validate.R | 16 ++++++++-------- man/validate.Rd | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/R/validate.R b/R/validate.R index d499d15e..8b894f30 100644 --- a/R/validate.R +++ b/R/validate.R @@ -14,15 +14,15 @@ #' #' validate_data_frame_inherits( d ) #' -#' validate_no_logical( d, stop_on_error ) +#' validate_no_logical( d, stop_on_error = FALSE ) #' #' validate_field_names( d, stop_on_error = FALSE ) #' #' validate_record_id_name( d, record_id_name = "record_id", stop_on_error = FALSE ) #' -#' validate_repeat_instance( d, stop_on_error ) +#' validate_repeat_instance( d, stop_on_error = FALSE ) #' -#' validate_uniqueness(d, record_id_name, stop_on_error) +#' validate_uniqueness( d, record_id_name, stop_on_error = FALSE) #' #' @title #' Inspect a dataset to anticipate problems before @@ -146,7 +146,7 @@ validate_data_frame_inherits <- function(d) { #' @export validate_no_logical <- function(d, stop_on_error = FALSE) { checkmate::assert_data_frame(d) - checkmate::assert_logical(stop_on_error, any.missing=FALSE, len=1) + checkmate::assert_logical(stop_on_error, any.missing = FALSE, len = 1L) indices <- which(vapply(d, \(x) inherits(x, "logical"), logical(1))) @@ -177,8 +177,7 @@ validate_no_logical <- function(d, stop_on_error = FALSE) { #' @export validate_field_names <- function(d, stop_on_error = FALSE) { checkmate::assert_data_frame(d) - # checkmate::assert_character(field_names, any.missing=FALSE, null.ok=TRUE, min.len=1, min.chars=1) - checkmate::assert_logical(stop_on_error, any.missing=FALSE, len=1) + checkmate::assert_logical(stop_on_error, any.missing = FALSE, len = 1L) pattern <- "^[a-z][0-9a-z_]*$" field_names <- colnames(d) @@ -216,7 +215,7 @@ validate_record_id_name <- function ( ) { checkmate::assert_data_frame(d) checkmate::assert_character(record_id_name, len = 1L, any.missing = FALSE, min.chars = 1L) - checkmate::assert_logical(stop_on_error, any.missing = FALSE, len = 1) + checkmate::assert_logical(stop_on_error, any.missing = FALSE, len = 1L) record_id_found <- (record_id_name %in% colnames(d)) @@ -244,7 +243,7 @@ validate_record_id_name <- function ( #' @export validate_repeat_instance <- function(d, stop_on_error = FALSE) { checkmate::assert_data_frame(d) - checkmate::assert_logical(stop_on_error, any.missing = FALSE, len = 1) + checkmate::assert_logical(stop_on_error, any.missing = FALSE, len = 1L) column_name <- "redcap_repeat_instance" if (!any(colnames(d) == column_name)) { @@ -284,6 +283,7 @@ validate_repeat_instance <- function(d, stop_on_error = FALSE) { validate_uniqueness <- function(d, record_id_name = "record_id", stop_on_error = FALSE) { checkmate::assert_data_frame(d) checkmate::assert_character(record_id_name, len = 1L, any.missing = FALSE, min.chars = 1L) + checkmate::assert_logical(stop_on_error, any.missing = FALSE, len = 1L) count_of_records <- NULL plumbing <- c(record_id_name, "redcap_event_name", "redcap_repeat_instrument", "redcap_repeat_instance") diff --git a/man/validate.Rd b/man/validate.Rd index 7c6a1220..22b93d43 100644 --- a/man/validate.Rd +++ b/man/validate.Rd @@ -15,15 +15,15 @@ validate_for_write( d, convert_logical_to_integer, record_id_name ) validate_data_frame_inherits( d ) -validate_no_logical( d, stop_on_error ) +validate_no_logical( d, stop_on_error = FALSE ) validate_field_names( d, stop_on_error = FALSE ) validate_record_id_name( d, record_id_name = "record_id", stop_on_error = FALSE ) -validate_repeat_instance( d, stop_on_error ) +validate_repeat_instance( d, stop_on_error = FALSE ) -validate_uniqueness(d, record_id_name, stop_on_error) +validate_uniqueness( d, record_id_name, stop_on_error = FALSE) } \arguments{ \item{d}{The \code{\link[base:data.frame]{base::data.frame()}} or \code{\link[tibble:tibble]{tibble::tibble()}} From c0250d76c0ef91b455a05e1d8700badeb38e81c4 Mon Sep 17 00:00:00 2001 From: Will Beasley Date: Tue, 11 Jul 2023 12:33:02 -0500 Subject: [PATCH 15/23] slightly more streamlined invert grep ref #485 --- R/validate.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/validate.R b/R/validate.R index 8b894f30..06c0bd87 100644 --- a/R/validate.R +++ b/R/validate.R @@ -182,7 +182,7 @@ validate_field_names <- function(d, stop_on_error = FALSE) { pattern <- "^[a-z][0-9a-z_]*$" field_names <- colnames(d) - indices <- which(!grepl(pattern, x = field_names, perl = TRUE)) + indices <- grep(pattern, x = field_names, perl = TRUE, invert = TRUE) if (length(indices) == 0L) { tibble::tibble( field_name = character(0), From b54b934a10dd639b320923f3aa75b14fe04cbb8b Mon Sep 17 00:00:00 2001 From: Will Beasley Date: Tue, 11 Jul 2023 12:50:41 -0500 Subject: [PATCH 16/23] shift to `assert_field_names()` for internal checks ref #485 --- R/redcap-file-download-oneshot.R | 2 +- R/redcap-file-upload-oneshot.R | 2 +- R/redcap-metadata-read.R | 2 +- R/redcap-read-eav-oneshot.R | 2 +- R/redcap-read-oneshot-eav.R | 2 +- R/redcap-read-oneshot.R | 2 +- R/redcap-read.R | 2 +- R/validate.R | 19 +++++++++++++++++++ tests/testthat/test-validate-field-names.R | 14 ++++++++++++++ 9 files changed, 40 insertions(+), 7 deletions(-) diff --git a/R/redcap-file-download-oneshot.R b/R/redcap-file-download-oneshot.R index 8c876dff..77b58d42 100644 --- a/R/redcap-file-download-oneshot.R +++ b/R/redcap-file-download-oneshot.R @@ -142,7 +142,7 @@ redcap_file_download_oneshot <- function( record <- as.character(record) checkmate::assert_character(record , any.missing=FALSE, len=1, pattern="^.{1,}$") checkmate::assert_character(field , any.missing=FALSE, len=1, pattern="^.{1,}$") - validate_field_names(field, stop_on_error = TRUE) + assert_field_names(field) checkmate::assert_character(event , any.missing=FALSE, len=1, pattern="^.{0,}$") checkmate::assert_logical( verbose , any.missing=FALSE) diff --git a/R/redcap-file-upload-oneshot.R b/R/redcap-file-upload-oneshot.R index d739677c..9aebe026 100644 --- a/R/redcap-file-upload-oneshot.R +++ b/R/redcap-file-upload-oneshot.R @@ -124,7 +124,7 @@ redcap_file_upload_oneshot <- function( checkmate::assert_character(redcap_uri , any.missing=FALSE, len=1, pattern="^.{1,}$") checkmate::assert_character(token , any.missing=FALSE, len=1, pattern="^.{1,}$") checkmate::assert_character(field , any.missing=FALSE, len=1, pattern="^.{1,}$") - validate_field_names(field, stop_on_error = TRUE) + assert_field_names(field) checkmate::assert_character(event , any.missing=FALSE, len=1, pattern="^.{0,}$") checkmate::assert_logical( verbose , any.missing=FALSE) diff --git a/R/redcap-metadata-read.R b/R/redcap-metadata-read.R index ae9f6382..d60fb754 100644 --- a/R/redcap-metadata-read.R +++ b/R/redcap-metadata-read.R @@ -89,7 +89,7 @@ redcap_metadata_read <- function( checkmate::assert_character(redcap_uri , any.missing=FALSE, len=1, pattern="^.{1,}$") checkmate::assert_character(token , any.missing=FALSE, len=1, pattern="^.{1,}$") - validate_field_names(fields, stop_on_error = TRUE) + assert_field_names(fields) token <- sanitize_token(token) fields_collapsed <- collapse_vector(fields) diff --git a/R/redcap-read-eav-oneshot.R b/R/redcap-read-eav-oneshot.R index 6bfa2fe6..478d6f33 100644 --- a/R/redcap-read-eav-oneshot.R +++ b/R/redcap-read-eav-oneshot.R @@ -227,7 +227,7 @@ redcap_read_eav_oneshot <- function( checkmate::assert_list( config_options , any.missing=TRUE , null.ok=TRUE) # checkmate::assert_character(encode_httr , any.missing=FALSE, len=1, null.ok = FALSE) - validate_field_names(fields, stop_on_error = TRUE) + assert_field_names(fields) token <- sanitize_token(token) records_collapsed <- collapse_vector(records) diff --git a/R/redcap-read-oneshot-eav.R b/R/redcap-read-oneshot-eav.R index 6be62f28..d9822fa2 100644 --- a/R/redcap-read-oneshot-eav.R +++ b/R/redcap-read-oneshot-eav.R @@ -195,7 +195,7 @@ redcap_read_oneshot_eav <- function( checkmate::assert_logical( verbose , any.missing=FALSE, len=1, null.ok=TRUE) checkmate::assert_list( config_options , any.missing=TRUE , null.ok=TRUE) - validate_field_names(fields, stop_on_error = TRUE) + assert_field_names(fields) token <- sanitize_token(token) records_collapsed <- collapse_vector(records) diff --git a/R/redcap-read-oneshot.R b/R/redcap-read-oneshot.R index 8bf6cac0..70b52da6 100644 --- a/R/redcap-read-oneshot.R +++ b/R/redcap-read-oneshot.R @@ -253,7 +253,7 @@ redcap_read_oneshot <- function( checkmate::assert_list( config_options , any.missing=TRUE , null.ok=TRUE) # checkmate::assert_character(encode_httr , any.missing=FALSE, len=1, null.ok = FALSE) - validate_field_names(fields, stop_on_error = TRUE) + assert_field_names(fields) pseudofields <- c( "redcap_event_name", diff --git a/R/redcap-read.R b/R/redcap-read.R index 0439f039..683b34c2 100644 --- a/R/redcap-read.R +++ b/R/redcap-read.R @@ -304,7 +304,7 @@ redcap_read <- function( checkmate::assert_list( config_options , any.missing=TRUE , null.ok=TRUE) checkmate::assert_integer( id_position , any.missing=FALSE, len=1, lower=1L) - validate_field_names(fields, stop_on_error = TRUE) + assert_field_names(fields) token <- sanitize_token(token) filter_logic <- filter_logic_prepare(filter_logic) diff --git a/R/validate.R b/R/validate.R index 06c0bd87..58e40784 100644 --- a/R/validate.R +++ b/R/validate.R @@ -207,6 +207,25 @@ validate_field_names <- function(d, stop_on_error = FALSE) { } } +# Intentionally not exported +assert_field_names <- function(field_names) { + checkmate::assert_character(field_names, any.missing=FALSE, null.ok=TRUE, min.len=1, min.chars=1) + pattern <- "^[a-z][0-9a-z_]*$" + + bad_names <- grep(pattern, x = field_names, perl = TRUE, invert = TRUE) + + if (0L < length(bad_names)) { + paste( + "%i field name(s) violated the naming rules. Only digits, lowercase ", + "letters, and underscores are allowed. The variable must start with ", + "a letter. The bad names are {%s}.", + collapse = "" + ) %>% + sprintf(length(bad_names), paste(bad_names, collapse = ", ")) %>% + stop() + } +} + #' @export validate_record_id_name <- function ( d, diff --git a/tests/testthat/test-validate-field-names.R b/tests/testthat/test-validate-field-names.R index 97694238..2a7cc703 100644 --- a/tests/testthat/test-validate-field-names.R +++ b/tests/testthat/test-validate-field-names.R @@ -37,3 +37,17 @@ test_that("validate_field_names -concern dataset", { expect_equal(object=ds$field_name, expected="bad_Uppercase") expect_equal(object=ds$field_index, expected="3") }) + + +test_that("assert_field_names -good", { + expect_no_condition( + assert_field_names(colnames(ds_good)) + ) +}) + +test_that("assert_field_names -bad", { + expect_error( + assert_field_names(colnames(ds_bad)), + "1 field name\\(s\\) violated the naming rules\\." + ) +}) From e0387bf8f93287a5746f1877246e917d566ec1f4 Mon Sep 17 00:00:00 2001 From: Will Beasley Date: Tue, 11 Jul 2023 13:14:21 -0500 Subject: [PATCH 17/23] detailed description of validation checks ref #485 --- R/validate.R | 47 ++++++++++++++++++++++++++++++++++++----------- man/validate.Rd | 37 ++++++++++++++++++++++++++++--------- 2 files changed, 64 insertions(+), 20 deletions(-) diff --git a/R/validate.R b/R/validate.R index 58e40784..3dfe2e74 100644 --- a/R/validate.R +++ b/R/validate.R @@ -60,16 +60,41 @@ #' individual validation checks. It allows the client to check everything #' with one call. #' -#' Currently it verifies that the dataset -#' * inherits from [base::data.frame()]. -#' * does not contain -#' [logical](https://stat.ethz.ch/R-manual/R-devel/library/base/html/logical.html) -#' values (because REDCap typically wants `0`/`1` values instead of -#' `FALSE`/`TRUE`). -#' * starts with a lowercase letter, and subsequent optional characters are a -#' sequence of (a) lowercase letters, (b) digits 0-9, and/or (c) underscores. -#' (The exact regex is `^[a-z][0-9a-z_]*$`.) -#' * has an integer for `redcap_repeat_instance`, if the column is present. +#' Currently, the individual checks include: +#' +#' 1. `validate_data_frame_inherits(d)`: +#' `d` inherits from [base::data.frame()] +#' +#' 1. `validate_field_names(d)`: +#' The columns of `d` +#' start with a lowercase letter, and subsequent optional characters are a +#' sequence of (a) lowercase letters, (b) digits 0-9, and/or (c) underscores. +#' (The exact regex is `^[a-z][0-9a-z_]*$`.) +#' +#' 1. `validate_record_id_name(d)`: +#' `d` contains a field called "record_id", +#' or whatever value was passed to `record_id_name`. +#' +#' 1. `validate_no_logical(d)` (unless `convert_logical_to_integer` is TRUE): +#' `d` does not contain +#' [logical](https://stat.ethz.ch/R-manual/R-devel/library/base/html/logical.html) +#' values (because REDCap typically wants `0`/`1` values instead of +#' `FALSE`/`TRUE`). +#' +#' 1. `validate_repeat_instance(d)`: +#' `d` has an integer for `redcap_repeat_instance`, if the column is present. +#' +#' 1. `validate_uniqueness(d, record_id_name = record_id_name)`: +#' `d` does not contain multiple rows with duplicate values of +#' `record_id`, +#' `redcap_event_name`, +#' `redcap_repeat_instrument`, and +#' `redcap_repeat_instance` +#' (depending on the longitudinal & repeating structure of the project). +#' +#' Technically duplicate rows are not errors, +#' but we feel that this will almost always be unintentional, +#' and lead to an irrecoverable corruption of the data. #' #' If you encounter additional types of problems when attempting to write to #' REDCap, please tell us by creating a @@ -123,7 +148,7 @@ #' ~record_id, ~redcap_event_name, ~redcap_repeat_instrument, ~redcap_repeat_instance, #' 1L, "e1", "i1", 1L, #' 1L, "e1", "i1", 3L, -#' 1L, "e1", "i1", 3L, +#' 1L, "e1", "i1", 3L, # Notice this duplicates the row above #' ) #' # validate_uniqueness(d3) #' # Throws error: diff --git a/man/validate.Rd b/man/validate.Rd index 22b93d43..19fa15e5 100644 --- a/man/validate.Rd +++ b/man/validate.Rd @@ -62,17 +62,36 @@ of the dataset. The \code{\link[=validate_for_write]{validate_for_write()}} fun individual validation checks. It allows the client to check everything with one call. -Currently it verifies that the dataset -\itemize{ -\item inherits from \code{\link[base:data.frame]{base::data.frame()}}. -\item does not contain +Currently, the individual checks include: +\enumerate{ +\item \code{validate_data_frame_inherits(d)}: +\code{d} inherits from \code{\link[base:data.frame]{base::data.frame()}} +\item \code{validate_field_names(d)}: +The columns of \code{d} +start with a lowercase letter, and subsequent optional characters are a +sequence of (a) lowercase letters, (b) digits 0-9, and/or (c) underscores. +(The exact regex is \verb{^[a-z][0-9a-z_]*$}.) +\item \code{validate_record_id_name(d)}: +\code{d} contains a field called "record_id", +or whatever value was passed to \code{record_id_name}. +\item \code{validate_no_logical(d)} (unless \code{convert_logical_to_integer} is TRUE): +\code{d} does not contain \href{https://stat.ethz.ch/R-manual/R-devel/library/base/html/logical.html}{logical} values (because REDCap typically wants \code{0}/\code{1} values instead of \code{FALSE}/\code{TRUE}). -\item starts with a lowercase letter, and subsequent optional characters are a -sequence of (a) lowercase letters, (b) digits 0-9, and/or (c) underscores. -(The exact regex is \verb{^[a-z][0-9a-z_]*$}.) -\item has an integer for \code{redcap_repeat_instance}, if the column is present. +\item \code{validate_repeat_instance(d)}: +\code{d} has an integer for \code{redcap_repeat_instance}, if the column is present. +\item \code{validate_uniqueness(d, record_id_name = record_id_name)}: +\code{d} does not contain multiple rows with duplicate values of +\code{record_id}, +\code{redcap_event_name}, +\code{redcap_repeat_instrument}, and +\code{redcap_repeat_instance} +(depending on the longitudinal & repeating structure of the project). + +Technically duplicate rows are not errors, +but we feel that this will almost always be unintentional, +and lead to an irrecoverable corruption of the data. } If you encounter additional types of problems when attempting to write to @@ -116,7 +135,7 @@ d3 <- tibble::tribble( ~record_id, ~redcap_event_name, ~redcap_repeat_instrument, ~redcap_repeat_instance, 1L, "e1", "i1", 1L, 1L, "e1", "i1", 3L, - 1L, "e1", "i1", 3L, + 1L, "e1", "i1", 3L, # Notice this duplicates the row above ) # validate_uniqueness(d3) # Throws error: From 753ae46e4904dc6f708e864734774dac482a9495 Mon Sep 17 00:00:00 2001 From: Will Beasley Date: Tue, 11 Jul 2023 13:22:12 -0500 Subject: [PATCH 18/23] lintr --- R/validate.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/validate.R b/R/validate.R index 3dfe2e74..76b302c1 100644 --- a/R/validate.R +++ b/R/validate.R @@ -252,7 +252,7 @@ assert_field_names <- function(field_names) { } #' @export -validate_record_id_name <- function ( +validate_record_id_name <- function( d, record_id_name = "record_id", stop_on_error = FALSE From 18ba050fc3beb203a6237ce39a6d3953264ce979 Mon Sep 17 00:00:00 2001 From: Will Beasley Date: Tue, 11 Jul 2023 13:24:41 -0500 Subject: [PATCH 19/23] missing alias ref #485 --- R/validate.R | 1 + man/validate.Rd | 1 + 2 files changed, 2 insertions(+) diff --git a/R/validate.R b/R/validate.R index 76b302c1..18c4341d 100644 --- a/R/validate.R +++ b/R/validate.R @@ -6,6 +6,7 @@ #' validate_data_frame_inherits #' validate_no_logical #' validate_field_names +#' validate_record_id_name #' validate_repeat_instance #' validate_uniqueness #' diff --git a/man/validate.Rd b/man/validate.Rd index 19fa15e5..49764b8a 100644 --- a/man/validate.Rd +++ b/man/validate.Rd @@ -6,6 +6,7 @@ \alias{validate_for_write} \alias{validate_no_logical} \alias{validate_field_names} +\alias{validate_record_id_name} \alias{validate_repeat_instance} \alias{validate_uniqueness} \title{Inspect a dataset to anticipate problems before From 439700e926296675504ec68ae9846dcb59996b2b Mon Sep 17 00:00:00 2001 From: Will Beasley Date: Tue, 11 Jul 2023 13:59:10 -0500 Subject: [PATCH 20/23] avoid twitter urls that break https://www.bleepingcomputer.com/news/technology/twitter-now-forces-you-to-sign-in-to-view-tweets/ --- README.md | 4 +++- vignettes/workflow-read.Rmd | 8 ++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3c9da232..895bfb73 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,9 @@ Much of this package has been developed to support the needs of the following pr * Additional Institutional Support from OUHSC [Dept of Pediatrics](https://medicine.ouhsc.edu/Academic-Departments/Pediatrics); 2013-2021. Thanks, -[Will Beasley](https://www.researchgate.net/profile/William-Beasley-5), David Bard, & Thomas Wilson
+[Will Beasley](https://orcid.org/0000-0002-5613-5006), +[David Bard](https://orcid.org/0000-0002-3922-8489), +& Thomas Wilson
[University of Oklahoma Health Sciences Center](https://www.ouhsc.edu/), [Department of Pediatrics](https://medicine.ouhsc.edu/Academic-Departments/Pediatrics), [Biomedical & Behavioral Research Core](https://www.ouhsc.edu/BBMC/). diff --git a/vignettes/workflow-read.Rmd b/vignettes/workflow-read.Rmd index 5c23d421..8521e35a 100644 --- a/vignettes/workflow-read.Rmd +++ b/vignettes/workflow-read.Rmd @@ -302,11 +302,15 @@ By default, `REDCapR::redcap_read()` requests datasets of 100 patients as a time Writing to the Server ------------------------- -Reading record data is only one API capability. REDCapR [exposes 20+ API functions](https://ouhscbbmc.github.io/REDCapR/reference/), such as reading metadata, retrieving survey links, and writing records back to REDCap. This last operation is relevant in [Kenneth McLean](https://twitter.com/kennethmclean92)'s presentation following a five-minute break. +Reading record data is only one API capability. REDCapR [exposes 20+ API functions](https://ouhscbbmc.github.io/REDCapR/reference/), such as reading metadata, retrieving survey links, and writing records back to REDCap. This last operation is relevant in +[Kenneth McLean](https://orcid.org/0000-0001-6482-9086)'s presentation following a five-minute break. Notes =================================== -This vignette was originally designed for a 2021 R/Medicine REDCap workshop with [Peter Higgins](https://www.med.umich.edu/higginslab/), [Amanda Miller](https://coloradosph.cuanschutz.edu/resources/directory/directory-profile/Miller-Amanda-UCD6000053152), and [Kenneth McLean](https://twitter.com/kennethmclean92). +This vignette was originally designed for a 2021 R/Medicine REDCap workshop with +[Peter Higgins](https://scholar.google.com/citations?user=UGJGFaAAAAAJ&hl=en), +[Amanda Miller](https://coloradosph.cuanschutz.edu/resources/directory/directory-profile/Miller-Amanda-UCD6000053152), +and [Kenneth McLean](https://orcid.org/0000-0001-6482-9086). This work was made possible in part by the NIH grant [U54GM104938](https://taggs.hhs.gov/Detail/AwardDetail?arg_AwardNum=U54GM104938&arg_ProgOfficeCode=127) to the [Oklahoma Shared Clinical and Translational Resource)](http://osctr.ouhsc.edu). From 511928db6b31f9ab84bcc39dbc24086485a3b407 Mon Sep 17 00:00:00 2001 From: Will Beasley Date: Tue, 11 Jul 2023 14:11:57 -0500 Subject: [PATCH 21/23] update community urls i guess the server url changed --- NEWS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NEWS.md b/NEWS.md index 4b52a6c4..bd86d2d8 100644 --- a/NEWS.md +++ b/NEWS.md @@ -74,8 +74,8 @@ This will help extract forms from longitudinal & repeating projects. * `redcap_file_upload_oneshot()` to `redcap_file_upload_opneshot()` * `redcap_download_instrument()` to `redcap_instrument_download()` -* `redcap_dag_read()` has new `data_access_group_id` field (introduced maybe in [13.1.0](https://community.projectredcap.org/articles/13/index.html)) (#459) -* `redcap_users_export()` has new `mycap_participants` field (introduced maybe in [13.0.0](https://community.projectredcap.org/articles/13/index.html)) (#459) +* `redcap_dag_read()` has new `data_access_group_id` field (introduced maybe in [13.1.0](https://redcap.vanderbilt.edu/community/post.php?id=13)) (#459) +* `redcap_users_export()` has new `mycap_participants` field (introduced maybe in [13.0.0](https://redcap.vanderbilt.edu/community/post.php?id=13)) (#459) * Accommodate older versions of REDCap that don't return project-level variable, like `has_repeating_instruments_or_events`, `missing_data_codes`, `external_modules`, `bypass_branching_erase_field_prompt` (@the-mad-statter, #465, #466) * `redcap_meta_coltypes()` correctly determines data type for autonumber `record_id` fields. It suggests a character if the project has DAGs, and an integer if not. (@pwildenhain, #472) * `redcap_log_read()` now returns a new column reflecting the affected record id value (ref #478) From cc6da98265b67fc6235553fe740656dee4083ef5 Mon Sep 17 00:00:00 2001 From: Will Beasley Date: Tue, 11 Jul 2023 16:40:00 -0500 Subject: [PATCH 22/23] verify `record_id` not returned by `redcap_read_oneshot()` when not requested ref #471 --- .../specify-fields-without-record-id.R | 11 ++++ .../specify-forms-without-record-id.R | 16 ++++++ tests/testthat/test-read-oneshot.R | 56 +++++++++++++++++++ 3 files changed, 83 insertions(+) create mode 100644 inst/test-data/specific-redcapr/read-oneshot/specify-fields-without-record-id.R create mode 100644 inst/test-data/specific-redcapr/read-oneshot/specify-forms-without-record-id.R diff --git a/inst/test-data/specific-redcapr/read-oneshot/specify-fields-without-record-id.R b/inst/test-data/specific-redcapr/read-oneshot/specify-fields-without-record-id.R new file mode 100644 index 00000000..d01b1c6d --- /dev/null +++ b/inst/test-data/specific-redcapr/read-oneshot/specify-fields-without-record-id.R @@ -0,0 +1,11 @@ +structure(list(name_first = c("Nutmeg", "Tumtum", "Marcus", "Trudy", +"John Lee"), address = c("14 Rose Cottage St.\nKenning UK, 323232", +"14 Rose Cottage Blvd.\nKenning UK 34243", "243 Hill St.\nGuthrie OK 73402", +"342 Elm\nDuncanville TX, 75116", "Hotel Suite\nNew Orleans LA, 70115" +), interpreter_needed = c(0, 0, 1, NA, 0)), row.names = c(NA, +-5L), spec = structure(list(cols = list(name_first = structure(list(), class = c("collector_character", +"collector")), address = structure(list(), class = c("collector_character", +"collector")), interpreter_needed = structure(list(), class = c("collector_double", +"collector"))), default = structure(list(), class = c("collector_guess", +"collector")), delim = ","), class = "col_spec"), class = c("spec_tbl_df", +"tbl_df", "tbl", "data.frame")) diff --git a/inst/test-data/specific-redcapr/read-oneshot/specify-forms-without-record-id.R b/inst/test-data/specific-redcapr/read-oneshot/specify-forms-without-record-id.R new file mode 100644 index 00000000..cfc90805 --- /dev/null +++ b/inst/test-data/specific-redcapr/read-oneshot/specify-forms-without-record-id.R @@ -0,0 +1,16 @@ +structure(list(height = c(7, 6, 180, 165, 193.04), weight = c(1, +1, 80, 54, 104), bmi = c(204.1, 277.8, 24.7, 19.8, 27.9), comments = c("Character in a book, with some guessing", +"A mouse character from a good book", "completely made up", "This record doesn't have a DAG assigned\n\nSo call up Trudy on the telephone\nSend her a letter in the mail", +"Had a hand for trouble and a eye for cash\n\nHe had a gold watch chain and a black mustache" +), mugshot = c("mugshot-1.jpg", "mugshot-2.jpg", "mugshot-3.jpg", +"mugshot-4.jpg", "mugshot-5.jpg"), health_complete = c(1, 0, +2, 2, 0)), row.names = c(NA, -5L), spec = structure(list(cols = list( + height = structure(list(), class = c("collector_double", + "collector")), weight = structure(list(), class = c("collector_double", + "collector")), bmi = structure(list(), class = c("collector_double", + "collector")), comments = structure(list(), class = c("collector_character", + "collector")), mugshot = structure(list(), class = c("collector_character", + "collector")), health_complete = structure(list(), class = c("collector_double", + "collector"))), default = structure(list(), class = c("collector_guess", +"collector")), delim = ","), class = "col_spec"), class = c("spec_tbl_df", +"tbl_df", "tbl", "data.frame")) diff --git a/tests/testthat/test-read-oneshot.R b/tests/testthat/test-read-oneshot.R index 8ee1b260..f3edceb0 100644 --- a/tests/testthat/test-read-oneshot.R +++ b/tests/testthat/test-read-oneshot.R @@ -246,6 +246,62 @@ test_that("specify-forms", { expect_s3_class(returned_object$data, "tbl") }) +test_that("specify-forms-without-record-id", { + testthat::skip_on_cran() + path_expected <- "test-data/specific-redcapr/read-oneshot/specify-forms-without-record-id.R" + desired_forms <- c("health") + expected_outcome_message <- "\\d+ records and \\d+ columns were read from REDCap in \\d+(\\.\\d+\\W|\\W)seconds\\." + + returned_object <- + redcap_read_oneshot( + redcap_uri = credential$redcap_uri, + token = credential$token, + forms = desired_forms, + verbose = FALSE + ) + + if (update_expectation) save_expected(returned_object$data, path_expected) + expected_data_frame <- retrieve_expected(path_expected) + + expect_equal(returned_object$data, expected=expected_data_frame, label="The returned data.frame should be correct", ignore_attr = TRUE) # dput(returned_object$data) + expect_equal(returned_object$status_code, expected=200L) + expect_equal(returned_object$raw_text, expected="", ignore_attr = TRUE) # dput(returned_object$raw_text) + expect_true(returned_object$records_collapsed=="", "A subset of records was not requested.") + expect_true(returned_object$fields_collapsed=="", "A subset of fields was not requested.") + expect_true(returned_object$filter_logic=="", "A filter was not specified.") + expect_match(returned_object$outcome_message, regexp=expected_outcome_message, perl=TRUE) + expect_true(returned_object$success) + + expect_s3_class(returned_object$data, "tbl") +}) +test_that("specify-fields-without-record-id", { + testthat::skip_on_cran() + path_expected <- "test-data/specific-redcapr/read-oneshot/specify-fields-without-record-id.R" + desired_fields <- c("name_first", "address", "interpreter_needed") + expected_outcome_message <- "\\d+ records and \\d+ columns were read from REDCap in \\d+(\\.\\d+\\W|\\W)seconds\\." + + returned_object <- + redcap_read_oneshot( + redcap_uri = credential$redcap_uri, + token = credential$token, + fields = desired_fields, + verbose = FALSE + ) + + if (update_expectation) save_expected(returned_object$data, path_expected) + expected_data_frame <- retrieve_expected(path_expected) + + expect_equal(returned_object$data, expected=expected_data_frame, label="The returned data.frame should be correct", ignore_attr = TRUE) # dput(returned_object$data) + expect_equal(returned_object$status_code, expected=200L) + expect_equal(returned_object$raw_text, expected="", ignore_attr = TRUE) # dput(returned_object$raw_text) + expect_true(returned_object$records_collapsed=="", "A subset of records was not requested.") + expect_equal(returned_object$fields_collapsed, paste(desired_fields, collapse = ",")) + expect_true(returned_object$filter_logic=="", "A filter was not specified.") + expect_match(returned_object$outcome_message, regexp=expected_outcome_message, perl=TRUE) + expect_true(returned_object$success) + + expect_s3_class(returned_object$data, "tbl") +}) test_that("specify-forms-only-1st", { testthat::skip_on_cran() path_expected <- "test-data/specific-redcapr/read-oneshot/specify-forms-only-1st.R" From ab4c58a889acfd968b1883d676450c3db934ef7a Mon Sep 17 00:00:00 2001 From: Will Beasley Date: Tue, 11 Jul 2023 16:49:56 -0500 Subject: [PATCH 23/23] verify `record_id` IS returned by `redcap_read_oneshot()` ref #471 --- .../specify-fields-without-record-id.R | 6 ++ .../specify-forms-without-record-id.R | 9 ++ tests/testthat/test-read-batch-simple.R | 96 +++++++++++++++++++ 3 files changed, 111 insertions(+) create mode 100644 inst/test-data/specific-redcapr/read-batch-simple/specify-fields-without-record-id.R create mode 100644 inst/test-data/specific-redcapr/read-batch-simple/specify-forms-without-record-id.R diff --git a/inst/test-data/specific-redcapr/read-batch-simple/specify-fields-without-record-id.R b/inst/test-data/specific-redcapr/read-batch-simple/specify-fields-without-record-id.R new file mode 100644 index 00000000..9bb2d1f2 --- /dev/null +++ b/inst/test-data/specific-redcapr/read-batch-simple/specify-fields-without-record-id.R @@ -0,0 +1,6 @@ +structure(list(record_id = c(1, 2, 3, 4, 5), name_first = c("Nutmeg", +"Tumtum", "Marcus", "Trudy", "John Lee"), address = c("14 Rose Cottage St.\nKenning UK, 323232", +"14 Rose Cottage Blvd.\nKenning UK 34243", "243 Hill St.\nGuthrie OK 73402", +"342 Elm\nDuncanville TX, 75116", "Hotel Suite\nNew Orleans LA, 70115" +), interpreter_needed = c(0, 0, 1, NA, 0)), row.names = c(NA, +-5L), class = c("spec_tbl_df", "tbl_df", "tbl", "data.frame")) diff --git a/inst/test-data/specific-redcapr/read-batch-simple/specify-forms-without-record-id.R b/inst/test-data/specific-redcapr/read-batch-simple/specify-forms-without-record-id.R new file mode 100644 index 00000000..51e4810a --- /dev/null +++ b/inst/test-data/specific-redcapr/read-batch-simple/specify-forms-without-record-id.R @@ -0,0 +1,9 @@ +structure(list(record_id = c(1, 2, 3, 4, 5), height = c(7, 6, +180, 165, 193.04), weight = c(1, 1, 80, 54, 104), bmi = c(204.1, +277.8, 24.7, 19.8, 27.9), comments = c("Character in a book, with some guessing", +"A mouse character from a good book", "completely made up", "This record doesn't have a DAG assigned\n\nSo call up Trudy on the telephone\nSend her a letter in the mail", +"Had a hand for trouble and a eye for cash\n\nHe had a gold watch chain and a black mustache" +), mugshot = c("mugshot-1.jpg", "mugshot-2.jpg", "mugshot-3.jpg", +"mugshot-4.jpg", "mugshot-5.jpg"), health_complete = c(1, 0, +2, 2, 0)), row.names = c(NA, -5L), class = c("spec_tbl_df", "tbl_df", +"tbl", "data.frame")) diff --git a/tests/testthat/test-read-batch-simple.R b/tests/testthat/test-read-batch-simple.R index c5a12328..c0ec4d55 100644 --- a/tests/testthat/test-read-batch-simple.R +++ b/tests/testthat/test-read-batch-simple.R @@ -350,6 +350,102 @@ test_that("specify-forms-only-1st", { expect_match(returned_object2$outcome_messages, regexp=expected_outcome_message, perl=TRUE) expect_s3_class(returned_object2$data, "tbl") }) +test_that("specify-forms-without-record-id", { + testthat::skip_on_cran() + path_expected <- "test-data/specific-redcapr/read-batch-simple/specify-forms-without-record-id.R" + desired_forms <- c("health") + expected_outcome_message <- "\\d+ records and 7 columns were read from REDCap in \\d+(\\.\\d+\\W|\\W)seconds\\." + + ########################### + ## Default Batch size + returned_object1 <- + redcap_read( + redcap_uri = credential$redcap_uri, + token = credential$token, + forms = desired_forms, + verbose = FALSE + ) + + if (update_expectation) save_expected(returned_object1$data, path_expected) + expected_data_frame <- retrieve_expected(path_expected) + + expect_equal(returned_object1$data, expected=expected_data_frame, label="The returned data.frame should be correct", ignore_attr = TRUE) # dput(returned_object1$data) + expect_true(returned_object1$success) + expect_match(returned_object1$status_codes, regexp="200", perl=TRUE) + expect_true(returned_object1$records_collapsed=="", "A subset of records was not requested.") + expect_equal(returned_object1$fields_collapsed, "record_id") + expect_true(returned_object1$filter_logic=="", "A filter was not specified.") + expect_match(returned_object1$outcome_messages, regexp=expected_outcome_message, perl=TRUE) + expect_s3_class(returned_object1$data, "tbl") + + ########################### + ## Tiny Batch size + returned_object2 <- + redcap_read( + redcap_uri = credential$redcap_uri, + token = credential$token, + forms = desired_forms, + batch_size = 2, + verbose = FALSE + ) + + expect_equal(returned_object2$data, expected=expected_data_frame, label="The returned data.frame should be correct", ignore_attr = TRUE) # dput(returned_object2$data) + expect_true(returned_object2$success) + expect_match(returned_object2$status_codes, regexp="200", perl=TRUE) + expect_true(returned_object2$records_collapsed=="", "A subset of records was not requested.") + expect_equal(returned_object2$fields_collapsed, "record_id") + expect_true(returned_object2$filter_logic=="", "A filter was not specified.") + expect_match(returned_object2$outcome_messages, regexp=expected_outcome_message, perl=TRUE) + expect_s3_class(returned_object2$data, "tbl") +}) +test_that("specify-fields-without-record-id", { + testthat::skip_on_cran() + path_expected <- "test-data/specific-redcapr/read-batch-simple/specify-fields-without-record-id.R" + desired_fields <- c("name_first", "address", "interpreter_needed") + expected_outcome_message <- "\\d+ records and 4 columns were read from REDCap in \\d+(\\.\\d+\\W|\\W)seconds\\." + + ########################### + ## Default Batch size + returned_object1 <- + redcap_read( + redcap_uri = credential$redcap_uri, + token = credential$token, + fields = desired_fields, + verbose = FALSE + ) + + if (update_expectation) save_expected(returned_object1$data, path_expected) + expected_data_frame <- retrieve_expected(path_expected) + + expect_equal(returned_object1$data, expected=expected_data_frame, label="The returned data.frame should be correct", ignore_attr = TRUE) # dput(returned_object1$data) + expect_true(returned_object1$success) + expect_match(returned_object1$status_codes, regexp="200", perl=TRUE) + expect_true(returned_object1$records_collapsed=="", "A subset of records was not requested.") + expect_equal(returned_object1$fields_collapsed, paste0("record_id,", paste(desired_fields, collapse = ","))) + expect_true(returned_object1$filter_logic=="", "A filter was not specified.") + expect_match(returned_object1$outcome_messages, regexp=expected_outcome_message, perl=TRUE) + expect_s3_class(returned_object1$data, "tbl") + + ########################### + ## Tiny Batch size + returned_object2 <- + redcap_read( + redcap_uri = credential$redcap_uri, + token = credential$token, + fields = desired_fields, + batch_size = 2, + verbose = FALSE + ) + + expect_equal(returned_object2$data, expected=expected_data_frame, label="The returned data.frame should be correct", ignore_attr = TRUE) # dput(returned_object2$data) + expect_true(returned_object2$success) + expect_match(returned_object2$status_codes, regexp="200", perl=TRUE) + expect_true(returned_object2$records_collapsed=="", "A subset of records was not requested.") + expect_equal(returned_object2$fields_collapsed, paste0("record_id,", paste(desired_fields, collapse = ","))) + expect_true(returned_object2$filter_logic=="", "A filter was not specified.") + expect_match(returned_object2$outcome_messages, regexp=expected_outcome_message, perl=TRUE) + expect_s3_class(returned_object2$data, "tbl") +}) test_that("raw", { testthat::skip_on_cran() path_expected <- "test-data/specific-redcapr/read-batch-simple/raw.R"