From dadec8903dfd39e2ba81b951d0b82a5cae600403 Mon Sep 17 00:00:00 2001 From: Nikolas Burkoff Date: Mon, 23 May 2022 15:39:40 +0100 Subject: [PATCH] add reporter (#635) --- DESCRIPTION | 2 + NAMESPACE | 3 + NEWS.md | 5 + R/module_nested_tabs.R | 33 +++-- R/module_tabs_with_filters.R | 8 +- R/module_teal.R | 15 +- R/modules.R | 45 ++++++ R/reporter_previewer_module.R | 29 ++++ README.md | 1 + _pkgdown.yml | 3 + man/append_module.Rd | 20 +++ man/is_reporter_used.Rd | 27 ++++ man/reporter_previewer_module.Rd | 24 ++++ man/srv_nested_tabs.Rd | 10 +- man/srv_tabs_with_filters.Rd | 4 +- staged_dependencies.yaml | 3 + tests/testthat/test-module_nested_tabs.R | 16 ++- .../testthat/test-module_tabs_with_filters.R | 13 +- tests/testthat/test-modules.R | 129 ++++++++++++++++++ tests/testthat/test-report_previewer_module.R | 14 ++ vignettes/teal.Rmd | 8 ++ 21 files changed, 381 insertions(+), 31 deletions(-) create mode 100644 R/reporter_previewer_module.R create mode 100644 man/append_module.Rd create mode 100644 man/is_reporter_used.Rd create mode 100644 man/reporter_previewer_module.Rd create mode 100644 tests/testthat/test-report_previewer_module.R diff --git a/DESCRIPTION b/DESCRIPTION index 327573c892..fa290e0ac4 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -36,6 +36,7 @@ Imports: styler, teal.code, teal.logger, + teal.reporter, teal.slice, utils Suggests: @@ -75,6 +76,7 @@ Collate: 'module_teal.R' 'module_teal_with_splash.R' 'modules_debugging.R' + 'reporter_previewer_module.R' 'show_rcode_modal.R' 'teal.R' 'utils.R' diff --git a/NAMESPACE b/NAMESPACE index ba439fb816..f8d98570fc 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,5 +1,7 @@ # Generated by roxygen2: do not edit by hand +S3method(is_reporter_used,teal_module) +S3method(is_reporter_used,teal_modules) S3method(print,default_filter) S3method(print,teal_module) S3method(print,teal_modules) @@ -23,6 +25,7 @@ export(init) export(log_app_usage) export(module) export(modules) +export(reporter_previewer_module) export(root_modules) export(show_rcode_modal) export(srv_teal_with_splash) diff --git a/NEWS.md b/NEWS.md index a1002dd806..3ad7321290 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,10 @@ # teal 0.11.0.9007 +### Enhancements + +* Added new function `reporter_previewer_module` to wrap the `teal.reporter` package previewer functionality as a `teal` module. +* `teal` now supports `modules` which include reporting. If any `module` which supports reporting is included then a `reporter_previewer_module` is included. + ### Breaking changes * Deprecated `bookmarkableShinyApp`. In future releases the `teal` framework will stop supporting shiny bookmarking (which has not officially been supported); it may be officially supported in the future. Note the filter panel in `teal.slice` retains its ability to save and restore its state if used in a standalone `shiny` app with bookmarking. diff --git a/R/module_nested_tabs.R b/R/module_nested_tabs.R index f1ec0c684f..7ddba56c41 100644 --- a/R/module_nested_tabs.R +++ b/R/module_nested_tabs.R @@ -86,15 +86,13 @@ ui_nested_tabs.teal_modules <- function(id, modules, datasets, depth = 0L) { ui_nested_tabs.teal_module <- function(id, modules, datasets, depth = 0L) { stopifnot(is(datasets, "FilteredData")) args <- isolate(teal.transform::resolve_delayed(modules$ui_args, datasets)) + args <- c(list(id = id, datasets = datasets), args) tags$div( id = id, class = "teal_module", tagList( if (depth >= 2L) div(style = "margin-top: 1.5rem;"), - do.call( - modules$ui, - c(list(id = id, datasets = datasets), args) - ) + do.call(modules$ui, args) ) ) } @@ -122,22 +120,23 @@ ui_nested_tabs.teal_module <- function(id, modules, datasets, depth = 0L) { #' #' @return `reactive` which returns the active module that corresponds to the selected tab #' @keywords internal -srv_nested_tabs <- function(id, datasets, modules) { +srv_nested_tabs <- function(id, datasets, modules, reporter) { stopifnot(inherits(datasets, "FilteredData")) + stopifnot(inherits(reporter, "Reporter")) UseMethod("srv_nested_tabs", modules) } #' @rdname srv_nested_tabs #' @export #' @keywords internal -srv_nested_tabs.default <- function(id, datasets, modules) { +srv_nested_tabs.default <- function(id, datasets, modules, reporter) { stop("Modules class not supported: ", paste(class(modules), collapse = " ")) } #' @rdname srv_nested_tabs #' @export #' @keywords internal -srv_nested_tabs.teal_modules <- function(id, datasets, modules) { +srv_nested_tabs.teal_modules <- function(id, datasets, modules, reporter) { moduleServer(id = id, module = function(input, output, session) { logger::log_trace( paste( @@ -147,7 +146,7 @@ srv_nested_tabs.teal_modules <- function(id, datasets, modules) { ) ) modules_reactive <- sapply(names(modules$children), USE.NAMES = TRUE, function(id) { - srv_nested_tabs(id = id, datasets = datasets, modules = modules$children[[id]]) + srv_nested_tabs(id = id, datasets = datasets, modules = modules$children[[id]], reporter = reporter) }) get_active_module <- reactive({ @@ -168,7 +167,7 @@ srv_nested_tabs.teal_modules <- function(id, datasets, modules) { #' @rdname srv_nested_tabs #' @export #' @keywords internal -srv_nested_tabs.teal_module <- function(id, datasets, modules) { +srv_nested_tabs.teal_module <- function(id, datasets, modules, reporter) { logger::log_trace( paste( "srv_nested_tabs.teal_module initializing the module with:", @@ -179,16 +178,16 @@ srv_nested_tabs.teal_module <- function(id, datasets, modules) { modules$server_args <- teal.transform::resolve_delayed(modules$server_args, datasets) is_module_server <- isTRUE("id" %in% names(formals(modules$server))) + + args <- c(list(id = id, datasets = datasets), modules$server_args) + if (is_reporter_used(modules)) { + args <- c(args, list(reporter = reporter)) + } + if (is_module_server) { - do.call(modules$server, c(list(id = id, datasets = datasets), modules$server_args)) + do.call(modules$server, args) } else { - do.call( - callModule, - c( - list(module = modules$server, id = id, datasets = datasets), - modules$server_args - ) - ) + do.call(callModule, c(args, list(module = modules$server))) } reactive(modules) } diff --git a/R/module_tabs_with_filters.R b/R/module_tabs_with_filters.R index b6c77ca5ec..a4dbf204d7 100644 --- a/R/module_tabs_with_filters.R +++ b/R/module_tabs_with_filters.R @@ -134,15 +134,17 @@ ui_tabs_with_filters <- function(id, modules, datasets) { #' @param datasets (`FilteredData`)\cr #' object to store filter state and filtered datasets, shared across modules. For more #' details see [`teal.slice::FilteredData`]. +#' @param reporter (`Reporter`) object from `teal.reporter` #' @return `reactive` currently selected active_module #' @keywords internal -srv_tabs_with_filters <- function(id, datasets, modules, filter) { - stopifnot(is(datasets, "FilteredData")) +srv_tabs_with_filters <- function(id, datasets, modules, reporter, filter) { + checkmate::assert_class(datasets, "FilteredData") + checkmate::assert_class(reporter, "Reporter") moduleServer(id, function(input, output, session) { logger::log_trace( "srv_tabs_with_filters initializing the module with datasets { paste(datasets$datanames(), collapse = ' ') }." ) - active_module <- srv_nested_tabs(id = "root", datasets = datasets, modules = modules) + active_module <- srv_nested_tabs(id = "root", datasets = datasets, modules = modules, reporter = reporter) active_datanames <- eventReactive( eventExpr = active_module(), diff --git a/R/module_teal.R b/R/module_teal.R index be97628f34..c1f6f98094 100644 --- a/R/module_teal.R +++ b/R/module_teal.R @@ -193,6 +193,13 @@ srv_teal <- function(id, modules, raw_data, filter = list()) { datasets }) + + reporter <- teal.reporter::Reporter$new() + + if (is_reporter_used(modules)) { + modules <- append_module(modules, reporter_previewer_module()) + } + # Replace splash / welcome screen once data is loaded ---- # ignoreNULL to not trigger at the beginning when data is NULL # just handle it once because data obtained through delayed loading should @@ -209,7 +216,11 @@ srv_teal <- function(id, modules, raw_data, filter = list()) { where = "beforeEnd", # we put it into a div, so it can easily be removed as a whole, also when it is a tagList (and not # just the first item of the tagList) - ui = div(ui_tabs_with_filters(session$ns("main_ui"), modules = modules, datasets = datasets_reactive())), + ui = div(ui_tabs_with_filters( + session$ns("main_ui"), + modules = modules, + datasets = datasets_reactive() + )), # needed so that the UI inputs are available and can be immediately updated, otherwise, updating may not # have any effect as they are ignored when not present, see note in `module_add_filter_variable.R` immediate = TRUE @@ -218,6 +229,7 @@ srv_teal <- function(id, modules, raw_data, filter = list()) { # switching filter to bookmarked state if (!is.null(saved_datasets_state())) filter <- saved_datasets_state() + # must make sure that this is only executed once as modules assume their observers are only # registered once (calling server functions twice would trigger observers twice each time) # `once = TRUE` ensures this @@ -225,6 +237,7 @@ srv_teal <- function(id, modules, raw_data, filter = list()) { id = "main_ui", datasets = datasets_reactive(), modules = modules, + reporter = reporter, filter = filter ) return(active_module) diff --git a/R/modules.R b/R/modules.R index 4157b6ea86..1b332a8dba 100644 --- a/R/modules.R +++ b/R/modules.R @@ -92,6 +92,51 @@ modules <- function(..., label = "root") { } + +#' Function which appends a teal_module onto the children of a teal_modules object +#' @keywords internal +#' @param modules `teal_modules` +#' @param module `teal_module` object to be appended onto the children of `modules` +#' @return `teal_modules` object with `module` appended +append_module <- function(modules, module) { + checkmate::assert_class(modules, "teal_modules") + checkmate::assert_class(module, "teal_module") + modules$children <- c(modules$children, list(module)) + labels <- vapply(modules$children, function(submodule) submodule$label, character(1)) + names(modules$children) <- make.unique(gsub("[^[:alnum:]]", "_", tolower(labels)), sep = "_") + modules +} + +#' Does the object make use of `teal.reporter` reporting +#' @param modules `teal_module` or `teal_modules` object +#' @return `logical` whether the object makes use of `teal.reporter` reporting +#' @rdname is_reporter_used +#' @keywords internal +is_reporter_used <- function(modules) { + UseMethod("is_reporter_used", modules) +} + +#' @rdname is_reporter_used +#' @keywords internal +is_reporter_used.default <- function(modules) { + stop("is_reporter_used function not implemented for this object") +} + +#' @rdname is_reporter_used +#' @export +#' @keywords internal +is_reporter_used.teal_modules <- function(modules) { + any(unlist(lapply(modules$children, function(x) is_reporter_used(x)))) +} + +#' @rdname is_reporter_used +#' @export +#' @keywords internal +is_reporter_used.teal_module <- function(modules) { + "reporter" %in% names(formals(modules$server)) +} + + #' Deprecated: Creates the root modules container #' #' @description `r lifecycle::badge("deprecated")` diff --git a/R/reporter_previewer_module.R b/R/reporter_previewer_module.R new file mode 100644 index 0000000000..291d6a7541 --- /dev/null +++ b/R/reporter_previewer_module.R @@ -0,0 +1,29 @@ +#' Create a `teal` module for previewing a report +#' +#' This function wraps [teal.reporter::reporter_previewer_ui()] and +#' [teal.reporter::reporter_previewer_srv()] into a `teal_module` to be +#' used in `teal` applications. +#' +#' If you are creating a `teal` application using [teal::init()] then this +#' module will be added to your application automatically if any of your `teal modules` +#' support report generation +#' +#' @inheritParams module +#' @return `teal_module` containing the `teal.reporter` previewer functionality +#' @export +reporter_previewer_module <- function(label = "Report previewer") { + checkmate::assert_string(label) + srv <- function(id, datasets, reporter, ...) { + teal.reporter::reporter_previewer_srv(id, reporter, ...) + } + + ui <- function(id, datasets, ...) { + teal.reporter::reporter_previewer_ui(id, ...) + } + + module( + label = label, + server = srv, ui = ui, + server_args = list(), ui_args = list(), filters = NULL + ) +} diff --git a/README.md b/README.md index f73956cc3c..82d3d5c9da 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ A lot of the functionality of the `teal` framework derives from the following pa - [`teal.code`](https://github.com/insightsengineering/teal.code): handles reproducibility of outputs. - [`teal.transform`](https://github.com/insightsengineering/teal.transform): standardizes extracting and merging data. - [`teal.logger`](https://github.com/insightsengineering/teal.logger): standardizes logging within `teal` framework. +- [`teal.reporter`](https://github.com/insightsengineering/teal.reporter): allows `teal` applications to generate reports. diff --git a/_pkgdown.yml b/_pkgdown.yml index 0a69bf79e2..beb3f77130 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -23,6 +23,9 @@ reference: desc: A simple example teal module contents: - example_module + - title: Report previewer module + contents: + - reporter_previewer_module - title: Functions moved to other packages desc: These functions have been moved from teal and will be deprecated. contents: diff --git a/man/append_module.Rd b/man/append_module.Rd new file mode 100644 index 0000000000..3c3be16559 --- /dev/null +++ b/man/append_module.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/modules.R +\name{append_module} +\alias{append_module} +\title{Function which appends a teal_module onto the children of a teal_modules object} +\usage{ +append_module(modules, module) +} +\arguments{ +\item{modules}{\code{teal_modules}} + +\item{module}{\code{teal_module} object to be appended onto the children of \code{modules}} +} +\value{ +\code{teal_modules} object with \code{module} appended +} +\description{ +Function which appends a teal_module onto the children of a teal_modules object +} +\keyword{internal} diff --git a/man/is_reporter_used.Rd b/man/is_reporter_used.Rd new file mode 100644 index 0000000000..9472dad603 --- /dev/null +++ b/man/is_reporter_used.Rd @@ -0,0 +1,27 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/modules.R +\name{is_reporter_used} +\alias{is_reporter_used} +\alias{is_reporter_used.default} +\alias{is_reporter_used.teal_modules} +\alias{is_reporter_used.teal_module} +\title{Does the object make use of \code{teal.reporter} reporting} +\usage{ +is_reporter_used(modules) + +\method{is_reporter_used}{default}(modules) + +\method{is_reporter_used}{teal_modules}(modules) + +\method{is_reporter_used}{teal_module}(modules) +} +\arguments{ +\item{modules}{\code{teal_module} or \code{teal_modules} object} +} +\value{ +\code{logical} whether the object makes use of \code{teal.reporter} reporting +} +\description{ +Does the object make use of \code{teal.reporter} reporting +} +\keyword{internal} diff --git a/man/reporter_previewer_module.Rd b/man/reporter_previewer_module.Rd new file mode 100644 index 0000000000..000e3303e3 --- /dev/null +++ b/man/reporter_previewer_module.Rd @@ -0,0 +1,24 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/reporter_previewer_module.R +\name{reporter_previewer_module} +\alias{reporter_previewer_module} +\title{Create a \code{teal} module for previewing a report} +\usage{ +reporter_previewer_module(label = "Report previewer") +} +\arguments{ +\item{label}{(\code{character}) Label shown in the navigation item for the module.} +} +\value{ +\code{teal_module} containing the \code{teal.reporter} previewer functionality +} +\description{ +This function wraps \code{\link[teal.reporter:reporter_previewer_ui]{teal.reporter::reporter_previewer_ui()}} and +\code{\link[teal.reporter:reporter_previewer_srv]{teal.reporter::reporter_previewer_srv()}} into a \code{teal_module} to be +used in \code{teal} applications. +} +\details{ +If you are creating a \code{teal} application using \code{\link[=init]{init()}} then this +module will be added to your application automatically if any of your \verb{teal modules} +support report generation +} diff --git a/man/srv_nested_tabs.Rd b/man/srv_nested_tabs.Rd index 2f23005f8a..488d52ab44 100644 --- a/man/srv_nested_tabs.Rd +++ b/man/srv_nested_tabs.Rd @@ -7,13 +7,13 @@ \alias{srv_nested_tabs.teal_module} \title{Server function that returns currently active module} \usage{ -srv_nested_tabs(id, datasets, modules) +srv_nested_tabs(id, datasets, modules, reporter) -\method{srv_nested_tabs}{default}(id, datasets, modules) +\method{srv_nested_tabs}{default}(id, datasets, modules, reporter) -\method{srv_nested_tabs}{teal_modules}(id, datasets, modules) +\method{srv_nested_tabs}{teal_modules}(id, datasets, modules, reporter) -\method{srv_nested_tabs}{teal_module}(id, datasets, modules) +\method{srv_nested_tabs}{teal_module}(id, datasets, modules, reporter) } \arguments{ \item{id}{(\code{character})\cr @@ -29,6 +29,8 @@ details see \code{\link[teal.slice:FilteredData]{teal.slice::FilteredData}}.} \item{modules}{(\code{list} or \code{teal_modules})\cr nested list of \code{teal_modules} or \code{module} objects. See \code{\link[=modules]{modules()}} and \code{\link[=module]{module()}} for more details.} + +\item{reporter}{(\code{Reporter}) object from \code{teal.reporter}} } \value{ \code{reactive} which returns the active module that corresponds to the selected tab diff --git a/man/srv_tabs_with_filters.Rd b/man/srv_tabs_with_filters.Rd index e5694e03c8..b0ee26d3a6 100644 --- a/man/srv_tabs_with_filters.Rd +++ b/man/srv_tabs_with_filters.Rd @@ -4,7 +4,7 @@ \alias{srv_tabs_with_filters} \title{Server function} \usage{ -srv_tabs_with_filters(id, datasets, modules, filter) +srv_tabs_with_filters(id, datasets, modules, reporter, filter) } \arguments{ \item{id}{(\code{character})\cr @@ -21,6 +21,8 @@ details see \code{\link[teal.slice:FilteredData]{teal.slice::FilteredData}}.} nested list of \code{teal_modules} or \code{module} objects. See \code{\link[=modules]{modules()}} and \code{\link[=module]{module()}} for more details.} +\item{reporter}{(\code{Reporter}) object from \code{teal.reporter}} + \item{filter}{(\code{list})\cr You can define filters that show when the app starts. List names should be named according to datanames passed to the \code{data} argument. diff --git a/staged_dependencies.yaml b/staged_dependencies.yaml index a662f36037..df415c11f8 100644 --- a/staged_dependencies.yaml +++ b/staged_dependencies.yaml @@ -21,6 +21,9 @@ upstream_repos: insightsengineering/teal.logger: repo: insightsengineering/teal.logger host: https://github.com + insightsengineering/teal.reporter: + repo: insightsengineering/teal.reporter + host: https://github.com insightsengineering/scda: repo: insightsengineering/scda host: https://github.com diff --git a/tests/testthat/test-module_nested_tabs.R b/tests/testthat/test-module_nested_tabs.R index d0f40d04cc..1906bfa08a 100644 --- a/tests/testthat/test-module_nested_tabs.R +++ b/tests/testthat/test-module_nested_tabs.R @@ -25,6 +25,13 @@ test_module4 <- module( filters = NULL ) +testthat::test_that("srv_nested_tabs throws error if reporter is not inherited from class Reporter", { + testthat::expect_error( + srv_nested_tabs(id, datasets = filtered_data, modules = modules(test_module1), reporter = list()), + "inherits\\(reporter, \"Reporter\"\\) is not TRUE" + ) +}) + # server ------- testthat::test_that("passed shiny module is initialized", { testthat::expect_message( @@ -33,7 +40,8 @@ testthat::test_that("passed shiny module is initialized", { args = list( id = "test", datasets = filtered_data, - modules = modules(test_module1) + modules = modules(test_module1), + reporter = teal.reporter::Reporter$new() ), expr = NULL ), @@ -51,7 +59,8 @@ testthat::test_that("nested teal-modules are initialized", { modules = modules( modules(label = "tab1", test_module1, test_module2), modules(label = "tab2", test_module3, test_module4) - ) + ), + reporter = teal.reporter::Reporter$new() ), expr = NULL ) @@ -68,7 +77,8 @@ out <- shiny::testServer( modules = modules( modules(label = "tab1", test_module1, test_module2), modules(label = "tab2", test_module3, test_module4) - ) + ), + reporter = teal.reporter::Reporter$new() ), expr = { # to adjust input modules to the active modules (server_args is dropped when NULL) diff --git a/tests/testthat/test-module_tabs_with_filters.R b/tests/testthat/test-module_tabs_with_filters.R index fe40a6d901..34d6f952cd 100644 --- a/tests/testthat/test-module_tabs_with_filters.R +++ b/tests/testthat/test-module_tabs_with_filters.R @@ -15,6 +15,13 @@ test_module2 <- module( filters = "mtcars" ) +testthat::test_that("srv_tabs_with_filters throws error if reporter is not of class Reporter", { + testthat::expect_error( + srv_tabs_with_filters(id, datasets = filtered_data, modules = modules(test_module1), reporter = list()), + "Assertion on 'reporter' failed" + ) +}) + testthat::test_that("active_datanames() returns dataname from single tab", { shiny::testServer( app = srv_tabs_with_filters, @@ -22,7 +29,8 @@ testthat::test_that("active_datanames() returns dataname from single tab", { id = "test", datasets = filtered_data, modules = modules(test_module1), - filter = list() + filter = list(), + reporter = teal.reporter::Reporter$new() ), expr = { testthat::expect_identical(active_datanames(), "iris") @@ -37,7 +45,8 @@ testthat::test_that("active_datanames() returns dataname from active tab after c id = "test", datasets = filtered_data, modules = modules(test_module1, test_module2), - filter = list() + filter = list(), + reporter = teal.reporter::Reporter$new() ), expr = { testthat::expect_error(active_datanames()) # to trigger active_module diff --git a/tests/testthat/test-modules.R b/tests/testthat/test-modules.R index 3c2418c753..a8b5d94d59 100644 --- a/tests/testthat/test-modules.R +++ b/tests/testthat/test-modules.R @@ -531,3 +531,132 @@ testthat::test_that("modules_depth increases depth by 1 for each teal_modules", 3L ) }) + + +# is_reporter_used ----- +get_srv_and_ui <- function() { + return(list( + server_fun = function(id, datasets) {}, # nolint + ui_fun = function(id, ...) { + tags$p(paste0("id: ", id)) + } + )) +} + +create_mod <- function(label = "label") { + srv_and_ui <- get_srv_and_ui() + mod <- module(label = label, server = srv_and_ui$server_fun, ui = srv_and_ui$ui_fun, filters = "") +} + +testthat::test_that("is_reporter_used throws error if object is not teal_module or teal_modules", { + testthat::expect_error(is_reporter_used(5), "is_reporter_used function not implemented for this object") + testthat::expect_error(is_reporter_used(list()), "is_reporter_used function not implemented for this object") +}) + +testthat::test_that("is_reporter_used returns true if teal_module has reporter in server function args", { + testthat::expect_false(is_reporter_used(create_mod())) +}) + +testthat::test_that("is_reporter_used returns false if teal_module does not have reporter in server function args", { + testthat::expect_false(is_reporter_used(create_mod())) +}) + + +testthat::test_that("is_reporter_used returns false if teal_modules has no children using reporter", { + srv_and_ui <- get_srv_and_ui() + + mod <- module( + label = "label", + server = srv_and_ui$server_fun, + ui = srv_and_ui$ui_fun, + filters = "" + ) + + mods <- modules(label = "lab", mod, mod) + testthat::expect_false(is_reporter_used(mods)) + + mods <- modules(label = "lab", mods, mod, mod) + testthat::expect_false(is_reporter_used(mods)) +}) + +testthat::test_that("is_reporter_used returns true if teal_modules has at least one child using reporter", { + server_fun_with_reporter <- function(id, datasets, reporter) {} # nolint + + srv_and_ui <- get_srv_and_ui() + + mod <- module(label = "label", server = srv_and_ui$server_fun, ui = srv_and_ui$ui_fun, filters = "") + + mod_with_reporter <- module(label = "label", server = server_fun_with_reporter, ui = srv_and_ui$ui_fun, filters = "") + + mods <- modules(label = "lab", mod, mod_with_reporter) + testthat::expect_true(is_reporter_used(mods)) + + mods_2 <- modules(label = "lab", mods, mod, mod) + testthat::expect_true(is_reporter_used(mods_2)) + + mods_3 <- modules(label = "lab", modules(label = "lab", mod, mod), mod_with_reporter, mod) + testthat::expect_true(is_reporter_used(mods_3)) +}) + +# ---- append_module +testthat::test_that("append_module throws error when modules is not inherited from teal_modules", { + mod <- create_mod() + + testthat::expect_error( + append_module(mod, mod), + "Assertion on 'modules' failed: Must inherit from class 'teal_modules'" + ) + + testthat::expect_error( + append_module(mod, list(mod)), + "Assertion on 'modules' failed: Must inherit from class 'teal_modules'" + ) +}) + +testthat::test_that("append_module throws error is module is not inherited from teal_module", { + mod <- create_mod() + mods <- modules(label = "A", mod) + + testthat::expect_error( + append_module(mods, mods), + "Assertion on 'module' failed: Must inherit from class 'teal_module'" + ) + + testthat::expect_error( + append_module(mods, list(mod)), + "Assertion on 'module' failed: Must inherit from class 'teal_module'" + ) +}) + +testthat::test_that("append_module appends a module to children of not nested teal_modules", { + mod <- create_mod(label = "a") + mod2 <- create_mod(label = "b") + mods <- modules(label = "c", mod, mod2) + mod3 <- create_mod(label = "d") + + appended_mods <- append_module(mods, mod3) + testthat::expect_equal(appended_mods$children, list(a = mod, b = mod2, d = mod3)) +}) + + +testthat::test_that("append_module appends a module to children of nested teal_modules", { + mod <- create_mod(label = "a") + mod2 <- create_mod(label = "b") + mods <- modules(label = "c", mod) + mods2 <- modules(label = "e", mods, mod2) + mod3 <- create_mod(label = "d") + + appended_mods <- append_module(mods2, mod3) + testthat::expect_equal(appended_mods$children, list(c = mods, b = mod2, d = mod3)) +}) + +testthat::test_that("append_module produces teal_modules with unique named children", { + mod <- create_mod(label = "a") + mod2 <- create_mod(label = "c") + mods <- modules(label = "c", mod, mod2) + mod3 <- create_mod(label = "c") + + appended_mods <- append_module(mods, mod3) + mod_names <- names(appended_mods$children) + testthat::expect_equal(mod_names, unique(mod_names)) +}) diff --git a/tests/testthat/test-report_previewer_module.R b/tests/testthat/test-report_previewer_module.R new file mode 100644 index 0000000000..d5d7b900ec --- /dev/null +++ b/tests/testthat/test-report_previewer_module.R @@ -0,0 +1,14 @@ +testthat::test_that("report_previewer_module throws error if label is not string", { + expect_error(reporter_previewer_module(label = 5), "Assertion on 'label' failed: Must be of type 'string'") + expect_error(reporter_previewer_module(label = c("A", "B")), "Assertion on 'label' failed: Must have length 1.") +}) + +testthat::test_that("report_previewer_module throws no error and stores label if label is string", { + expect_error(r_p_m <- reporter_previewer_module(label = "My label"), NA) + expect_equal(r_p_m$label, "My label") +}) + +testthat::test_that("report_previewer_module default label is Report previewer ", { + r_p_m <- reporter_previewer_module() + expect_equal(r_p_m$label, "Report previewer") +}) diff --git a/vignettes/teal.Rmd b/vignettes/teal.Rmd index b112ea2136..aa0ee849ef 100644 --- a/vignettes/teal.Rmd +++ b/vignettes/teal.Rmd @@ -19,6 +19,7 @@ CDISC clinical trial data. `teal` applications provide their users with: * Ability to "pull" in data from external data sources * Dynamic filtering of data to be used in the analyses * Ability to generate reproducible code to regenerate the on-screen analyses +* Ability to create and download reports containing results of analyses (for analysis modules which support reporting) In addition, the `teal` framework also provides application developers with: @@ -100,6 +101,13 @@ We recommend creating applications using pre-defined `teal` modules. See the ref The `init` function has an additional argument `filters` which allows you to initialize the application with certain filters pre-selected. See the documentation for `init` for further details. +### Reporting + +If any of the `modules` in your `teal` application support reporting +(see [`teal.reporter`](https://insightsengineering.github.io/teal.reporter/) for more details) then users of your application +can add these outputs to a report. This report can be downloaded and a special "Report Previewer" module will be added to your application as an additional tab +so your users can view and configure their reports before downloading them. + ## Where to go next To learn more about the `teal` framework we recommend first exploring some of the available analysis modules.