Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
105 commits
Select commit Hold shift + click to select a range
6f54851
feat: add script template for all operations but slope exclusions & r…
Gero1999 Nov 27, 2025
867957d
feat: make a function to deparse the fill-in the script template
Gero1999 Nov 27, 2025
84207ca
improve fun allowing reactives
Gero1999 Nov 27, 2025
27a933f
include reactives in template script
Gero1999 Nov 27, 2025
8fe876d
feat: include in zip folder for code script
Gero1999 Nov 27, 2025
3b8a051
save filters and mapping for userData
Gero1999 Nov 27, 2025
76ebe77
include in session datapath & final units table
Gero1999 Nov 27, 2025
b214e70
adapt script template for final units
Gero1999 Nov 27, 2025
3e6ee2c
feat: add ratio table to the script
Gero1999 Nov 27, 2025
07bc425
include slope_rules in userData
Gero1999 Nov 27, 2025
0dedfca
generalize fun
Gero1999 Nov 28, 2025
68ef670
integrate slope rules
Gero1999 Nov 28, 2025
cf2d74b
refactor: lintr
Gero1999 Nov 28, 2025
2a898fc
man: add docs
Gero1999 Nov 28, 2025
3a2f51d
update wordlist & global variables
Gero1999 Nov 28, 2025
09eb170
Apply suggestions from code review
Gero1999 Nov 28, 2025
4f49a86
refactor: get_session_script_code
Gero1999 Nov 28, 2025
bc3fb4d
refactor: lintr
Gero1999 Nov 28, 2025
bc10724
lintr, roxygen2
Gero1999 Nov 28, 2025
81667bb
Apply suggestions from code review (m-kolomanski)
Gero1999 Nov 28, 2025
c825d48
refactor: use switch for clean_deparse
Gero1999 Nov 28, 2025
9c0d993
test: clean_deparse() unit tests
Gero1999 Nov 28, 2025
fe89909
change warning to error in get_session_script_code.R
Gero1999 Nov 28, 2025
28e44f1
refactor: lintr
Gero1999 Nov 28, 2025
87a25c8
Apply suggestions from copilot review
Gero1999 Nov 28, 2025
0cf7275
handle empty cases in clean_deparse
Gero1999 Nov 28, 2025
16bf164
consider tbl_df option as data.frame for switch
Gero1999 Nov 28, 2025
198cd9e
switch has a high cyclomatic complexity, do instead methods
Gero1999 Nov 28, 2025
9555215
man: add docs
Gero1999 Nov 28, 2025
a844516
change error msg & change get_session_script_code > get_session_code
Gero1999 Nov 28, 2025
bd22e67
spelling: update wordlist
Gero1999 Nov 28, 2025
8d7925b
refactor script_template
Gero1999 Dec 1, 2025
930a02f
change utils::zip > zip::zipr for ZIP file exportation
Gero1999 Dec 5, 2025
eab4308
Merge remote-tracking branch 'origin/main' into 467-enhancement/r-script
Gero1999 Dec 5, 2025
163654b
refactor: include in pivot_wider option to locate profile rules
Gero1999 Dec 10, 2025
af86697
roxygen: add docs, update man & namespace
Gero1999 Dec 11, 2025
94596fe
add tests for new functionality in pivot_wider_pknca_results
Gero1999 Dec 11, 2025
e9122bb
update wordlist
Gero1999 Dec 11, 2025
3cbbd23
script: change |> with %>% & add flag_rules to pivot_wider()
Gero1999 Dec 12, 2025
afc3cd6
mv calculate_ratio_app to \R
Gero1999 Dec 12, 2025
521e856
install pkg if missing (fixes #727)
Gero1999 Dec 12, 2025
25ebaa6
merge: solve conflict in nca_results (png reading & R script)
Gero1999 Dec 15, 2025
06a3860
merge: this is the conflict solved
Gero1999 Dec 15, 2025
37e5009
internalize code in nca_results to pivot_Wider_pknca_results.R
Gero1999 Dec 15, 2025
1aea3b8
make test for pivot_wider_pknca_result new arg
Gero1999 Dec 15, 2025
d518136
docs: roxygenise
Gero1999 Dec 15, 2025
ecef742
refactor: lintr
Gero1999 Dec 15, 2025
5ae7cf7
roxygen: docs
Gero1999 Dec 15, 2025
df923f6
refactor: lintr
Gero1999 Dec 15, 2025
4f10ab8
docs: roxygenise
Gero1999 Dec 15, 2025
2f7fd28
fix: consider data if not present for userData
Gero1999 Dec 15, 2025
6972895
save extra_vars_to_keep in userData & use in template
Gero1999 Dec 15, 2025
862e2aa
bump pkg version
Gero1999 Dec 15, 2025
e288422
news: add r-script news
Gero1999 Dec 15, 2025
d89b4aa
rename test file
Gero1999 Dec 15, 2025
da94426
Merge branch 'main' into 467-enhancement/r-script
Gero1999 Dec 15, 2025
0e224d3
fix: consider excluded records also in script
Gero1999 Dec 17, 2025
19cdcde
fix: use group_vars to join the pivot_wider extra groups
Gero1999 Dec 17, 2025
f36af9a
merge: solved conflicts tab_nca.R & nca_results.R (deny incoming, jus…
Gero1999 Dec 23, 2025
8e29485
fix: issue parsing parameters_types with asyntactic list names
Gero1999 Dec 23, 2025
c35f58d
test: add case for asyntactic list deparsing
Gero1999 Dec 23, 2025
75e5fe0
fix: save parameter setts & adapt script template to new processing
Gero1999 Dec 23, 2025
6871759
refactor: lintr
Gero1999 Dec 23, 2025
2a68562
combine use of methods and split in-lines when multiple elements
Gero1999 Dec 24, 2025
c69e9bc
tests: add case for line-split in clean_deparse
Gero1999 Dec 24, 2025
a516839
refactor: add arg max_per_line
Gero1999 Dec 24, 2025
e2e2819
test: add tests for arg max_per_line
Gero1999 Dec 24, 2025
dcf98c8
man: roxygenise
Gero1999 Dec 24, 2025
3a44f36
refactor: lintr
Gero1999 Dec 24, 2025
5efa505
feat: save input data in ZIP & add relative path in script template
Gero1999 Dec 24, 2025
80e05ad
feat: use rep for clean_deparse
Gero1999 Dec 30, 2025
110595e
test: add the rep cases
Gero1999 Dec 30, 2025
17594ab
fix: ensure USUBJID is always char
Gero1999 Dec 30, 2025
e9254c4
refactor & docs
Gero1999 Dec 30, 2025
3b0a9e4
man: roxygenise
Gero1999 Dec 30, 2025
1ebfeaf
lintr: exclude template from check
Gero1999 Dec 30, 2025
0f4acac
fix: min_to_rep
Gero1999 Dec 30, 2025
28a2841
test: check that min_to_rep is accurate
Gero1999 Dec 30, 2025
e150b75
fix: pivot_wider_pknca_results intersect typpo
Gero1999 Dec 30, 2025
613ece8
fix: rep for all cases when needed
Gero1999 Dec 30, 2025
a845355
test: adapt test for new case expected
Gero1999 Dec 30, 2025
70fd838
refactor: simplify code with common fun
Gero1999 Dec 30, 2025
05f2c98
fix little typpos
Gero1999 Dec 30, 2025
1c3921b
merge: solve conflict keeping current and moving changes of flag muta…
Gero1999 Jan 7, 2026
9f02bfc
make fun to filter PPTESTCD or parameters not requested
Gero1999 Jan 8, 2026
c34b7bb
merge: accept both changes in PKNCA.R while in setup.R just accept cu…
Gero1999 Jan 8, 2026
377ed97
add general_exclusions (recent merge) to script_template.R
Gero1999 Jan 8, 2026
f19fc1a
man: roxygenise remove_parameters_not_requested
Gero1999 Jan 8, 2026
d140074
rename: remove_parameters_not_requested > remove_pp_not_requested
Gero1999 Jan 8, 2026
1aa17fe
spelling: unrequested
Gero1999 Jan 9, 2026
b5ef41d
test: todo excluding tmp weird case test
Gero1999 Jan 9, 2026
1b78da1
add global vars to zzz.R
Gero1999 Jan 9, 2026
a332b90
add todo & rm redundant data.frame check
Gero1999 Jan 9, 2026
af3fd7c
rename calculate_table_ratios_app > calculate_table ratios
Gero1999 Jan 12, 2026
7144766
use apply instead of a for loop in ratio_calculations.R
Gero1999 Jan 12, 2026
ef71957
man: roxygenise & refactor lintr
Gero1999 Jan 12, 2026
47c2b2f
Merge branch 'main' into 467-enhancement/r-script
Gero1999 Jan 12, 2026
1fb2b2a
Merge branch 'main' into 467-enhancement/r-script
Gero1999 Jan 14, 2026
892d4d8
readme: add R script msg
Gero1999 Jan 16, 2026
b80a389
add PR task temporarely until tests are added
Gero1999 Jan 16, 2026
5b60c53
Merge branch '467-enhancement/r-script' of https://github.com/pharmav…
Gero1999 Jan 16, 2026
cb35e83
Merge branch 'main' into 467-enhancement/r-script
Gero1999 Jan 16, 2026
20c47d4
rephrase news & bump pkg version
Gero1999 Jan 19, 2026
3370ca7
merge: conflict in pkg version solved using current
Gero1999 Jan 19, 2026
086f66e
Merge remote-tracking branch 'origin/main' into 467-enhancement/r-script
Gero1999 Jan 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ How to test features not covered by unit tests.
- [ ] New logic is documented
- [ ] App or package changes are reflected in NEWS
- [ ] Package version is incremented
- [ ] R script works with the new implementation (if applicable)

## Notes to reviewer

Expand Down
2 changes: 1 addition & 1 deletion .lintr
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ linters:
seq_linter = seq_linter()
)
encoding: "UTF-8"
exclusions: list("R/PKNCA_extra_parameters.R", "tests/testthat/test-PKNCA_extra_parameters.R")
exclusions: list("R/PKNCA_extra_parameters.R", "tests/testthat/test-PKNCA_extra_parameters.R", "inst/shiny/www/templates/script_template.R")
5 changes: 5 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export(apply_mapping)
export(calculate_f)
export(calculate_ratios)
export(calculate_summary_stats)
export(calculate_table_ratios)
export(check_slope_rule_overlap)
export(convert_volume_units)
export(create_metabfl)
Expand All @@ -46,6 +47,7 @@ export(g_pkcg03_log)
export(generate_tooltip_text)
export(get_conversion_factor)
export(get_label)
export(get_session_code)
export(interval_add_impute)
export(interval_remove_impute)
export(l_pkcl01)
Expand All @@ -65,6 +67,7 @@ export(pknca_calculate_f)
export(process_data_individual)
export(process_data_mean)
export(read_pk)
export(remove_pp_not_requested)
export(run_app)
export(simplify_unit)
export(translate_terms)
Expand Down Expand Up @@ -139,6 +142,8 @@ importFrom(stats,median)
importFrom(stats,na.omit)
importFrom(stats,sd)
importFrom(stats,setNames)
importFrom(stringr,fixed)
importFrom(stringr,str_detect)
importFrom(stringr,str_glue)
importFrom(stringr,str_split)
importFrom(stringr,str_trim)
Expand Down
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

* Enhancements to the slides outputs including grouping by PKNCA groups, dose profile, and additional grouping variables (#791)
* Option to include and apply NCA flag rules with reasons (NCAwXRS) as defined by ADNCA standards. Any record populated within these columns will be excluded for the NCA (#752)
* R script exported in ZIP folder to re-run and replicate App outputs (#789)
* Individual and Mean plots tabs now created using the same function, so the layout and plot themes are consistent across both plots (#712)
* New flagging rule for lambda-z calculations based on r-squared, R2 (#834)
* New Parameter Selection section in NCA tab allowing to select parameters by study type (#795)
Expand Down
31 changes: 31 additions & 0 deletions R/PKNCA.R
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,37 @@ PKNCA_hl_rules_exclusion <- function(res, rules) { # nolint
res
}

#' Filter Out Parameters Not Requested in PKNCA Results (Pivot Version)
#'
#' This function removes parameters from the PKNCA results that were not requested by the user,
#' using a pivoted approach that also handles bioavailability settings.
#'
#' @param pknca_res A PKNCA results object containing at least $data$intervals and $result.
#' @return The PKNCA results object with non requested parameters removed from $result.
#' @export
remove_pp_not_requested <- function(pknca_res) {
params <- c(setdiff(names(PKNCA::get.interval.cols()), c("start", "end")))
# Reshape intervals, filter
params_not_requested <- pknca_res$data$intervals %>%
pivot_longer(
cols = (any_of(params)),
names_to = "PPTESTCD",
values_to = "is_requested"
) %>%
mutate(PPTESTCD = translate_terms(PPTESTCD, "PKNCA", "PPTESTCD")) %>%
group_by(across(c(-impute, -is_requested))) %>%
summarise(
is_requested = any(is_requested),
.groups = "drop"
) %>%
filter(!is_requested)

# Filter for requested params based on intervals
pknca_res$result <- pknca_res$result %>%
anti_join(params_not_requested, by = intersect(names(.), names(params_not_requested)))
pknca_res
}

#' Add Exclusion Reasons to PKNCAdata Object
#'
#' This function adds exclusion reasons to the `exclude` column of the concentration object
Expand Down
191 changes: 191 additions & 0 deletions R/get_session_code.R
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: I believe this whole file should be in inst/shiny since it is only related and usable from within the application and it handles app-specific entities, like the session object.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a good point, but for automation purposes it can also be interesting for some programming-users to use this function with a settings file and then perhaps just change the input rather than go through the App

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, but currently the function needs the session object, so you have to go through the app anyway? I would agree if the function also accepted a settings file and had a more generic name, but currently it does not.

At the very least, if we want to implement it this way right now and then add the functionality of parsing a settings file instead of session object, we should have

  • more generic name
  • # TODO comments with link or number of appropriate issue to rely that intent.

Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
#' Generate a session script code in R that can replicate the App outputs
#'
#' @param template_path Path to the R script template (e.g., script_template.R)
#' @param session The session object containing userData, etc.
#' @param output_path Path to write the resulting script file (e.g., "output_script.R")
#' @return The output_path (invisibly)
#' @export
get_session_code <- function(template_path, session, output_path) {
# Helper to get value from session$userData by path (e.g., 'settings$method')
get_session_value <- function(path) {
parts <- strsplit(path, "\\$")[[1]]
obj <- session$userData
for (p in parts) {
if (inherits(obj[[p]], "reactive")) {
obj <- obj[[p]]()
} else {
obj <- obj[[p]]
}
if (is.null(obj)) {
return(NULL)
}
}
obj
}
# Read template
script <- readLines(template_path, warn = FALSE) %>%
paste(collapse = "\n")

# Find all session$userData$...
pattern <- "session\\$userData(\\$[a-zA-Z0-9_]+(\\(\\))?(\\$[a-zA-Z0-9_]+)*)"
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex pattern for finding session userData references includes an optional () suffix (line 30: (\\(\\))?), suggesting it can match reactive calls like session$userData$settings(). However, in get_session_value() (lines 11-23), the code already handles reactive objects by checking inherits(obj[[p]], "reactive") and calling them.

This creates potential ambiguity: the pattern would match session$userData$settings() but get_session_value("settings()") would fail because it tries to access a field named "settings()" instead of "settings". Consider clarifying the intended behavior or removing the (\\(\\))? from the pattern if reactive calls should not be included in the template.

Suggested change
pattern <- "session\\$userData(\\$[a-zA-Z0-9_]+(\\(\\))?(\\$[a-zA-Z0-9_]+)*)"
pattern <- "session\\$userData(\\$[a-zA-Z0-9_]+(\\$[a-zA-Z0-9_]+)*)"

Copilot uses AI. Check for mistakes.
matches <- gregexpr(pattern, script, perl = TRUE)[[1]]
if (matches[1] == -1) {
stop(
"Template has no placeholders (session$userData...) to substitute.",
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message could be more helpful by including the actual template path that was attempted. This would help users debug path issues. Consider:

stop(
  "Template at '", template_path, "' has no placeholders (session$userData...) to substitute. ",
  "This may be due to an incorrect file path, a missing template, ",
  "or a modified template without placeholders."
)
Suggested change
"Template has no placeholders (session$userData...) to substitute.",
"Template at '", template_path, "' has no placeholders (session$userData...) to substitute. ",

Copilot uses AI. Check for mistakes.
"This may be due to an incorrect file path, a missing template, ",
"or a modified template without placeholders."
)
}

# Replace each match with deparsed value
for (i in rev(seq_along(matches))) {
start <- matches[i]
len <- attr(matches, "match.length")[i]
matched <- substr(script, start, start + len - 1)
# Extract the path after session$userData$
path <- sub("^session\\$userData\\$", "", matched)
value <- get_session_value(path)

deparsed <- clean_deparse(value, max_per_line = 15)
script <- paste0(
substr(script, 1, start - 1),
deparsed,
substr(script, start + len, nchar(script))
)
}

# Split back into lines
script_lines <- strsplit(script, "\n")[[1]]
writeLines(script_lines, output_path)
invisible(output_path)
}

#' Convert R objects into reproducible R code strings (internal)
#'
#' This internal S3 generic converts common R objects (data frames, lists,
#' atomic vectors, etc.) into character strings containing R code that will
#' reconstruct the object. It is used by the app script generator to
#' serialize `session$userData` values into a runnable R script.
#'
#' @param obj An R object to convert to a string of R code.
#' @param max_per_line Maximum number of elements to include per line for
#' long vectors/lists.
#' @param min_to_rep Minimum number of repeated elements to use `rep()` for
#' long vectors/lists.
#' @param indent Integer indentation level for multi-line outputs.
#' @return A single string containing R code that, when evaluated, will
#' reconstruct `obj` (or a close approximation for complex types).
#' @keywords internal
clean_deparse <- function(obj, indent = 0, max_per_line = 10, min_to_rep = 3) {
# Handle trivial length-0 constructors (character(0), numeric(0), list(), data.frame(), ...)
if (length(obj) == 0 && !is.null(obj)) {
return(paste0(class(obj)[1], "()"))
}
UseMethod("clean_deparse")
}

#' @noRd
clean_deparse.default <- function(obj, indent = 0, max_per_line = 10, min_to_rep = 3) {
paste(deparse(obj, width.cutoff = 500), collapse = "")
}

#' @noRd
clean_deparse.data.frame <- function(obj, indent = 0, max_per_line = 10, min_to_rep = 3) {
ind <- paste(rep(" ", indent), collapse = "")

cols <- lapply(obj, function(col) {
clean_deparse(col, indent + 1, max_per_line = max_per_line)
})

col_strs <- paste0(ind, " ", names(obj), " = ", unlist(cols))
if (length(col_strs) > 1) {
not_last <- seq_len(length(col_strs) - 1)
col_strs[not_last] <- paste0(col_strs[not_last], ",")
}
paste0("data.frame(\n", paste(col_strs, collapse = "\n"), "\n", ind, ")")
}

#' @noRd
clean_deparse.list <- function(obj, indent = 0, max_per_line = 10, min_to_rep = 3) {
ind <- paste(rep(" ", indent), collapse = "")
n <- length(obj)
nms <- names(obj)
items <- vapply(seq_len(n), FUN.VALUE = "", function(i) {
name <- if (!is.null(nms) && nzchar(nms[i])) nms[i] else paste0("V", i)
# Quote name if not a valid R symbol
if (!grepl("^[A-Za-z.][A-Za-z0-9._]*$", name)) {
name <- sprintf('"%s"', name)
}
val <- obj[[i]]
val_str <- clean_deparse(val, indent + 1, max_per_line = max_per_line)
paste0(name, " = ", val_str)
})
if (length(items) > 1) {
not_last <- seq_len(length(items) - 1)
items[not_last] <- paste0(items[not_last], ",")
}
item_strs <- paste0(ind, " ", items)
paste0("list(\n", paste(item_strs, collapse = "\n"), "\n", ind, ")")
}

#' @noRd

clean_deparse.character <- function(obj, indent = 0, max_per_line = 10, min_to_rep = 3) {
obj <- sprintf('"%s"', obj)
.deparse_vector(obj, indent, max_per_line, min_to_rep)
}

#' @noRd

clean_deparse.numeric <- function(obj, indent = 0, max_per_line = 10, min_to_rep = 3) {
obj <- sprintf("%s", obj)
.deparse_vector(obj, indent, max_per_line, min_to_rep)
}

#' @noRd
clean_deparse.integer <- function(obj, indent = 0, max_per_line = 10, min_to_rep = 3) {
clean_deparse.numeric(obj, indent = indent, max_per_line = max_per_line, min_to_rep = min_to_rep)
}

#' @noRd
clean_deparse.logical <- function(obj, indent = 0, max_per_line = 10, min_to_rep = 3) {
obj <- as.character(obj)
.deparse_vector(obj, indent, max_per_line, min_to_rep)
}

#' Internal helper to deparse atomic vectors
#' using repetition simplification (rep) and line splitting
#'
#' @noRd
.deparse_vector <- function(obj, indent = 0, max_per_line = 10, min_to_rep = 3) {
n <- length(obj)
if (n == 1) {
return(obj)
} else {
rle_obj <- rle(obj)
lines_obj <- c()
for (i in seq_along(rle_obj$values)) {
val <- rle_obj$values[i]
len <- rle_obj$lengths[i]
if (len >= min_to_rep) {
rep_obj <- paste0("rep(", val, ", ", len, ")")
lines_obj <- c(lines_obj, rep_obj)
} else {
lines_obj <- c(lines_obj, rep(val, len))
}
}
}
ind <- paste(rep(" ", indent), collapse = "")
lines <- split(lines_obj, ceiling(seq_along(lines_obj) / max_per_line))
line_strs <- vapply(lines, function(x) paste(x, collapse = ", "), "")
if (is.list(lines) && length(lines) > 1) {
out <- paste0(ind, " ", line_strs, collapse = ",\n")
paste0("c(\n", out, "\n", ind, ")")
} else {
paste0("c(", paste0(line_strs, collapse = ",\n"), ")")
}
}
Comment on lines +133 to +187
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_session_code writes a runnable R script by interpolating values from session$userData, but clean_deparse.character and .deparse_vector embed raw string contents into the generated code without escaping quotes, backslashes, or newlines. An attacker who controls any session$userData string used in the template (for example via uploaded data, mapping names, or text inputs) can craft a value like "abc"); system("whoami"); # so that the exported session_code.R contains and executes arbitrary R commands when run. To mitigate this, ensure that all character data and list/data.frame names are safely encoded for R string literals (e.g., via robust escaping or dput-style serialization) before being inserted into the script, or treat the export as pure data rather than executable code.

Copilot uses AI. Check for mistakes.

# TODO (Gerardo): Create a linked function
# to obtain the code from a settings file
# (#826)
Loading