-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature download csv #13
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did not review the entire PR code-wise, but tested the mock app. A few things which I noticed:
- The old checkbox label should be removed. You can do it as in dv.listings where only "I agree to the following:" is printed and the mod function contains an argument
intended_use_label = "Use only for internal review and monitoring during the conduct of clinical trials."
, which will be pasted to the first string. - Only the first level gets downloaded. Could we change this to download the entire table?
- I'm not sure if commas as separators are a good choice, at least in the mock app, the download gets a bit messy, because AEBODYSYS contains commas as well.
- There are no column headers in the exported file.
@iglauss thanks for the review. The requested features are implemented. For 3., it now downloads as excel For 4., Because we separate columns into two (N and per), Adding Overall Patients to column header was messy. So they are added as a first row in the dataframe. |
R/mod_export_count_table.R
Outdated
download_enable <- shiny::eventReactive( | ||
c(input[[EXP$ID$FILENAME_BOX]], input[[EXP$ID$DATAPROTECT_BOX]]), | ||
{ | ||
if ( | ||
(input[[EXP$ID$FILENAME_BOX]] != "") & | ||
input[[EXP$ID$DATAPROTECT_BOX]] | ||
) { # nolint end | ||
return(TRUE) | ||
} else { | ||
return(FALSE) | ||
} | ||
} | ||
) | ||
|
||
# Enable/disable download button separately (to allow testing of logic) | ||
shiny::observeEvent(download_enable(), { | ||
if (download_enable()) { | ||
shinyjs::enable(EXP$ID$DOWNLOAD_BUTTON) | ||
} else { | ||
shinyjs::disable(EXP$ID$DOWNLOAD_BUTTON) | ||
} | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we compact these lines in a single observe? download_enable
is not used anywhere else as far as I can see
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Combined in one Observe
R/mod_export_count_table.R
Outdated
|
||
# If there is no dataset being displayed | ||
if (is.null(dataset)) { | ||
shinyFeedback::showToast( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need shinyFeedback in this case? I would rather use shiny::validate
to avoid an extra dependency. Or the native showNotification
in shiny
as we are inside an obvserve.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replaced with shiny::validate
R/mod_export_count_table.R
Outdated
# Get col names and total patients | ||
total_colname <- dataset[["meta"]]$n_denominator | ||
|
||
#Get event variables | ||
event_vars <- dataset[["meta"]]$hierarchy | ||
|
||
excelfile <- dataset[["df"]] |> | ||
dplyr::select( | ||
event_vars, | ||
names(total_colname) | ||
) |> | ||
dplyr::mutate(dplyr::across( | ||
dplyr::where(is.list), | ||
~ purrr::map_chr(., "count") | ||
)) |> | ||
dplyr::mutate(dplyr::across( | ||
dplyr::all_of(names(total_colname)), | ||
~ gsub("\u2014", NA, .) | ||
)) |> | ||
dplyr::mutate(dplyr::across( | ||
dplyr::all_of(event_vars), | ||
~ gsub("\035", "Total", .) | ||
)) | ||
|
||
excelfile <- add_total_patient( | ||
excelfile, | ||
total_colname | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would extract this in a separate function, and test is in a non reactive environment.
Maybe with the following structure
count_table <- call_table_computing_function(xxx)
call_data_extraction(count_table)
This way if we change the way we count in the table we break the test and remember the coupling between both of them
R/utils-export_table.R
Outdated
#' Function to separate a column | ||
#' | ||
#' @details | ||
#' Columns where `(` is encountered is split into two and new columns | ||
#' are mutated | ||
#' | ||
#' @param df dataframe | ||
#' @param col column name | ||
#' | ||
#' @return dataframe with new mutated separated columns | ||
#' @keywords internal | ||
separate_column <- function(df, col) { | ||
df |> | ||
tidyr::separate(col, into = paste0(col, c("_N", "_per")), | ||
sep = " \\(", | ||
remove = TRUE, | ||
fill = "right") %>% | ||
dplyr::mutate(dplyr::across(dplyr::ends_with("_per"), ~stringr::str_remove(., "\\)"))) | ||
} | ||
|
||
|
||
#' Adds Overall number of patients | ||
#' | ||
#' @details | ||
#' Extracts overall number of patient (html table output column headers) | ||
#' from metadata, adds it to first row of the downloaded table. | ||
#' | ||
#' | ||
#' @param df dataframe | ||
#' @param total_colname named int, col names with overall patients | ||
#' | ||
#' @return dataframe with additional row containing overall patients | ||
#' | ||
#' @keywords internal | ||
add_total_patient <- function(df, total_colname) { | ||
|
||
# empty row with column name placeholder | ||
new_row <- setNames(data.frame(matrix(ncol = ncol(df), nrow = 1)), names(df)) | ||
|
||
new_row[[1, 1]] <- "Overall No. of Patients" | ||
|
||
# match col names to add total patients | ||
matched_cols <- match(names(total_colname), names(new_row)) | ||
new_row[1, matched_cols] <- total_colname | ||
|
||
return(rbind(new_row, df)) | ||
|
||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would compact this in a single function together with the code that is now inside the download handler.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Put different export utils (and the dplyr operation from mod into main function).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, Yash! I'm almost completely happy. Just one remaining issue: When I add a minimum % >0, this is not reflected in the PDF, right? Could we change this?
DESCRIPTION
Outdated
Config/testthat/edition: 3 | ||
Config/testthat/parallel: false | ||
Imports: shiny (>= 1.7.1),dplyr (>= 1.0.7), purrr (>= 0.3.4), | ||
tidyr (>= 1.1.4), | ||
rlang, checkmate (>= 2.0.0), htmltools, | ||
stats, pharmaverseadam | ||
stats, pharmaverseadam, writexl |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to make sure that we do not add unnecessary independencies: Could we use openxlsx instead of writexl? Becuase dv.listings is already using openxlsx. If not, this is also fine. Just want to question it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for pointing out previous comment, Mistake in passing non-reactive object. Fixed!
Replaced with openxlsx
@@ -473,7 +475,8 @@ hierarchical_count_table_server <- function( | |||
subjid_var, | |||
show_modal_on_click = FALSE, | |||
default_hierarchy = NULL, | |||
default_group = NULL) { | |||
default_group = NULL, | |||
intended_use_label = NULL) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We still need to add the intended_use_label in the roxygen docu above
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's a bit of feedback. Thanks for the good work!
R/utils-export_table.R
Outdated
remove = TRUE, | ||
fill = "right" | ||
) %>% | ||
dplyr::mutate(dplyr::across(dplyr::ends_with("_per"), ~ stringr::str_remove(., "\\)"))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is the only use of stringr
in the whole package, we could rely instead on base::sub
and cut a dependency:
dplyr::mutate(dplyr::across(dplyr::ends_with("_per"), ~ stringr::str_remove(., "\\)"))) | |
dplyr::mutate(dplyr::across(dplyr::ends_with("_per"), ~ sub(")", "", . ))) |
DESCRIPTION
Outdated
@@ -13,13 +13,14 @@ LazyData: true | |||
Roxygen: list(markdown = TRUE) | |||
RoxygenNote: 7.3.1 | |||
Suggests: dv.manager (>= 2.1.4), jsonlite, rmarkdown, testthat (>= | |||
3.0.0), shinytest2, devtools, knitr, tibble, utils | |||
3.0.0), shinytest2, devtools, knitr, tibble, utils, | |||
shinyjs, stringr |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- There is a solitary call to
stringr::str_remove
which I've suggested to replace withbase::sub
. If you think that's OK, then we could dropstringr
. - I believe both
stringr
(if we were to keep it) andshinyjs
should be underImports
instead of underSuggests
. Reasoning.
R/mod_export_count_table.R
Outdated
# check validity of parameters | ||
|
||
|
||
checkmate::assert( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When an assertion triggers inside an observer, it kills the app.
We could instead either:
shiny::req()
the checks, which would prevent the crash but fail silently. Probably would leave users scratching their heads.- Use a
shiny::showNotification
similar like the one you have below describing the problem to the user.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @ml-ebs-ext thanks for the comment. Just to make a case for the checkmate assertion into the observer; Wouldn't it be better to let the app gracefully crash, as the intended person for this check would be app creator rather than app user? Since this way they would know something is wrong in the dataset before publishing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Letting the app creator know something is wrong is perfectly OK, but
gracefully crash
sounds like an oxymoron to me 😋.
Also, in this case app creators need to press the export button in order to trigger the error. So, if they forget to do it, it's the final user that gets the crash and no message.
Still, I see there's only one caller of the export_counttable
module and it already performs some checks which happen without user interaction, so I'll leave it to you to judge the best course of action here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, I replaced it with simple if and showNotifications.
R/mod_export_count_table.R
Outdated
# Check if the inputs exist | ||
if (!is.null(input[[EXP$ID$FILENAME_BOX]]) && !is.null(input[[EXP$ID$DATAPROTECT_BOX]])) { | ||
# Check if the conditions are met | ||
if (input[[EXP$ID$FILENAME_BOX]] != "" && input[[EXP$ID$DATAPROTECT_BOX]]) { | ||
shinyjs::enable(EXP$ID$DOWNLOAD_BUTTON) | ||
} else { | ||
shinyjs::disable(EXP$ID$DOWNLOAD_BUTTON) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is mostly a stylistic suggestion. The only thing of value is the rejection of invalid destination file names. The rest, you can ignore:
# Check if the inputs exist | |
if (!is.null(input[[EXP$ID$FILENAME_BOX]]) && !is.null(input[[EXP$ID$DATAPROTECT_BOX]])) { | |
# Check if the conditions are met | |
if (input[[EXP$ID$FILENAME_BOX]] != "" && input[[EXP$ID$DATAPROTECT_BOX]]) { | |
shinyjs::enable(EXP$ID$DOWNLOAD_BUTTON) | |
} else { | |
shinyjs::disable(EXP$ID$DOWNLOAD_BUTTON) | |
} | |
} | |
sanitized_fname <- fs::path_sanitize(input[[EXP$ID$FILENAME_BOX]]) | |
enable_button <- (nchar(sanitized_fname) > 0) && isTRUE(input[[EXP$ID$DATAPROTECT_BOX]]) | |
shinyjs::toggleState(id = EXP$ID$DOWNLOAD_BUTTON, condition = enable_button) |
R/mod_export_count_table.R
Outdated
# Download | ||
output[[EXP$ID$DOWNLOAD_BUTTON]] <- shiny::downloadHandler( | ||
filename = function() { | ||
paste0(input[[EXP$ID$FILENAME_BOX]], ".xlsx") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoids illegal destination file names:
paste0(input[[EXP$ID$FILENAME_BOX]], ".xlsx") | |
sanitized_fname <- fs::path_sanitize(input[[EXP$ID$FILENAME_BOX]]) | |
paste0(sanitized_fname, ".xlsx") |
…internal task 242403.
Tweak docs, de-exported symbols, update API-spec-related module wrappers
- prevent app crash
Implement download button for events table
Percentage and number should be in separate columns
A disclaimer should be presented before the download letting the user know it is a unvalidated output (follow dv.listings strategy)