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 e1e8b41a..bd86d2d8 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -74,14 +74,21 @@ 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)
* `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/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 72f1222c..683b34c2 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.
@@ -182,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
#'
@@ -283,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)
@@ -300,10 +321,33 @@ redcap_read <- function(
handle_httr = handle_httr
)
- # browser()
- 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[["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)
+ }
# Retrieve list of record ids --------------------------------------
initial_call <- REDCapR::redcap_read_oneshot(
@@ -405,6 +449,7 @@ redcap_read <- function(
" (ie, ", length(selected_ids), " unique subject records)."
)
}
+
read_result <- REDCapR::redcap_read_oneshot(
redcap_uri = redcap_uri,
token = token,
diff --git a/R/validate.R b/R/validate.R
index 6168e6d5..18c4341d 100644
--- a/R/validate.R
+++ b/R/validate.R
@@ -6,21 +6,24 @@
#' validate_data_frame_inherits
#' validate_no_logical
#' validate_field_names
+#' validate_record_id_name
#' validate_repeat_instance
#' 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 )
#'
-#' validate_no_logical( data_types, stop_on_error )
+#' validate_no_logical( d, stop_on_error = FALSE )
#'
-#' validate_field_names( field_names, stop_on_error = FALSE )
+#' validate_field_names( d, stop_on_error = FALSE )
#'
-#' validate_repeat_instance( d, stop_on_error )
+#' validate_record_id_name( d, record_id_name = "record_id", stop_on_error = FALSE )
#'
-#' validate_uniqueness(d, record_id_name, stop_on_error)
+#' validate_repeat_instance( d, stop_on_error = FALSE )
+#'
+#' validate_uniqueness( d, record_id_name, stop_on_error = FALSE)
#'
#' @title
#' Inspect a dataset to anticipate problems before
@@ -33,10 +36,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.
#' The default name in REDCap is "record_id".
#' @param stop_on_error If `TRUE`, an error is thrown for violations.
@@ -54,7 +53,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
@@ -62,16 +61,41 @@
#' individual validation checks. It allows the client to check everything
#' with one call.
#'
-#' Currently it verifies that the dataset
-#' * inherits from [data.table::data.table()].
-#' * 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
@@ -125,7 +149,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:
@@ -133,7 +157,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 ",
@@ -146,16 +170,16 @@ 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)
- checkmate::assert_logical(stop_on_error, any.missing=FALSE, len=1)
+validate_no_logical <- function(d, stop_on_error = FALSE) {
+ checkmate::assert_data_frame(d)
+ checkmate::assert_logical(stop_on_error, any.missing = FALSE, len = 1L)
- indices <- which(data_types == "logical")
+ indices <- which(vapply(d, \(x) inherits(x, "logical"), logical(1)))
if (length(indices) == 0L) {
tibble::tibble(
field_name = character(0),
- field_index = integer(0),
+ field_index = character(0),
concern = character(0),
suggestion = character(0)
)
@@ -168,8 +192,8 @@ validate_no_logical <- function(data_types, stop_on_error = FALSE) {
)
} else {
tibble::tibble(
- field_name = names(data_types)[indices],
- field_index = 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."
)
@@ -177,17 +201,18 @@ validate_no_logical <- function(data_types, 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)
- checkmate::assert_logical(stop_on_error, any.missing=FALSE, len=1)
+validate_field_names <- function(d, stop_on_error = FALSE) {
+ checkmate::assert_data_frame(d)
+ checkmate::assert_logical(stop_on_error, any.missing = FALSE, len = 1L)
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),
- field_index = integer(0),
+ field_index = character(0),
concern = character(0),
suggestion = character(0)
)
@@ -201,30 +226,82 @@ 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()`."
)
}
}
+# 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,
+ 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)
+
+ 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)
- 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)) {
+ 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 +316,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."
)
@@ -250,6 +327,8 @@ 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)
+ 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")
@@ -263,16 +342,16 @@ 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),
+ field_index = character(0),
concern = character(0),
suggestion = character(0)
)
} else if (stop_on_error) {
m <-
- if(requireNamespace("knitr", quietly = TRUE)) {
+ if (requireNamespace("knitr", quietly = TRUE)) {
d_replicates %>%
knitr::kable() %>%
paste(collapse = "\n")
@@ -288,7 +367,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."
)
@@ -306,15 +385,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(colnames(d)),
- validate_uniqueness(d),
+ validate_field_names(d),
+ validate_record_id_name(d),
+ validate_uniqueness(d, record_id_name = record_id_name),
validate_repeat_instance(d)
)
@@ -324,9 +406,10 @@ 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()
# Vertically stack all the data.frames into a single data frame
dplyr::bind_rows(lst_concerns)
}
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/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
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/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/man/redcap_read.Rd b/man/redcap_read.Rd
index 37b19ce8..b666a6f8 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,25 @@ throw an error like
}\if{html}{\out{}}
}
}
+
+\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/"
diff --git a/man/validate.Rd b/man/validate.Rd
index 2a9ed8ff..49764b8a 100644
--- a/man/validate.Rd
+++ b/man/validate.Rd
@@ -6,34 +6,31 @@
\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
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 )
-validate_no_logical( data_types, stop_on_error )
+validate_no_logical( d, stop_on_error = FALSE )
-validate_field_names( field_names, stop_on_error = FALSE )
+validate_field_names( d, stop_on_error = FALSE )
-validate_repeat_instance( d, stop_on_error )
+validate_record_id_name( d, record_id_name = "record_id", stop_on_error = FALSE )
-validate_uniqueness(d, record_id_name, stop_on_error)
+validate_repeat_instance( d, stop_on_error = FALSE )
+
+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()}}
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.}
-
\item{record_id_name}{The name of the field that represents one record.
The default name in REDCap is "record_id".}
@@ -66,17 +63,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[data.table:data.table]{data.table::data.table()}}.
-\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
@@ -120,7 +136,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:
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"
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"
diff --git a/tests/testthat/test-validate-field-names.R b/tests/testthat/test-validate-field-names.R
index 1176195b..2a7cc703 100644
--- a/tests/testthat/test-validate-field-names.R
+++ b/tests/testthat/test-validate-field-names.R
@@ -12,33 +12,42 @@ 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)
+ 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\\."
+ )
})
diff --git a/tests/testthat/test-validate-no-logical.R b/tests/testthat/test-validate-no-logical.R
index 00fd5aa0..8fbcd1e5 100644
--- a/tests/testthat/test-validate-no-logical.R
+++ b/tests/testthat/test-validate-no-logical.R
@@ -12,22 +12,22 @@ 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)
+ expect_equal(object=unname(ds$field_index), expected="2")
})
# ---- redcap-repeat-instance --------------------------------------------------
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))
+})
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")
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).