1- # YT#VHf6c40bb7738a4549da708e6cffa92411#VHa84423515cdb57d0fffac288f003e279 #
1+ # YT#VHb3a67af97b323ee47762788154a489fd#VHa53bd254d6ac8e6a19dfa057febb06b5 #
22CM <- local({ # _C_hecked _M_odule
3- message_well <- function (title , contents , color = " f5f5f5" ) { # repeats #iewahg
4- style <- sprintf(r " ---(
3+ # 2025-03-21: [feature] report errors for all loaded datasets and [fix] dehardcode "PARAM" string and use `par` argument
4+
5+ message_well <- function (title , contents , color = " f5f5f5" ) {
6+ style <- sprintf("
57 padding: 0.5rem;
68 padding-left: 1rem;
79 margin-bottom: 20px;
@@ -10,7 +12,7 @@ CM <- local({ # _C_hecked _M_odule
1012 border-radius: 4px;
1113 -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.05);
1214 box-shadow: inset 0 1px 1px rgba(0,0,0,.05);
13- )--- " , color )
15+ " , color )
1416
1517 res <- list (shiny :: h3(title ))
1618 if (length(contents )) res <- append(res , list (shiny :: tags [[" div" ]](contents , style = style )))
@@ -47,16 +49,8 @@ CM <- local({ # _C_hecked _M_odule
4749 )
4850 }
4951
50- err <- error_messages
51- if (length(err )) {
52- res [[length(res ) + 1 ]] <-
53- message_well(" Module configuration errors" , {
54- tmp <- Map(function (x ) htmltools :: p(htmltools :: HTML(paste(" \u 2022" , x ))), err )
55- if (! is.null(preface )) {
56- tmp <- append(list (htmltools :: p(htmltools :: HTML(preface ))), tmp )
57- }
58- tmp
59- }, color = " #f4d7d7" )
52+ if (length(error_messages )) {
53+ res [[length(res ) + 1 ]] <- message_well(" Module configuration errors" , error_messages , color = " #f4d7d7" )
6054 }
6155
6256 return (res )
@@ -109,7 +103,6 @@ CM <- local({ # _C_hecked _M_odule
109103
110104 matched_args <- try(as.list(match.call(module )), silent = TRUE )
111105 error_message <- attr(matched_args , " condition" )$ message
112- error_message_dataset_index <- NULL
113106 if (is.null(error_message )) {
114107 missing_args <- setdiff(mandatory_module_args , names(matched_args ))
115108 if (length(missing_args )) {
@@ -143,31 +136,80 @@ CM <- local({ # _C_hecked _M_odule
143136 # - Here we also have access to the original datasets, which allows us to ensure call
144137 # correctness independent of filter state or operation in a single pass.
145138 # - "catch errors early"
146- for (i_dataset in seq_along(afmm [[" data" ]])) {
139+ dataset_count <- length(afmm [[" data" ]])
140+
141+ res_by_dataset <- list ()
142+ error_count_by_dataset <- integer(0 )
143+ error_count <- 0
144+ for (i_dataset in seq_len(dataset_count )) {
147145 check_args <- append(
148146 list (
149147 afmm = afmm , # To check receiver_ids, among others
150148 datasets = afmm [[" data" ]][[i_dataset ]] # Allows data checks prior to reactive time
151149 ),
152150 args
153151 )
154- res <- do.call(check_mod_fn , check_args )
155- # NOTE: Stop when errors are found on a single dataset to avoid overwhelming users with repeat messages
156- if (length(res [[" errors" ]])) { # NOTE: Not checking "warnings" because they are going away soon
157- error_message_dataset_index <- i_dataset
158- break
152+ info <- do.call(check_mod_fn , check_args )
153+ res_by_dataset [[i_dataset ]] <- info
154+ error_count_by_dataset [[i_dataset ]] <- length(info [[" errors" ]])
155+ error_count <- error_count + length(info [[" errors" ]])
156+ }
157+
158+ as_items <- function (x ) htmltools :: p(htmltools :: HTML(paste(" \u 2022" , x )))
159+
160+ if (error_count == 0 ) NULL
161+ else if (dataset_count == 1 ) {
162+ # single dataset
163+ res <- res_by_dataset [[1 ]]
164+ res [[" errors" ]] <- Map(as_items , res [[" errors" ]])
165+ } else {
166+ # multiple datasets
167+
168+ # FIXME(miguel): We don't do merge "warnings" here because it's a feature that goes unused and we will
169+ # remove it soon. We can't remove it _now_ because it would still require minor fixes
170+ # in at least five different packages and we want to roll a CM bugfix (unrelated to
171+ # the "multiple dataset" feature reporting) while avoiding cascading work.
172+ errors <- list ()
173+
174+ dataset_names <- names(afmm [[" datasets" ]])
175+
176+ errors <- c(
177+ list (htmltools :: p(htmltools :: HTML(
178+ " Issues have been grouped by input dataset. Expand/collapse the elements below to inspect them:"
179+ )))
180+ )
181+
182+ details_extra <- " open"
183+ for (i_dataset in seq_len(dataset_count )){
184+ if (error_count_by_dataset [[i_dataset ]] == 0 ) next
185+
186+ details_pre <- htmltools :: HTML(
187+ sprintf('
188+ <details %s>
189+ <summary style="display:list-item"><b>%s</b></summary>
190+ ' , details_extra , names(afmm [[" data" ]]))[[i_dataset ]]
191+ )
192+ details_extra <- " "
193+
194+ details_post <- htmltools :: HTML(" </details>" )
195+
196+ errors <- c(errors , list (details_pre ))
197+
198+ errors <- c(
199+ errors ,
200+ list (htmltools :: HTML(" <div style='padding: 0.5rem; margin-bottom: 1rem; background-color: #FFFFFF55;
201+ border: 1px solid #AAAAAA; border-radius: 4px;'>" )),
202+ Map(as_items , res_by_dataset [[i_dataset ]][[" errors" ]]),
203+ list (htmltools :: HTML(" </div>" ))
204+ )
205+
206+ errors <- c(errors , list (details_post ))
207+
208+ res [[" errors" ]] <- errors
159209 }
160210 }
161211 }
162212
163- if (! is.null(error_message_dataset_index ) && length(afmm [[" data" ]]) > 1 ) {
164- dataset_name <- names(afmm [[" data" ]])[[error_message_dataset_index ]]
165- res [[" preface" ]] <- paste(
166- " This application has been configured with more than one dataset." ,
167- sprintf(" The following error messages apply to the dataset named <b>%s</b>.<br>" , dataset_name ),
168- " No error checking has been performed on datasets specified after it. <hr>"
169- )
170- }
171213 return (res )
172214 })
173215
@@ -599,7 +641,7 @@ CM <- local({ # _C_hecked _M_odule
599641 } else if (is.character(v )) {
600642 res <- sprintf(' "%s"' , unique(v ))
601643 } else {
602- browser( )
644+ stop(sprintf( ' Unsuported class "%s" as argument to `list_values` ' , class( v )) )
603645 }
604646
605647 res <- paste(res , collapse = " , " )
@@ -719,9 +761,9 @@ CM <- local({ # _C_hecked _M_odule
719761
720762 unique_cat_par_combinations <- unique(dataset [c(cat , par )])
721763 dup_mask <- duplicated(unique_cat_par_combinations [par ])
722- unique_repeat_params <- unique_cat_par_combinations [[par ]][dup_mask ]
723764
724- ok <- assert(err , length(unique_repeat_params ) == 0 , {
765+ ok <- assert(err , ! any(dup_mask ), {
766+ unique_repeat_params <- unique_cat_par_combinations [[par ]][dup_mask ]
725767 dups <- df_to_string(
726768 data.frame (
727769 check.names = FALSE ,
@@ -738,7 +780,7 @@ CM <- local({ # _C_hecked _M_odule
738780 sprintf(' %s <- dv.explorer.parameter::prefix_repeat_parameters(%s, cat_var = "%s", par_var = "%s")' ,
739781 ds_value , ds_value , cat , par )
740782
741- mask <- unique_cat_par_combinations [[" PARAM " ]] %in% unique_repeat_params
783+ mask <- unique_cat_par_combinations [[par ]] %in% unique_repeat_params
742784 deduplicated_table <- df_to_string({
743785 cats <- unique_cat_par_combinations [mask , ][[cat ]]
744786 pars <- unique_cat_par_combinations [mask , ][[par ]]
@@ -764,21 +806,47 @@ CM <- local({ # _C_hecked _M_odule
764806 })
765807
766808 supposedly_unique <- dataset [c(sub , cat , par , vis )]
767- dups <- duplicated(supposedly_unique )
809+ dup_mask <- duplicated(supposedly_unique )
768810
769- ok <- ok && assert(err , ! any(dups ), {
811+ ok <- ok && assert(err , ! any(dup_mask ), {
770812 prefixes <- c(
771813 rep(" Subject:" , length(sub )), rep(" Category:" , length(cat )),
772814 rep(" Parameter:" , length(par )), rep(" Visit:" , length(vis ))
773815 )
774816
775- first_duplicates <- head(supposedly_unique [dups , ], 5 )
817+ first_duplicates <- head(supposedly_unique [dup_mask , ], 5 )
776818 names(first_duplicates ) <- paste(prefixes , names(first_duplicates ))
777819 dups <- df_to_string(first_duplicates )
820+
821+ unique_repeats <- unique(supposedly_unique [dup_mask , ])
822+ target <- unique_repeats [1 , ]
823+ target_rows <- which(supposedly_unique [[sub ]] == target [[sub ]] & supposedly_unique [[cat ]] == target [[cat ]] &
824+ supposedly_unique [[par ]] == target [[par ]] & supposedly_unique [[vis ]] == target [[vis ]])
825+
826+ row_a <- dataset [target_rows [[1 ]], ]
827+ row_b <- dataset [target_rows [[2 ]], ]
828+ diff_cols <- character (0 )
829+ for (col in names(row_a )) if (! identical(row_a [[col ]], row_b [[col ]])) diff_cols <- c(diff_cols , col )
830+
831+ col_diff_report <- " are identical."
832+ if (length(diff_cols )) {
833+ col_diff_report <- paste0(
834+ " have indeed identical subject, category, parameter and visit values, but differ in columns: " ,
835+ paste(diff_cols , collapse = " , " ), " ." ,
836+ " <pre>" ,
837+ df_to_string(dataset [target_rows [1 : 2 ], c(sub , cat , par , vis , diff_cols )]),
838+ " </pre>"
839+ )
840+ }
841+
778842 paste(
779843 sprintf(" The dataset provided by `%s` (%s) contains repeated rows with identical subject, category, parameter" , ds_name , ds_value ),
780- " and visit values. This module expects them to be unique. Here are the first few duplicates:" ,
781- paste0(" <pre>" , dups , " </pre>" )
844+ sprintf(" and visit values. This module expects them to be unique. There are a total of %d duplicates." , sum(dup_mask )),
845+ " Here are the first few:" ,
846+ paste0(" <pre>" , dups , " </pre>" ),
847+ sprintf(" These findings can be partially confirmed by examining that rows <b>%d</b> and <b>%d</b> of that dataset" ,
848+ target_rows [[1 ]], target_rows [[2 ]]),
849+ col_diff_report
782850 )
783851 })
784852
0 commit comments