Skip to content

Commit be7ae6a

Browse files
committed
[review](feat) Dataset/parameterization update checks.
1 parent b2171b6 commit be7ae6a

7 files changed

Lines changed: 536 additions & 71 deletions

File tree

R/check_call_auto.R

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
# dv.listings::mod_listings
66
check_mod_listings_auto <- function(afmm, datasets, module_id, dataset_names, default_vars, pagination,
7-
intended_use_label, subjid_var, review, receiver_id, warn, err) {
7+
intended_use_label, subjid_var, receiver_id, review, warn, err) {
88
OK <- logical(0)
99
used_dataset_names <- new.env(parent = emptyenv())
1010
OK[["module_id"]] <- CM$check_module_id("module_id", module_id, warn, err)
@@ -23,10 +23,10 @@ check_mod_listings_auto <- function(afmm, datasets, module_id, dataset_names, de
2323
"NOTE: subjid_var (character) tagged as \"manual_check\""
2424
" The expectation is that it either does not require automated checks or that"
2525
" the caller of this function has written manual checks near the call site."
26-
"NOTE: review (group) tagged as \"manual_check\""
26+
"NOTE: receiver_id (character) tagged as \"manual_check\""
2727
" The expectation is that it either does not require automated checks or that"
2828
" the caller of this function has written manual checks near the call site."
29-
"NOTE: receiver_id (character) tagged as \"manual_check\""
29+
"NOTE: review (group) tagged as \"manual_check\""
3030
" The expectation is that it either does not require automated checks or that"
3131
" the caller of this function has written manual checks near the call site."
3232
return(OK)

R/mock_listings.R

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,123 @@ mock_listings_mm <- function() {
129129
enableBookmarking = "url"
130130
)
131131
}
132+
133+
mock_cqm <- function() {
134+
ae <- safetyData::sdtm_ae
135+
dm <- safetyData::sdtm_dm
136+
137+
# Filter out subjects that didn't pass initial screening
138+
screen_fail_mask <- dm[["ACTARMCD"]] == "Scrnfail"
139+
dm <- dm[!screen_fail_mask, ]
140+
141+
# Subset AE columns and extend the domain with a couple of date columns from the demographic data frame
142+
ae_cols <- c("STUDYID", "USUBJID", "AETERM", "AEHLGT", "AEHLT", "AELLT", "AEDECOD", "AESOC",
143+
"AESTDTC", "AEENDTC", "AEOUT", "AEACN", "AEREL", "AESEV", "AESEQ")
144+
ae <- merge(dm[c("USUBJID", "RFXSTDTC", "RFSTDTC")], ae[ae_cols], by = "USUBJID")
145+
146+
# Add labels
147+
var_labels <- c(
148+
STUDYID = "Study Identifier",
149+
USUBJID = "Unique Subject Identifier",
150+
AETERM = "Reported Term for the Adverse Event",
151+
AEHLGT = "High Level Group Term",
152+
AEHLT = "High Level Term",
153+
AELLT = "Lowest Level Term",
154+
AEDECOD = "Dictionary-Derived Term",
155+
AESEQ = "Sequence Number",
156+
AESOC = "Primary System Organ Class",
157+
AESTDTC = "Start Date/Time of Adverse Event",
158+
AEENDTC = "End Date/Time of Adverse Event",
159+
AEOUT = "Outcome of Adverse Event",
160+
AEACN = "Action Taken with Study Treatment",
161+
AEREL = "Causality",
162+
AESEV = "Severity/Intensity",
163+
RFXSTDTC = "Date/Time of First Study Treatment",
164+
RFSTDTC = "Subject Reference Start Date/Time",
165+
DOMAIN = "Domain Abbreviation",
166+
SUBJID = "Subject Identifier for the Study",
167+
RFENDTC = "Subject Reference End Date/Time",
168+
RFXENDTC = "Date/Time of Last Study Treatment",
169+
RFICDTC = "Date/Time of Informed Consent",
170+
RFPENDTC = "Date/Time of End of Participation",
171+
DTHDTC = "Date/Time of Death",
172+
DTHFL = "Subject Death Flag",
173+
SITEID = "Study Site Identifier",
174+
AGE = "Age",
175+
AGEU = "Age Units",
176+
SEX = "Sex",
177+
RACE = "Race",
178+
ETHNIC = "Ethnicity",
179+
ARMCD = "Planned Arm Code",
180+
ARM = "Description of Planned Arm",
181+
ACTARMCD = "Actual Arm Code",
182+
ACTARM = "Description of Actual Arm",
183+
COUNTRY = "Country",
184+
DMDTC = "Date/Time of Collection",
185+
DMDY = "Study Day of Collection"
186+
)
187+
188+
dm <- set_labels(dm, var_labels[names(dm)])
189+
ae <- set_labels(ae, var_labels[names(ae)])
190+
191+
data_list <- list(ae = ae, dm = dm)
192+
193+
attr(data_list[["ae"]], "label") <- "Adverse Events" # NOTE(miguel): Otherwise uses label from the `dm` dataset
194+
195+
# Step 4 - Module specification
196+
listing <- mod_listings(
197+
module_id = "listing",
198+
dataset_names = c("ae", "dm"),
199+
default_vars = list(
200+
ae = c(
201+
"USUBJID",
202+
"AESEV",
203+
"RFXSTDTC",
204+
"RFSTDTC",
205+
"AETERM",
206+
"AEHLGT", "AEHLT", "AELLT", "AEDECOD", "AESOC",
207+
"AESTDTC", "AEENDTC",
208+
"AEOUT",
209+
"AEACN",
210+
"AEREL"
211+
),
212+
dm = c("COUNTRY", "RFXSTDTC")
213+
),
214+
# Jumping to the Patient Profile module is possible, provided that it is included as well:
215+
receiver_id = "papo",
216+
review = list(
217+
datasets = list(ae = list(id_vars = c("USUBJID", "AESEQ"), untracked_vars = c())),
218+
choices = c("Pending", "Reviewed with no issues", "Action required", "Resolved"),
219+
roles = c("TSTAT", "SP", "Safety", "CTL"),
220+
store_path = tempdir()
221+
)
222+
)
223+
224+
mod_receiver <- function(module_id, sender_id) {
225+
list(
226+
ui = function(id) shiny::verbatimTextOutput(shiny::NS(id)("out")),
227+
server = function(afmm) {
228+
shiny::moduleServer(
229+
module_id,
230+
function(input, output, session) {
231+
output[["out"]] <- shiny::reactive({
232+
sprintf('Message from module "%s": %s', sender_id, afmm[["module_output"]]()[[sender_id]][["subj_id"]]())
233+
})
234+
}
235+
)
236+
},
237+
module_id = module_id
238+
)
239+
}
240+
241+
# Step 5 - Run app
242+
dv.manager::run_app(
243+
data = list("CQM_AE_list" = data_list),
244+
module_list = list(
245+
"Listing" = listing,
246+
"Signal receiver" = mod_receiver(module_id = "papo", sender_id = "listing")
247+
),
248+
filter_data = "dm",
249+
filter_key = "USUBJID"
250+
)
251+
}

R/mod_listings.R

Lines changed: 81 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -760,14 +760,14 @@ mod_listings_API_docs <- list(
760760
pagination = list(""),
761761
intended_use_label = list(""),
762762
subjid_var = list(""),
763+
receiver_id = list(""),
763764
review = list(
764765
"Review-related fields",
765766
datasets = list(""),
766767
choices = list(""),
767768
roles = list(""),
768769
store_path = list("")
769-
),
770-
receiver_id = list("")
770+
)
771771
)
772772

773773
mod_listings_API_spec <- TC$group(
@@ -777,14 +777,13 @@ mod_listings_API_spec <- TC$group(
777777
pagination = TC$logical() |> TC$flag("manual_check", "optional"), # manually tested by check_mod_listings
778778
intended_use_label = TC$character() |> TC$flag("manual_check", "optional"), # manually tested by check_mod_listings
779779
subjid_var = TC$character() |> TC$flag("manual_check"), # manually tested by check_mod_listings
780+
receiver_id = TC$character() |> TC$flag("manual_check"), # manually tested by check_mod_listings
780781
review = TC$group(
781-
# TODO: functionality is a WIP, so not defining for now
782782
datasets = TC$group(),
783783
choices = TC$character() |> TC$flag("one_or_more"),
784784
roles = TC$character() |> TC$flag("one_or_more"),
785785
store_path = TC$character() |> TC$flag("optional")
786-
) |> TC$flag("manual_check", "optional"),
787-
receiver_id = TC$character() |> TC$flag("manual_check") # manually tested by check_mod_listings
786+
) |> TC$flag("manual_check", "optional")
788787
) |> TC$attach_docs(mod_listings_API_docs)
789788

790789
dataset_info_listings <- function(dataset_names, ...) {
@@ -800,7 +799,7 @@ check_mod_listings <- function(afmm, datasets, module_id, dataset_names,
800799
ok <- check_mod_listings_auto(
801800
afmm, datasets,
802801
module_id, dataset_names, default_vars, pagination, intended_use_label,
803-
subjid_var, receiver_id, warn, err
802+
subjid_var, receiver_id, review, warn, err
804803
)
805804

806805
# default_vars
@@ -845,10 +844,6 @@ check_mod_listings <- function(afmm, datasets, module_id, dataset_names,
845844
msg = "`subjid_var` should be either character(1) or NULL."
846845
)
847846

848-
# review
849-
# TODO:
850-
851-
# receiver_id
852847
CM$assert(
853848
container = err,
854849
cond = checkmate::test_string(receiver_id, null.ok = TRUE),
@@ -860,6 +855,82 @@ check_mod_listings <- function(afmm, datasets, module_id, dataset_names,
860855
receiver_id, paste(names(afmm$module_names), collapse = ", ")
861856
)
862857
)
858+
859+
# review
860+
local({
861+
if (is.null(review)) return(NULL)
862+
ok <- CM$assert(
863+
container = err,
864+
cond = (checkmate::test_list(review, names = "unique") &&
865+
checkmate::test_subset(c("datasets", "choices", "roles"), names(review))),
866+
msg = "`review` should be a list with at least three elements: `datasets`, `choices` and `roles`"
867+
) &&
868+
CM$assert(
869+
container = err,
870+
cond = (checkmate::test_list(review[["datasets"]], names = "unique") &&
871+
checkmate::test_subset(names(review[["datasets"]]), dataset_names)),
872+
msg = sprintf(
873+
"`review$datasets` should be a list and its elements should be named after the following dataset names: %s",
874+
paste(dataset_names, collapse = ", ")
875+
)
876+
) &&
877+
CM$assert(
878+
container = err,
879+
cond = checkmate::test_character(review[["choices"]], min.len = 1, min.chars = 1, unique = TRUE),
880+
msg = "`review$choices` should be a non-empty character vector of unique, non-empty strings"
881+
) &&
882+
CM$assert(
883+
container = err,
884+
cond = checkmate::test_character(review[["roles"]], min.len = 1, min.chars = 1, unique = TRUE),
885+
msg = "`review$roles` should be a non-empty character vector of unique, non-empty strings"
886+
)
887+
888+
if (!ok) return(NULL)
889+
for (domain in names(review[["datasets"]])){
890+
info <- review[["datasets"]][[domain]]
891+
892+
for (ds_name in names(afmm[["data"]])){
893+
datasets <- afmm[["data"]][[ds_name]]
894+
dataset <- datasets[[domain]]
895+
896+
CM$assert(
897+
container = err,
898+
cond = (checkmate::test_list(review, names = "unique") &&
899+
checkmate::test_subset(c("id_vars", "untracked_vars"), names(info))),
900+
msg = sprintf("`review$datasets$%s` should be a list with two elements named `id_vars` and `untracked_vars`",
901+
domain)
902+
) &&
903+
CM$assert(
904+
container = err,
905+
cond = (checkmate::test_character(info[["id_vars"]], min.len = 1, min.chars = 1, unique = TRUE) &&
906+
checkmate::test_subset(info[["id_vars"]], names(dataset))),
907+
msg = sprintf(
908+
paste(
909+
"`review$datasets$%s$id_vars` should be a character vector listing a subset of the columns",
910+
"available in dataset `%s`"
911+
), domain, domain
912+
)
913+
) &&
914+
CM$assert(
915+
container = err,
916+
cond = nrow(dataset[info[["id_vars"]]]) == nrow(unique(dataset[info[["id_vars"]]])),
917+
msg = sprintf("`review$datasets$%s$id_vars` should identify uniquely every row of the dataset `%s`",
918+
domain, domain)
919+
) &&
920+
CM$assert(
921+
container = err,
922+
cond = (checkmate::test_character(info[["untracked_vars"]], min.chars = 1, unique = TRUE, null.ok = TRUE) &&
923+
checkmate::test_subset(info[["untracked_vars"]], names(dataset))),
924+
msg = sprintf(
925+
paste(
926+
"`review$datasets$%s$untracked_vars` should be a character vector listing a subset of the columns",
927+
"available in dataset `%s`"
928+
), domain, domain
929+
)
930+
)
931+
}
932+
}
933+
})
863934

864935
res <- list(warnings = warn[["messages"]], errors = err[["messages"]])
865936
return(res)

0 commit comments

Comments
 (0)