diff --git a/.Rbuildignore b/.Rbuildignore index ce47f17cd6..195e2ccd1f 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -42,3 +42,4 @@ SECURITY.md ^templates$ ^test_full_example$ coverage.* +^pkgdown$ diff --git a/.github/ISSUE_TEMPLATE/cran-release.yaml b/.github/ISSUE_TEMPLATE/cran-release.yaml new file mode 100644 index 0000000000..bab34fcff5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/cran-release.yaml @@ -0,0 +1,124 @@ +--- +name: 🎉 CRAN Release +description: Template for release to CRAN +title: "[Release]: " +labels: ["release"] +assignees: + - KlaudiaBB + - cicdguy + - shajoezhu +body: + - type: markdown + attributes: + value: | + ⚠️ Please do not link or mention any internal references in this issue. This includes internal URLs, intellectual property and references. + - type: textarea + id: blocked-by + attributes: + label: Blocked by + description: Any PRs or issues that this release is blocked by. + placeholder: Add a list of blocking PRs or issues here. + value: | + ### PRs + + - [ ] PR 1 + + ### Issues + + - [ ] Issue 1 + validations: + required: true + - type: textarea + id: pre-release + attributes: + label: Pre-release + description: Pre-requisites that must be fulfilled before initiating the release process. + placeholder: Add your list of pre-requisites here. + value: | + - [ ] Make sure you adhere to CRAN submission policy: https://cran.r-project.org/web/packages/submission_checklist.html; https://cran.r-project.org/web/packages/policies.html. + - [ ] Make sure that high priority bugs (label "priority" + "bug") have been resolved before going into the release. + - [ ] Review old/hanging PRs before going into the release (Optional). + - [ ] Revisit R-package's lifecycle badges (Optional). + - [ ] Make sure that all upstream dependencies of this package that need to be submitted to CRAN were accepted before going into release activities. + - [ ] Make sure integration tests are green 2-3 days before the release. Look carefully through logs (check for warnings and notes). + - [ ] Decide what gets merged in before starting release activities. + - type: textarea + id: release + attributes: + label: Release + description: The steps to be taken in order to create a release. + placeholder: Steps to create a release. + value: | + ### Prepare the release + + - [ ] Create a new release candidate branch + `git checkout -b release-candidate-vX.Y.Z` + - [ ] Update NEWS.md file: make sure it reflects a holistic summary of what has changed in the package. + - [ ] Remove the additional fields (`Remotes`) from the DESCRIPTION file where applicable. + - [ ] Make sure that the minimum dependency versions are updated in the DESCRIPTION file for the package and its reverse dependencies (Optional). + - [ ] Increase versioned dependency on {package name} to >=X.Y.Z (Optional). + - [ ] Commit your changes and create the PR on GitHub (add "[skip vbump]" in the PR title). Add all updates, commit, and push changes: + `# Make the necessary modifications to your files + # Stage the changes + git add + # Commit the changes + git commit -m "[skip vbump] " + git push origin release-candidate-vX.Y.Z` + + ### Test the release + + - [ ] Execute the manual tests on Shiny apps that are deployed on various hosting providers (Posit connect and shinyapps.io) - track the results in GitHub issue (Applicable only for frameworks that use Shiny). + - [ ] Monitor integration tests, if integration fails, create priority issues on the board. + - [ ] Execute UAT tests (Optional). + + ### CRAN submission + + - [ ] Tag the update(s) as a release candidate vX.Y.Z-rc (e.g. v0.5.3-rc1) on the release candidate branch (release-candidate-vX.Y.Z). + `# Create rc tag for submission for internal validation + git tag vX.Y.Z-rc + git push origin vX.Y.Z-rc` + - [ ] Build the package locally using the command:`R CMD build .` which will generate a .tar.gz file necessary for the CRAN submission. + - [ ] Submit the package to https://win-builder.r-project.org/upload.aspx for testing, for more details please see "Building and checking R source packages for Windows": https://win-builder.r-project.org/. + - [ ] Once tested, send the package that was built in the previous steps to CRAN via this form: https://cran.r-project.org/submit.html. + - [ ] Address CRAN feedback, tag the package vX.Y.Z-rc(n+1) and repeat the submission to CRAN whenever necessary. + - [ ] Get the package accepted and published on CRAN. + + ### Tag the release + + - [ ] If the additional fields were removed, add them back in a separate PR, and then merge the PR back to main (add "[skip vbump]" in the PR title). If nothing was removed just merge the PR you created in the "Prepare the release" section to 'main'. Note the commit hash of the merged commit. **Note:** additional commits might be added to the `main` branch by a bot or an automation - we do **NOT** want to tag this commit. + + ### Make sure of the following before continuing + + - [ ] CI checks are passing in GH before releasing the package. + - [ ] Shiny apps are deployable and there are no errors/warnings (Applicable only for frameworks that use Shiny). + + - [ ] Create a git tag with the final version set to vX.Y.Z on the main branch. In order to do this: + 1. Checkout the commit hash. + `git checkout ` + 2. Tag the hash with the release version (vX.Y.Z). + `git tag vX.Y.Z` + 3. Push the tag to make the final release. + `git push origin vX.Y.Z` + - [ ] Update downstream package dependencies to (>=X.Y.Z) in {package name}. + Note: Once the release tag is created, the package is automatically published to internal repositories. + - type: textarea + id: post-release + attributes: + label: Post-release + description: The list of activities to be completed after the release. + placeholder: The steps that must be taken after the release. + value: | + - [ ] Ensure that CRAN checks are passing for the package. + - [ ] Make sure that the package is published to internal repositories. + - [ ] Make sure internal documentation is up to date. + - [ ] Review and update installation instructions for the package wherever needed (Optional). + - [ ] Update all integration tests to reference the new release. + - [ ] Announce the release on ________. + - type: textarea + id: decision-tree + attributes: + label: Decision tree + description: Any decision tree(s) that would aid release management + placeholder: Any decision tree(s) that would aid release management. + value: | + Click [here](https://github.com/insightsengineering/.github/blob/main/.github/ISSUE_TEMPLATE/RELEASE_DECISION_TREE.md) to see the release decision tree. diff --git a/.github/ISSUE_TEMPLATE/release.yaml b/.github/ISSUE_TEMPLATE/release.yaml index 1d97dff5a3..73bb11dc88 100644 --- a/.github/ISSUE_TEMPLATE/release.yaml +++ b/.github/ISSUE_TEMPLATE/release.yaml @@ -28,72 +28,87 @@ body: validations: required: true - type: textarea - id: pre-requisites + id: pre-release attributes: - label: Pre-requisites + label: Pre-release description: Pre-requisites that must be fulfilled before initiating the release process. placeholder: Add your list of pre-requisites here. value: | - [ ] Make sure that high priority bugs (label "priority" + "bug") have been resolved before going into the release. - [ ] Review old/hanging PRs before going into the release. - [ ] Revisit R-package's lifecycle badges (Optional). - - [ ] Discuss package dependencies before going into release activities. - - [ ] Create a plan to sequentially close release activities and submit groups of packages for internal validation (Applicable only for regulatory release). + - [ ] Release Manager: Discuss package dependencies, create a plan to sequentially close release activities and submit groups of packages for internal validation (Applicable only for regulatory release). + - [ ] Check Validation Pipeline dry-run results for the package. - [ ] Make sure all relevant integration tests are green 2-3 days before the release. Look carefully through logs (check for warnings and notes). - - [ ] Check if a package is installable on our supported internal systems (Optional). - [ ] Inform about the soft code freeze, decide what gets merged in before starting release activities. - type: textarea - id: release-checklist + id: release attributes: - label: Release Checklist + label: Release description: The steps to be taken in order to create a release. placeholder: Steps to create a release. value: | - - [ ] Recurring tasks: Execute the manual tests on Shiny apps that are deployed on various hosting providers (Posit connect and shinyapps.io) - track the results in GitHub issue (Applicable only for frameworks that use Shiny). - - [ ] Recurring tasks: Monitor integration tests, if integration fails, create priority issues on the board. - - [ ] Sanity checks for Shiny applications e.g. checking if Shiny apps are deployable and making sure there are no errors/warnings. + ### Prepare the release + + - [ ] Create a new release candidate branch + `git checkout -b release-candidate-vX.Y.Z` - [ ] Update NEWS.md file: make sure it reflects a holistic summary of what has changed in the package, check README. - - [ ] Remove the additional fields (`Remotes` and `Config/Needs/*`) from the DESCRIPTION file where applicable. + - [ ] Remove the additional fields (`Remotes`) from the DESCRIPTION file where applicable. - [ ] Make sure that the minimum dependency versions are updated in the DESCRIPTION file for the package. - - [ ] Increase versioned dependency on {package name} to >=X.X.X. - - [ ] Create a pull request to make necessary bug fixes/changes (add "[skip vbump]" in the PR title), and after merging the PR, tag the update(s) as a release candidate v < intended release version > -rc < release candidate iteration > on the main branch. Note that tags are created in GitHub and synchronized with GitLab automatically. - - [ ] The package is submitted for internal validation by Release Coordinator (Applicable only for regulatory release). - - [ ] Address any feedback (internal validation/user testing), retag the package as a release candidate vX.X.X-rc(n+1). Repeat the submission for internal validation if necessary. - - [ ] Get the package validated (Applicable only for regulatory release). - - [ ] If the additional fields were removed, add them back in a separate PR, and then merge the PR back to main (add "[skip vbump]" in the PR title). - - [ ] Create a git tag with the final version set to X.X.X on the main branch. - - [ ] Update downstream package dependencies to (>=X.X.X) in {package name}. - - type: textarea - id: testing - attributes: - label: Testing - description: Summary of testing activities - integration tests, UAT, other. - placeholder: Tests results - value: | - - [ ] Integration tests results - accepted. - - [ ] UAT results - accepted. - - [ ] Shiny apps test results - accepted (Applicable only for Shiny apps). - - [ ] Necessary testing on target environment - performed (up to ETL). - - type: textarea - id: feedback - attributes: - label: Release Feedback - description: Feedback received from internal validation/UAT testers. - placeholder: Feedback to be implemented after submission for internal validation/testing. - value: | - - [ ] Fix 1 - - [ ] Enhancement 1 - - [ ] Defect 1 + - [ ] Increase versioned dependency on {package name} to >=X.Y.Z. + - [ ] Commit your changes and create the PR on GitHub (add "[skip vbump]" in the PR title). Add all updates, commit, and push changes: + `# Make the necessary modifications to your files + # Stage the changes + git add + # Commit the changes + git commit -m "[skip vbump] " + git push origin release-candidate-vX.Y.Z` + + ### Test the release + + - [ ] Execute the manual tests on Shiny apps that are deployed on various hosting providers (Posit connect and shinyapps.io) - track the results in GitHub issue (Applicable only for frameworks that use Shiny). + - [ ] Monitor integration tests, if integration fails, create priority issues on the board. + - [ ] Execute UAT tests (Optional). + + ### Validation loop + + Note: This section is applicable only for regulatory packages. + + - [ ] Tag the update(s) as a release candidate vX.Y.Z-rc (e.g. v0.5.3-rc1) on the release candidate branch (release-candidate-vX.Y.Z). + `# Create rc tag for submission for internal validation + git tag vX.Y.Z-rc + git push origin vX.Y.Z-rc` + - [ ] Submit the package for internal validation. + - [ ] Address any feedback (internal validation/user testing), retag the package as a release candidate vX.Y.Z-rc(n+1). Repeat the submission for internal validation if necessary. + - [ ] Get the package validated. + + ### Tag the release + + - [ ] If the additional fields were removed, add them back in a separate PR, and then merge the PR back to main (add "[skip vbump]" in the PR title). If nothing was removed just merge the PR you created in the "Prepare the release" section to `main`. Note the commit hash of the merged commit. **Note:** additional commits might be added to the `main` branch by a bot or an automation - we do **NOT** want to tag this commit. + + #### Make sure of the following before continuing with the release: + + - [ ] CI checks are passing in GH. + - [ ] Shiny apps are deployable and there are no errors/warnings (Applicable only for frameworks that use Shiny). + + - [ ] Create a git tag with the final version set to vX.Y.Z on the main branch. In order to do this: + 1. Checkout the commit hash. + `git checkout ` + 2. Tag the hash with the release version (vX.Y.Z). + `git tag vX.Y.Z` + 3. Push the tag to make the final release. + `git push origin vX.Y.Z` + - [ ] Update downstream package dependencies to (>=X.Y.Z) in {package name}. + Note: Once the release tag is created, the package is automatically published to internal repositories. - type: textarea id: post-release attributes: - label: Post-release Checklist + label: Post-release description: The list of activities to be completed after the release. placeholder: The steps that must be taken after the release. value: | - [ ] Make sure that the package is published to internal repositories (Validated and/or Non-Validated repository). - [ ] Review and update installation instructions for the package if needed. - - [ ] Verify if a new dev version (.9XXX) has been added to the NEWS.md file and DESCRIPTION file as a placeholder for release notes by automation. - [ ] Make sure internal documentation/documentation catalogs are up to date. - [ ] Notify the IDR team to start post-release/clean-up activities. - [ ] Announce the release on ________. diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index e401c44ae5..0b9fc4d244 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -37,6 +37,7 @@ jobs: checking S3 generic/method consistency .* NOTE checking Rd .usage sections .* NOTE checking for unstated dependencies in vignettes .* NOTE + checking top-level files .* NOTE unit-test-report-brand: >- https://raw.githubusercontent.com/insightsengineering/hex-stickers/main/thumbs/teal.png coverage: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index a742d6ab7f..0fa4a06c8f 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -52,6 +52,7 @@ jobs: checking R code for possible problems .* NOTE checking examples .* NOTE checking Rd line widths .* NOTE + checking top-level files .* NOTE unit-test-report-brand: >- https://raw.githubusercontent.com/insightsengineering/hex-stickers/main/thumbs/teal.png coverage: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0fb140fda1..48f8f6e02a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,7 @@ default_language_version: python: python3 repos: - repo: https://github.com/lorenzwalthert/precommit - rev: v0.3.2.9027 + rev: v0.4.0 hooks: - id: style-files name: Style code with `styler` @@ -28,7 +28,6 @@ repos: - insightsengineering/teal.logger - insightsengineering/teal.reporter - insightsengineering/teal.slice - - insightsengineering/teal.transform - insightsengineering/teal.widgets - utils - id: spell-check @@ -48,9 +47,9 @@ repos: .*\.sh| .*\.svg| .*\.xml| - (.*/|)\_pkgdown.y[a]ml| + (.*/|)\_pkgdown.y[a]?ml| (.*/|)\.gitignore| - (.*/|)\.gitlab-ci\.yml| + (.*/|)\.gitlab-ci\.y[a]?ml| (.*/|)\.lintr| (.*/|)\.pre-commit-.*| (.*/|)\.Rbuildignore| @@ -60,9 +59,9 @@ repos: (.*/|)DESCRIPTION| (.*/|)LICENSE| (.*/|)NAMESPACE| - (.*/|)staged_dependencies\.yaml| + (.*/|)staged_dependencies\.y[a]?ml| (.*/|)WORDLIST| - \.github/.*\.yaml| + \.github/.*\.y[a]?ml| data/.* )$ - id: no-browser-statement diff --git a/DESCRIPTION b/DESCRIPTION index f2a20199eb..6073618682 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,8 +1,8 @@ Type: Package Package: teal Title: Exploratory Web Apps for Analyzing Clinical Trials Data -Version: 0.14.0.9027 -Date: 2023-12-08 +Version: 0.14.0.9040 +Date: 2024-01-23 Authors@R: c( person("Dawid", "Kaledkowski", , "dawid.kaledkowski@roche.com", role = c("aut", "cre")), person("Pawel", "Rucki", , "pawel.rucki@roche.com", role = "aut"), @@ -32,8 +32,7 @@ Depends: R (>= 4.0), shiny (>= 1.7.0), teal.data (>= 0.3.0.9017), - teal.slice (>= 0.4.0.9027), - teal.transform (>= 0.4.0.9010) + teal.slice (>= 0.4.0.9027) Imports: checkmate (>= 2.1.0), jsonlite, @@ -70,20 +69,20 @@ VignetteBuilder: RdMacros: lifecycle Config/Needs/verdepcheck: rstudio/shiny, insightsengineering/teal.data, - insightsengineering/teal.slice, insightsengineering/teal.transform, - mllg/checkmate, jeroen/jsonlite, r-lib/lifecycle, daroczig/logger, - tidyverse/magrittr, r-lib/rlang, daattali/shinyjs, - insightsengineering/teal.logger, insightsengineering/teal.reporter, - insightsengineering/teal.widgets, rstudio/bslib, yihui/knitr, - bioc::MultiAssayExperiment, r-lib/R6, rstudio/rmarkdown, - rstudio/shinyvalidate, insightsengineering/teal.code, r-lib/testthat, - r-lib/withr, yaml=vubiostat/r-yaml + insightsengineering/teal.slice, mllg/checkmate, jeroen/jsonlite, + r-lib/lifecycle, daroczig/logger, tidyverse/magrittr, r-lib/rlang, + daattali/shinyjs, insightsengineering/teal.logger, + insightsengineering/teal.reporter, insightsengineering/teal.widgets, + rstudio/bslib, yihui/knitr, bioc::MultiAssayExperiment, r-lib/R6, + rstudio/rmarkdown, rstudio/shinyvalidate, + insightsengineering/teal.code, r-lib/testthat, r-lib/withr, + yaml=vubiostat/r-yaml Config/Needs/website: insightsengineering/nesttemplate Encoding: UTF-8 Language: en-US LazyData: true Roxygen: list(markdown = TRUE) -RoxygenNote: 7.2.3 +RoxygenNote: 7.3.0 Collate: 'dummy_functions.R' 'get_rcode_utils.R' @@ -102,8 +101,9 @@ Collate: 'show_rcode_modal.R' 'tdata.R' 'teal.R' - 'teal_data_module-eval_code.R' 'teal_data_module.R' + 'teal_data_module-eval_code.R' + 'teal_data_module-within.R' 'teal_reporter.R' 'teal_slices-store.R' 'teal_slices.R' diff --git a/NAMESPACE b/NAMESPACE index 341b7e34e1..8c78f7e555 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -19,6 +19,7 @@ export("%>%") export(TealReportCard) export(as.teal_slices) export(as_tdata) +export(build_app_title) export(example_module) export(get_code_tdata) export(get_metadata) @@ -43,11 +44,9 @@ export(validate_inputs) export(validate_n_levels) export(validate_no_intersection) export(validate_one_row_per_id) -exportMethods(eval_code) import(shiny) import(teal.data) import(teal.slice) -import(teal.transform) importFrom(lifecycle,badge) importFrom(magrittr,"%>%") importFrom(methods,setMethod) diff --git a/NEWS.md b/NEWS.md index 37a56981c7..faf1a86571 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,4 +1,4 @@ -# teal 0.14.0.9027 +# teal 0.14.0.9040 ### New features @@ -6,6 +6,7 @@ * Added `landing_popup_module` function which creates a module that will display a popup when the app starts. The popup will block access to the app until it is dismissed. * Filter state snapshots can now be uploaded from file. See `?snapshot`. * Added `as_tdata` function to facilitate migration of modules to the new `teal_data` class. +* Added `build_app_title` function to facilitate adding favicons to app title. ### Breaking changes diff --git a/R/get_rcode_utils.R b/R/get_rcode_utils.R index 5954a78c40..a7a862d8f8 100644 --- a/R/get_rcode_utils.R +++ b/R/get_rcode_utils.R @@ -46,7 +46,8 @@ get_rcode_str_install <- function() { #' @keywords internal get_datasets_code <- function(datanames, datasets, hashes) { # preprocessing code - str_prepro <- teal.data:::get_code_dependency(attr(datasets, "preprocessing_code"), names = datanames) + str_prepro <- + teal.data:::get_code_dependency(attr(datasets, "preprocessing_code"), names = datanames, check_names = FALSE) if (length(str_prepro) == 0) { str_prepro <- "message('Preprocessing is empty')" } else { diff --git a/R/init.R b/R/init.R index bf7b82dc8a..2bbe954795 100644 --- a/R/init.R +++ b/R/init.R @@ -10,27 +10,31 @@ #' End-users: This is the most important function for you to start a #' teal app that is composed out of teal modules. #' +#' @details +#' When initializing the `teal` app, if `datanames` are not set for the `teal_data` object, +#' defaults from the `teal_data` environment will be used. +#' #' @param data (`teal_data`, `teal_data_module`, `named list`)\cr #' `teal_data` object as returned by [teal.data::teal_data()] or -#' `teal_data_modules` or simply a list of a named list of objects +#' `teal_data_module` or simply a list of a named list of objects #' (`data.frame` or `MultiAssayExperiment`). #' @param modules (`list`, `teal_modules` or `teal_module`)\cr #' nested list of `teal_modules` or `teal_module` objects or a single #' `teal_modules` or `teal_module` object. These are the specific output modules which #' will be displayed in the teal application. See [modules()] and [module()] for #' more details. -#' @param title (`NULL` or `character`)\cr -#' The browser window title (defaults to the host URL of the page). +#' @param title (`shiny.tag` or `character(1)`)\cr +#' The browser window title. Defaults to a title "teal app" with the icon of NEST. +#' Can be created using the `build_app_title()` or +#' by passing a valid `shiny.tag` which is a head tag with title and link tag. #' @param filter (`teal_slices`)\cr #' Specification of initial filter. Filters can be specified using [teal::teal_slices()]. #' Old way of specifying filters through a list is deprecated and will be removed in the #' next release. Please fix your applications to use [teal::teal_slices()]. -#' @param header (`shiny.tag` or `character`) \cr -#' the header of the app. Note shiny code placed here (and in the footer -#' argument) will be placed in the app's `ui` function so code which needs to be placed in the `ui` function -#' (such as loading `CSS` via [htmltools::htmlDependency()]) should be included here. -#' @param footer (`shiny.tag` or `character`)\cr -#' the footer of the app +#' @param header (`shiny.tag` or `character(1)`) \cr +#' The header of the app. +#' @param footer (`shiny.tag` or `character(1)`)\cr +#' The footer of the app. #' @param id (`character`)\cr #' module id to embed it, if provided, #' the server function must be called with [shiny::moduleServer()]; @@ -96,16 +100,15 @@ #' init <- function(data, modules, - title = NULL, + title = build_app_title(), filter = teal_slices(), header = tags$p(), footer = tags$p(), id = character(0)) { - logger::log_trace("init initializing teal app with: data ({ class(data)[1] }).") - if (is.list(data) && !inherits(data, "teal_data_module")) { - checkmate::assert_list(data, names = "named") - data <- do.call(teal.data::teal_data, data) - } + logger::log_trace("init initializing teal app with: data ('{ class(data) }').") + + # argument checking (independent) + ## `data` if (inherits(data, "TealData")) { lifecycle::deprecate_stop( when = "0.99.0", @@ -116,46 +119,75 @@ init <- function(data, ) ) } - - checkmate::assert_multi_class(data, c("teal_data", "teal_data_module")) - checkmate::assert_multi_class(modules, c("teal_module", "list", "teal_modules")) - checkmate::assert_string(title, null.ok = TRUE) checkmate::assert( - checkmate::check_class(filter, "teal_slices"), - checkmate::check_list(filter, names = "named") + .var.name = "data", + checkmate::check_multi_class(data, c("teal_data", "teal_data_module")), + checkmate::check_list(data, names = "named") ) - checkmate::assert_multi_class(header, c("shiny.tag", "character")) - checkmate::assert_multi_class(footer, c("shiny.tag", "character")) - checkmate::assert_character(id, max.len = 1, any.missing = FALSE) - - teal.logger::log_system_info() + if (is.list(data) && !inherits(data, "teal_data_module")) { + data <- do.call(teal.data::teal_data, data) + } + ## `modules` + checkmate::assert( + .var.name = "modules", + checkmate::check_multi_class(modules, c("teal_modules", "teal_module")), + checkmate::check_list(modules, min.len = 1, any.missing = FALSE, types = c("teal_module", "teal_modules")) + ) if (inherits(modules, "teal_module")) { modules <- list(modules) } - if (inherits(modules, "list")) { + if (checkmate::test_list(modules, min.len = 1, any.missing = FALSE, types = c("teal_module", "teal_modules"))) { modules <- do.call(teal::modules, modules) } + ## `filter` + checkmate::assert( + .var.name = "filter", + checkmate::check_class(filter, "teal_slices"), + checkmate::check_list(filter, names = "named") + ) + + ## all other arguments + checkmate::assert( + .var.name = "title", + checkmate::check_string(title), + checkmate::check_multi_class(title, c("shiny.tag", "shiny.tag.list", "html")) + ) + checkmate::assert( + .var.name = "header", + checkmate::check_string(header), + checkmate::check_multi_class(header, c("shiny.tag", "shiny.tag.list", "html")) + ) + checkmate::assert( + .var.name = "footer", + checkmate::check_string(footer), + checkmate::check_multi_class(footer, c("shiny.tag", "shiny.tag.list", "html")) + ) + checkmate::assert_character(id, max.len = 1, any.missing = FALSE) + + # log + teal.logger::log_system_info() + + # argument transformations + ## `modules` - landing module landing <- extract_module(modules, "teal_module_landing") - if (length(landing) > 1L) stop("Only one `landing_popup_module` can be used.") - modules <- drop_module(modules, "teal_module_landing") - - # Calculate app id that will be used to stamp filter state snapshots. - # App id is a hash of the app's data and modules. - # See "transferring snapshots" section in ?snapshot. - hashables <- mget(c("data", "modules")) - hashables$data <- if (inherits(hashables$data, "teal_data")) { - as.list(hashables$data@env) - } else if (inherits(data, "teal_data_module")) { - body(data$server) + landing_module <- NULL + if (length(landing) == 1L) { + landing_module <- landing[[1L]] + modules <- drop_module(modules, "teal_module_landing") + } else if (length(landing) > 1L) { + stop("Only one `landing_popup_module` can be used.") } - attr(filter, "app_id") <- rlang::hash(hashables) + ## `filter` - app_id attribute + attr(filter, "app_id") <- create_app_id(data, modules) - # convert teal.slice::teal_slices to teal::teal_slices + ## `filter` - convert teal.slice::teal_slices to teal::teal_slices filter <- as.teal_slices(as.list(filter)) + # argument checking (interdependent) + ## `filter` - `modules` if (isTRUE(attr(filter, "module_specific"))) { module_names <- unlist(c(module_labels(modules), "global_filters")) failed_mod_names <- setdiff(names(attr(filter, "mapping")), module_names) @@ -182,22 +214,21 @@ init <- function(data, } } + ## `data` - `modules` if (inherits(data, "teal_data")) { - if (length(teal.data::datanames(data)) == 0) { - stop("`data` object has no datanames. Specify `datanames(data)` and try again.") + if (length(teal_data_datanames(data)) == 0) { + stop("`data` object has no datanames and its environment is empty. Specify `datanames(data)` and try again.") } - # in case of teal_data_module this check is postponed to the srv_teal_with_splash - is_modules_ok <- check_modules_datanames(modules, teal.data::datanames(data)) + is_modules_ok <- check_modules_datanames(modules, teal_data_datanames(data)) if (!isTRUE(is_modules_ok)) { logger::log_error(is_modules_ok) checkmate::assert(is_modules_ok, .var.name = "modules") } - - is_filter_ok <- check_filter_datanames(filter, teal.data::datanames(data)) + is_filter_ok <- check_filter_datanames(filter, teal_data_datanames(data)) if (!isTRUE(is_filter_ok)) { - logger::log_warn(is_filter_ok) + warning(is_filter_ok) # we allow app to continue if applied filters are outside # of possible data range } @@ -210,14 +241,14 @@ init <- function(data, res <- list( ui = ui_teal_with_splash(id = id, data = data, title = title, header = header, footer = footer), server = function(input, output, session) { - if (length(landing) == 1L) { - landing_module <- landing[[1L]] + if (!is.null(landing_module)) { do.call(landing_module$server, c(list(id = "landing_module_shiny_id"), landing_module$server_args)) } - filter <- deep_copy_filter(filter) - srv_teal_with_splash(id = id, data = data, modules = modules, filter = filter) + srv_teal_with_splash(id = id, data = data, modules = modules, filter = deep_copy_filter(filter)) } ) + logger::log_trace("init teal app has been initialized.") + return(res) } diff --git a/R/module_nested_tabs.R b/R/module_nested_tabs.R index 7a0e4a9c93..73afb01be8 100644 --- a/R/module_nested_tabs.R +++ b/R/module_nested_tabs.R @@ -112,8 +112,7 @@ ui_nested_tabs.teal_module <- function(id, modules, datasets, depth = 0L, is_mod checkmate::assert_class(datasets, classes = "FilteredData") ns <- NS(id) - args <- isolate(teal.transform::resolve_delayed(modules$ui_args, datasets)) - args <- c(list(id = ns("module")), args) + args <- c(list(id = ns("module")), modules$ui_args) teal_ui <- tags$div( id = id, @@ -202,7 +201,6 @@ srv_nested_tabs.teal_module <- function(id, datasets, modules, is_module_specifi logger::log_trace("srv_nested_tabs.teal_module initializing the module: { deparse1(modules$label) }.") moduleServer(id = id, module = function(input, output, session) { - modules$server_args <- teal.transform::resolve_delayed(modules$server_args, datasets) if (!is.null(modules$datanames) && is_module_specific) { datasets$srv_filter_panel("module_filter_panel") } diff --git a/R/module_teal.R b/R/module_teal.R index cc4c08dcfe..cdecfaf639 100644 --- a/R/module_teal.R +++ b/R/module_teal.R @@ -61,32 +61,39 @@ NULL #' @rdname module_teal ui_teal <- function(id, splash_ui = tags$h2("Starting the Teal App"), - title = NULL, - header = tags$p(""), - footer = tags$p("")) { - if (checkmate::test_string(header)) { - header <- tags$h1(header) - } - if (checkmate::test_string(footer)) { - footer <- tags$p(footer) + title = build_app_title(), + header = tags$p(), + footer = tags$p()) { + checkmate::assert_character(id, max.len = 1, any.missing = FALSE) + + checkmate::assert_multi_class(splash_ui, c("shiny.tag", "shiny.tag.list", "html")) + + if (is.character(title)) { + title <- build_app_title(title) + } else { + validate_app_title_tag(title) } + checkmate::assert( - checkmate::check_class(splash_ui, "shiny.tag"), - checkmate::check_class(splash_ui, "shiny.tag.list"), - checkmate::check_class(splash_ui, "html") - ) - checkmate::assert( - checkmate::check_class(header, "shiny.tag"), - checkmate::check_class(header, "shiny.tag.list"), - checkmate::check_class(header, "html") + .var.name = "header", + checkmate::check_string(header), + checkmate::check_multi_class(header, c("shiny.tag", "shiny.tag.list", "html")) ) + if (checkmate::test_string(header)) { + header <- tags$p(header) + } + checkmate::assert( - checkmate::check_class(footer, "shiny.tag"), - checkmate::check_class(footer, "shiny.tag.list"), - checkmate::check_class(footer, "html") + .var.name = "footer", + checkmate::check_string(footer), + checkmate::check_multi_class(footer, c("shiny.tag", "shiny.tag.list", "html")) ) + if (checkmate::test_string(footer)) { + footer <- tags$p(footer) + } ns <- NS(id) + # Once the data is loaded, we will remove this element and add the real teal UI instead splash_ui <- div( # id so we can remove the splash screen once ready, which is the first child of this container @@ -146,7 +153,8 @@ srv_teal <- function(id, modules, teal_data_rv, filter = teal_slices()) { ) # `JavaScript` code - run_js_files(files = "init.js") # `JavaScript` code to make the clipboard accessible + run_js_files(files = "init.js") + # set timezone in shiny app # timezone is set in the early beginning so it will be available also # for `DDL` and all shiny modules @@ -189,8 +197,8 @@ srv_teal <- function(id, modules, teal_data_rv, filter = teal_slices()) { # null controls a display of filter panel but data should be still passed datanames <- if (is.null(modules$datanames) || modules$datanames == "all") { include_parent_datanames( - teal.data::datanames(teal_data_rv()), - teal_data_rv()@join_keys + teal_data_datanames(teal_data_rv()), + teal.data::join_keys(teal_data_rv()) ) } else { modules$datanames diff --git a/R/module_teal_with_splash.R b/R/module_teal_with_splash.R index a15e69d507..252aa8406e 100644 --- a/R/module_teal_with_splash.R +++ b/R/module_teal_with_splash.R @@ -19,10 +19,27 @@ #' @export ui_teal_with_splash <- function(id, data, - title, - header = tags$p("Add Title Here"), - footer = tags$p("Add Footer Here")) { + title = build_app_title(), + header = tags$p(), + footer = tags$p()) { + checkmate::assert_character(id, max.len = 1, any.missing = FALSE) checkmate::assert_multi_class(data, c("teal_data", "teal_data_module")) + checkmate::assert( + .var.name = "title", + checkmate::check_string(title), + checkmate::check_multi_class(title, c("shiny.tag", "shiny.tag.list", "html")) + ) + checkmate::assert( + .var.name = "header", + checkmate::check_string(header), + checkmate::check_multi_class(header, c("shiny.tag", "shiny.tag.list", "html")) + ) + checkmate::assert( + .var.name = "footer", + checkmate::check_string(footer), + checkmate::check_multi_class(footer, c("shiny.tag", "shiny.tag.list", "html")) + ) + ns <- NS(id) # Startup splash screen for delayed loading @@ -58,7 +75,10 @@ ui_teal_with_splash <- function(id, #' If data is not loaded yet, `reactive` returns `NULL`. #' @export srv_teal_with_splash <- function(id, data, modules, filter = teal_slices()) { + checkmate::assert_character(id, max.len = 1, any.missing = FALSE) checkmate::check_multi_class(data, c("teal_data", "teal_data_module")) + checkmate::assert_class(modules, "teal_modules") + checkmate::assert_class(filter, "teal_slices") moduleServer(id, function(input, output, session) { logger::log_trace("srv_teal_with_splash initializing module with data.") @@ -72,7 +92,7 @@ srv_teal_with_splash <- function(id, data, modules, filter = teal_slices()) { teal_data_rv <- if (inherits(data, "teal_data_module")) { data <- data$server(id = "teal_data_module") if (!is.reactive(data)) { - stop("The `teal_data_module` must return a reactive expression.", call. = FALSE) + stop("The `teal_data_module` passed to `data` must return a reactive expression.", call. = FALSE) } data } else if (inherits(data, "teal_data")) { @@ -94,7 +114,7 @@ srv_teal_with_splash <- function(id, data, modules, filter = teal_slices()) { need( FALSE, paste( - "Error when executing `teal_data_module`:\n ", + "Error when executing `teal_data_module` passed to `data`:\n ", paste(data$message, collapse = "\n"), "\n Check your inputs or contact app developer if error persists." ) @@ -108,7 +128,7 @@ srv_teal_with_splash <- function(id, data, modules, filter = teal_slices()) { need( FALSE, paste( - "Error when executing `teal_data_module`:\n ", + "Error when executing `teal_data_module` passed to `data`:\n ", paste(data$message, collpase = "\n"), "\n Check your inputs or contact app developer if error persists." ) @@ -120,26 +140,31 @@ srv_teal_with_splash <- function(id, data, modules, filter = teal_slices()) { need( inherits(data, "teal_data"), paste( - "Error: `teal_data_module` did not return `teal_data` object", - "\n Check your inputs or contact app developer if error persists" + "Error: `teal_data_module` passed to `data` failed to return `teal_data` object, returned", + toString(sQuote(class(data))), + "instead.", + "\n Check your inputs or contact app developer if error persists." ) ) ) - validate(need(teal.data::datanames(data), "Data has no datanames. Contact app developer.")) - + if (!length(teal.data::datanames(data))) { + warning("`data` object has no datanames. Default datanames are set using `teal_data`'s environment.") + } - is_modules_ok <- check_modules_datanames(modules, teal.data::datanames(data)) - validate(need(isTRUE(is_modules_ok), is_modules_ok)) + is_modules_ok <- check_modules_datanames(modules, teal_data_datanames(data)) + if (!isTRUE(is_modules_ok)) { + validate(need(isTRUE(is_modules_ok), sprintf("%s. Contact app developer.", is_modules_ok))) + } - is_filter_ok <- check_filter_datanames(filter, teal.data::datanames(data)) + is_filter_ok <- check_filter_datanames(filter, teal_data_datanames(data)) if (!isTRUE(is_filter_ok)) { showNotification( "Some filters were not applied because of incompatibility with data. Contact app developer.", type = "warning", duration = 10 ) - logger::log_warn(is_filter_ok) + warning(is_filter_ok) } teal_data_rv() diff --git a/R/modules.R b/R/modules.R index f243bb6120..dc769758af 100644 --- a/R/modules.R +++ b/R/modules.R @@ -230,22 +230,9 @@ module <- function(label = "module", datanames = "all", server_args = NULL, ui_args = NULL) { + # argument checking (independent) + ## `label` checkmate::assert_string(label) - checkmate::assert_function(server) - checkmate::assert_function(ui) - checkmate::assert_character(datanames, min.len = 1, null.ok = TRUE, any.missing = FALSE) - checkmate::assert_list(server_args, null.ok = TRUE, names = "named") - checkmate::assert_list(ui_args, null.ok = TRUE, names = "named") - - if (!missing(filters)) { - checkmate::assert_character(filters, min.len = 1, null.ok = TRUE, any.missing = FALSE) - datanames <- filters - msg <- - "The `filters` argument is deprecated and will be removed in the next release. Please use `datanames` instead." - logger::log_warn(msg) - warning(msg) - } - if (label == "global_filters") { stop( sprintf("module(label = \"%s\", ...\n ", label), @@ -256,10 +243,13 @@ module <- function(label = "module", if (label == "Report previewer") { stop( sprintf("module(label = \"%s\", ...\n ", label), - "Label 'Report previewer' is reserved in teal.", + "Label 'Report previewer' is reserved in teal. Please change to something else.", call. = FALSE ) } + + ## `server` + checkmate::assert_function(server) server_formals <- names(formals(server)) if (!( "id" %in% server_formals || @@ -277,11 +267,6 @@ module <- function(label = "module", "\n - `...` server_args elements will be passed to the module named argument or to the `...`" ) } - - if (!is.element("data", server_formals) && !is.null(datanames)) { - message(sprintf("module \"%s\" server function takes no data so \"datanames\" will be ignored", label)) - datanames <- NULL - } if ("datasets" %in% server_formals) { warning( sprintf("Called from module(label = \"%s\", ...)\n ", label), @@ -291,15 +276,9 @@ module <- function(label = "module", ) } - srv_extra_args <- setdiff(names(server_args), server_formals) - if (length(srv_extra_args) > 0 && !"..." %in% server_formals) { - stop( - "\nFollowing `server_args` elements have no equivalent in the formals of the `server`:\n", - paste(paste(" -", srv_extra_args), collapse = "\n"), - "\n\nUpdate the `server` arguments by including above or add `...`" - ) - } + ## `ui` + checkmate::assert_function(ui) ui_formals <- names(formals(ui)) if (!"id" %in% ui_formals) { stop( @@ -309,7 +288,6 @@ module <- function(label = "module", "\n - `...` ui_args elements will be passed to the module argument of the same name or to the `...`" ) } - if (any(c("data", "datasets") %in% ui_formals)) { stop( sprintf("Called from module(label = \"%s\", ...)\n ", label), @@ -319,6 +297,37 @@ module <- function(label = "module", ) } + + ## `filters` + if (!missing(filters)) { + datanames <- filters + msg <- + "The `filters` argument is deprecated and will be removed in the next release. Please use `datanames` instead." + logger::log_warn(msg) + warning(msg) + } + + ## `datanames` (also including deprecated `filters`) + # please note a race condition between datanames set when filters is not missing and data arg in server function + if (!is.element("data", server_formals) && !is.null(datanames)) { + message(sprintf("module \"%s\" server function takes no data so \"datanames\" will be ignored", label)) + datanames <- NULL + } + checkmate::assert_character(datanames, min.len = 1, null.ok = TRUE, any.missing = FALSE) + + ## `server_args` + checkmate::assert_list(server_args, null.ok = TRUE, names = "named") + srv_extra_args <- setdiff(names(server_args), server_formals) + if (length(srv_extra_args) > 0 && !"..." %in% server_formals) { + stop( + "\nFollowing `server_args` elements have no equivalent in the formals of the `server`:\n", + paste(paste(" -", srv_extra_args), collapse = "\n"), + "\n\nUpdate the `server` arguments by including above or add `...`" + ) + } + + ## `ui_args` + checkmate::assert_list(ui_args, null.ok = TRUE, names = "named") ui_extra_args <- setdiff(names(ui_args), ui_formals) if (length(ui_extra_args) > 0 && !"..." %in% ui_formals) { stop( @@ -374,10 +383,7 @@ module <- function(label = "module", #' ) #' stopifnot(teal:::modules_depth(mods) == 2L) modules_depth <- function(modules, depth = 0L) { - checkmate::assert( - checkmate::check_class(modules, "teal_module"), - checkmate::check_class(modules, "teal_modules") - ) + checkmate::assert_multi_class(modules, c("teal_module", "teal_modules")) checkmate::assert_int(depth, lower = 0) if (inherits(modules, "teal_modules")) { max(vapply(modules$children, modules_depth, integer(1), depth = depth + 1L)) diff --git a/R/tdata.R b/R/tdata.R index 391df963c6..a996cc69e3 100644 --- a/R/tdata.R +++ b/R/tdata.R @@ -72,13 +72,6 @@ new_tdata <- function(data, code = "", join_keys = NULL, metadata = NULL) { for (x in names(data)) { if (!is.reactive(data[[x]])) { data[[x]] <- do.call(reactive, list(as.name(x)), envir = list2env(data[x])) - } else { - isolate( - checkmate::assert_multi_class( - data[[x]](), c("data.frame", "MultiAssayExperiment"), - .var.name = "data" - ) - ) } } @@ -191,12 +184,12 @@ as_tdata <- function(x) { } if (is.reactive(x)) { checkmate::assert_class(isolate(x()), "teal_data") - datanames <- isolate(teal.data::datanames(x())) + datanames <- isolate(teal_data_datanames(x())) datasets <- sapply(datanames, function(dataname) reactive(x()[[dataname]]), simplify = FALSE) code <- reactive(teal.code::get_code(x())) join_keys <- isolate(teal.data::join_keys(x())) } else if (inherits(x, "teal_data")) { - datanames <- teal.data::datanames(x) + datanames <- teal_data_datanames(x) datasets <- sapply(datanames, function(dataname) reactive(x[[dataname]]), simplify = FALSE) code <- reactive(teal.code::get_code(x)) join_keys <- isolate(teal.data::join_keys(x)) diff --git a/R/teal.R b/R/teal.R index 6ec6e6cee4..ddd5072512 100644 --- a/R/teal.R +++ b/R/teal.R @@ -14,7 +14,7 @@ magrittr::`%>%` # Fix R CMD check notes -#' @import shiny teal.data teal.slice teal.transform +#' @import shiny teal.data teal.slice #' @importFrom stats setNames NULL diff --git a/R/teal_data_module-eval_code.R b/R/teal_data_module-eval_code.R index 4be1655c82..4f80f49010 100644 --- a/R/teal_data_module-eval_code.R +++ b/R/teal_data_module-eval_code.R @@ -1,18 +1,16 @@ setOldClass("teal_data_module") -#' Evaluate the code in the qenv environment -#' @name eval_code -#' @description -#' Given code is evaluated in the `qenv` environment of `teal_data` reactive defined in `teal_data_module`. +#' Evaluate Code on `teal_data_module` +#' +#' @details +#' `eval_code` evaluates given code in the environment of the `teal_data` object created by the `teal_data_module`. +#' The code is added to the `@code` slot of the `teal_data`. +#' #' @param object (`teal_data_module`) #' @inheritParams teal.code::eval_code -#' @return Returns a `teal_data_module` object. -#' @importMethodsFrom teal.code eval_code -#' @importFrom methods setMethod -NULL - -#' @rdname eval_code -#' @export +#' +#' @return +#' `eval_code` returns a `teal_data_module` object with a delayed evaluation of `code` when the module is run. #' #' @examples #' tdm <- teal_data_module( @@ -24,6 +22,17 @@ NULL #' } #' ) #' eval_code(tdm, "IRIS <- subset(IRIS, Species == 'virginica')") +#' +#' @include teal_data_module.R +#' @name eval_code +#' @rdname teal_data_module +#' @aliases eval_code,teal_data_module,character-method +#' @aliases eval_code,teal_data_module,language-method +#' @aliases eval_code,teal_data_module,expression-method +#' +#' @importFrom methods setMethod +#' @importMethodsFrom teal.code eval_code +#' setMethod("eval_code", signature = c("teal_data_module", "character"), function(object, code) { teal_data_module( ui = function(id) { @@ -54,51 +63,10 @@ setMethod("eval_code", signature = c("teal_data_module", "character"), function( ) }) -#' @rdname eval_code -#' @export setMethod("eval_code", signature = c("teal_data_module", "language"), function(object, code) { eval_code(object, code = paste(lang2calls(code), collapse = "\n")) }) -#' @rdname eval_code -#' @export setMethod("eval_code", signature = c("teal_data_module", "expression"), function(object, code) { eval_code(object, code = paste(lang2calls(code), collapse = "\n")) }) - -#' @inherit teal.code::within.qenv params title details -#' @description -#' Convenience function for evaluating inline code inside the environment of a -#' `teal_data_module` -#' -#' @param data (`teal_data_module`) object -#' @return Returns a `teal_data_module` object with a delayed evaluation of `expr` -#' when module. -#' @export -#' @seealso [base::within()], [teal_data_module()] -#' @examples -#' tdm <- teal_data_module( -#' ui = function(id) div(id = shiny::NS(id)("div_id")), -#' server = function(id) { -#' shiny::moduleServer(id, function(input, output, session) { -#' shiny::reactive(teal_data(IRIS = iris)) -#' }) -#' } -#' ) -#' within(tdm, IRIS <- subset(IRIS, Species == "virginica")) -within.teal_data_module <- function(data, expr, ...) { - expr <- substitute(expr) - extras <- list(...) - - # Add braces for consistency. - if (!identical(as.list(expr)[[1L]], as.symbol("{"))) { - expr <- call("{", expr) - } - - calls <- as.list(expr)[-1] - - # Inject extra values into expressions. - calls <- lapply(calls, function(x) do.call(substitute, list(x, env = extras))) - - eval_code(object = data, code = as.expression(calls)) -} diff --git a/R/teal_data_module-within.R b/R/teal_data_module-within.R new file mode 100644 index 0000000000..e8ab3020b3 --- /dev/null +++ b/R/teal_data_module-within.R @@ -0,0 +1,45 @@ +#' Evaluate Expression on `teal_data_module` +#' +#' @details +#' `within` is a convenience function for evaluating inline code inside the environment of a `teal_data_module`. +#' +#' @param data (`teal_data_module`) object +#' @param expr (`expression`) to evaluate. Must be inline code. See +#' @param ... See `Details`. +#' +#' @return +#' `within` returns a `teal_data_module` object with a delayed evaluation of `expr` when the module is run. +#' +#' @examples +#' tdm <- teal_data_module( +#' ui = function(id) div(id = shiny::NS(id)("div_id")), +#' server = function(id) { +#' shiny::moduleServer(id, function(input, output, session) { +#' shiny::reactive(teal_data(IRIS = iris)) +#' }) +#' } +#' ) +#' within(tdm, IRIS <- subset(IRIS, Species == "virginica")) +#' +#' @include teal_data_module.R +#' @name within +#' @rdname teal_data_module +#' +#' @export +#' +within.teal_data_module <- function(data, expr, ...) { + expr <- substitute(expr) + extras <- list(...) + + # Add braces for consistency. + if (!identical(as.list(expr)[[1L]], as.symbol("{"))) { + expr <- call("{", expr) + } + + calls <- as.list(expr)[-1] + + # Inject extra values into expressions. + calls <- lapply(calls, function(x) do.call(substitute, list(x, env = extras))) + + eval_code(object = data, code = as.expression(calls)) +} diff --git a/R/teal_data_module.R b/R/teal_data_module.R index 75a5557e56..6b5a4eb5b1 100644 --- a/R/teal_data_module.R +++ b/R/teal_data_module.R @@ -1,15 +1,18 @@ -#' Data module for `teal` applications +#' Data Module for `teal` Applications #' -#' @description `r lifecycle::badge("experimental")` -#' Creates `teal_data_module` object - a `shiny` module to supply or modify data in a `teal` application. +#' @description +#' `r lifecycle::badge("experimental")` #' -#' This function creates a `shiny` module that allows for running data pre-processing code after the app starts. -#' The body of the server function will be run in the app rather than in the global environment. -#' This means it will be run every time the app starts, so use sparingly. +#' Create a `teal_data_module` object and evaluate code on it with history tracking. #' +#' @details +#' `teal_data_module` creates a `shiny` module to supply or modify data in a `teal` application. +#' The module allows for running data pre-processing code (creation _and_ some modification) after the app starts. +#' The body of the server function will be run in the app rather than in the global environment. +#' This means it will be run every time the app starts, so use sparingly.\cr #' Pass this module instead of a `teal_data` object in a call to [init()]. -#' -#' See vignette \code{vignette("data-as-shiny-module", package = "teal")} for more details. +#' Note that the server function must always return a `teal_data` object wrapped in a reactive expression.\cr +#' See vignette `vignette("data-as-shiny-module", package = "teal")` for more details. #' #' @param ui (`function(id)`)\cr #' `shiny` module `ui` function; must only take `id` argument @@ -17,7 +20,8 @@ #' `shiny` module `ui` function; must only take `id` argument; #' must return reactive expression containing `teal_data` object #' -#' @return Object of class `teal_data_module`. +#' @return +#' `teal_data_module` returns an object of class `teal_data_module`. #' #' @examples #' data <- teal_data_module( @@ -42,6 +46,12 @@ #' }) #' } #' ) +#' +#' @name teal_data_module +#' @rdname teal_data_module +#' +#' @seealso [`teal_data-class`], [`base::within()`], [`teal.code::within.qenv()`] +#' #' @export teal_data_module <- function(ui, server) { checkmate::assert_function(ui, args = "id", nargs = 1) diff --git a/R/teal_slices-store.R b/R/teal_slices-store.R index 8c33733103..a426ccf99c 100644 --- a/R/teal_slices-store.R +++ b/R/teal_slices-store.R @@ -64,17 +64,21 @@ slices_restore <- function(file) { lapply(tss_json$slices, function(slice) { for (field in c("selected", "choices")) { if (!is.null(slice[[field]])) { - date_partial_regex <- "^[0-9]{4}-[0-9]{2}-[0-9]{2}" - time_stamp_regex <- paste0(date_partial_regex, "\\s[0-9]{2}:[0-9]{2}:[0-9]{2}\\sUTC$") + if (length(slice[[field]]) > 0) { + date_partial_regex <- "^[0-9]{4}-[0-9]{2}-[0-9]{2}" + time_stamp_regex <- paste0(date_partial_regex, "\\s[0-9]{2}:[0-9]{2}:[0-9]{2}\\sUTC$") - slice[[field]] <- - if (all(grepl(paste0(date_partial_regex, "$"), slice[[field]]))) { - as.Date(slice[[field]]) - } else if (all(grepl(time_stamp_regex, slice[[field]]))) { - as.POSIXct(slice[[field]], tz = "UTC") - } else { - slice[[field]] - } + slice[[field]] <- + if (all(grepl(paste0(date_partial_regex, "$"), slice[[field]]))) { + as.Date(slice[[field]]) + } else if (all(grepl(time_stamp_regex, slice[[field]]))) { + as.POSIXct(slice[[field]], tz = "UTC") + } else { + slice[[field]] + } + } else { + slice[[field]] <- character(0) + } } } slice diff --git a/R/utils.R b/R/utils.R index 3a33619688..99ba006535 100644 --- a/R/utils.R +++ b/R/utils.R @@ -56,10 +56,9 @@ include_parent_datanames <- function(dataname, join_keys) { #' @param datanames (`character`) vector of data set names to include; must be subset of `datanames(x)` #' @return (`FilteredData`) object #' @keywords internal -teal_data_to_filtered_data <- function(x, datanames = teal.data::datanames(x)) { +teal_data_to_filtered_data <- function(x, datanames = teal_data_datanames(x)) { checkmate::assert_class(x, "teal_data") - checkmate::assert_character(datanames, min.len = 1L, any.missing = FALSE) - checkmate::assert_subset(datanames, teal.data::datanames(x)) + checkmate::assert_character(datanames, min.chars = 1L, any.missing = FALSE) ans <- teal.slice::init_filtered_data( x = sapply(datanames, function(dn) x[[dn]], simplify = FALSE), @@ -215,3 +214,88 @@ check_filter_datanames <- function(filters, datanames) { TRUE } } + +#' Wrapper on `teal.data::datanames` +#' +#' Special function used in internals of `teal` to return names of datasets even if `datanames` +#' has not been set. +#' @param data (`teal_data`) +#' @return `character` +#' @keywords internal +teal_data_datanames <- function(data) { + checkmate::assert_class(data, "teal_data") + if (length(teal.data::datanames(data))) { + teal.data::datanames(data) + } else { + ls(teal.code::get_env(data), all.names = TRUE) + } +} + +#' Function for validating the title parameter of `teal::init` +#' +#' Checks if the input of the title from `teal::init` will create a valid title and favicon tag. +#' @param shiny_tag (`shiny.tag`) Object to validate for a valid title. +#' @keywords internal +validate_app_title_tag <- function(shiny_tag) { + checkmate::assert_class(shiny_tag, "shiny.tag") + checkmate::assert_true(shiny_tag$name == "head") + child_names <- vapply(shiny_tag$children, `[[`, character(1L), "name") + checkmate::assert_subset(c("title", "link"), child_names, .var.name = "child tags") + rel_attr <- shiny_tag$children[[which(child_names == "link")]]$attribs$rel + checkmate::assert_subset( + rel_attr, + c("icon", "shortcut icon"), + .var.name = "Link tag's rel attribute", + empty.ok = FALSE + ) +} + +#' Build app title with favicon +#' +#' A helper function to create the browser title along with a logo. +#' +#' @param title (`character`) The browser title for the teal app +#' @param favicon (`character`) The path for the icon for the title. +#' The image/icon path can be remote or the static path accessible by shiny, like the `www/` +#' +#' @return A `shiny.tag` containing the element that adds the title and logo to the shiny app +#' @export +build_app_title <- function(title = "teal app", favicon = "https://raw.githubusercontent.com/insightsengineering/hex-stickers/main/PNG/nest.png") { # nolint + checkmate::assert_string(title, null.ok = TRUE) + checkmate::assert_string(favicon, null.ok = TRUE) + tags$head( + tags$title(title), + tags$link( + rel = "icon", + href = favicon, + sizes = "any" + ) + ) +} + +#' Application ID +#' +#' Creates App ID used to match filter snapshots to application. +#' +#' Calculate app ID that will be used to stamp filter state snapshots. +#' App ID is a hash of the app's data and modules. +#' See "transferring snapshots" section in ?snapshot. +#' +#' @param data `teal_data` or `teal_data_module` as accepted by `init` +#' @param modules `teal_modules` object as accepted by `init` +#' +#' @return A single character string. +#' +#' @keywords internal +create_app_id <- function(data, modules) { + checkmate::assert_multi_class(data, c("teal_data", "teal_data_module")) + checkmate::assert_class(modules, "teal_modules") + + hashables <- c(data, modules) + hashables$data <- if (inherits(hashables$data, "teal_data")) { + as.list(hashables$data@env) + } else if (inherits(data, "teal_data_module")) { + body(data$server) + } + rlang::hash(hashables) +} diff --git a/R/validate_inputs.R b/R/validate_inputs.R index 403188334a..8377d2018a 100644 --- a/R/validate_inputs.R +++ b/R/validate_inputs.R @@ -152,7 +152,7 @@ gather_messages <- function(iv) { failing_inputs <- Filter(Negate(is.null), status) unique(lapply(failing_inputs, function(x) x[["message"]])) } else { - logger::log_warn("Validator is disabled and will be omitted.") + warning("Validator is disabled and will be omitted.") list() } } diff --git a/README.md b/README.md index b37c4fb10c..ec27cede9b 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,6 @@ A lot of the functionality of the `teal` framework derives from the following pa - [`teal.widgets`](https://insightsengineering.github.io/teal.widgets/): shiny components used within `teal`. - [`teal.slice`](https://insightsengineering.github.io/teal.slice/): provides a filtering panel to allow filtering of data. - [`teal.code`](https://insightsengineering.github.io/teal.code/): handles reproducibility of outputs. -- [`teal.transform`](https://insightsengineering.github.io/teal.transform/): standardizes extracting and merging data. - [`teal.logger`](https://insightsengineering.github.io/teal.logger/): standardizes logging within `teal` framework. - [`teal.reporter`](https://insightsengineering.github.io/teal.reporter/): allows `teal` applications to generate reports. diff --git a/_pkgdown.yml b/_pkgdown.yml index bbdbe9cc2e..caf2b07604 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -6,9 +6,12 @@ template: navbar: structure: - left: [intro, reference, articles, blueprint, tutorials, news, reports] + left: [get-started, reference, articles, blueprint, tutorials, news, reports] right: [search, github] components: + get-started: + text: Get started + href: articles/getting-started-with-teal.html reports: text: Reports menu: @@ -45,8 +48,8 @@ navbar: href: https://github.com/insightsengineering/teal articles: -- title: Get Started - navbar: ~ +- title: Get started + navbar: Get started contents: - getting-started-with-teal - title: Using teal @@ -97,6 +100,10 @@ reference: - srv_teal_with_splash - ui_teal_with_splash - teal_slices + - title: Helper Functions + desc: Helper functions for `teal` + contents: + - build_app_title - title: Example Module desc: A simple `teal` module contents: @@ -120,9 +127,6 @@ reference: - within.teal_data_module - show_rcode_modal - join_keys.tdata - # - title: Functions Moved to Other Packages - # desc: These functions have been moved from teal and will be deprecated - # contents: - title: Validation Functions contents: - starts_with("validate_") diff --git a/inst/WORDLIST b/inst/WORDLIST index 799c44f7f5..74db826371 100644 --- a/inst/WORDLIST +++ b/inst/WORDLIST @@ -11,12 +11,13 @@ analysing cloneable customizable dropdown +favicon +favicons funder omics pharmaverse pre programmatically -qenv repo reproducibility summarization diff --git a/man/build_app_title.Rd b/man/build_app_title.Rd new file mode 100644 index 0000000000..9354b85b8d --- /dev/null +++ b/man/build_app_title.Rd @@ -0,0 +1,24 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils.R +\name{build_app_title} +\alias{build_app_title} +\title{Build app title with favicon} +\usage{ +build_app_title( + title = "teal app", + favicon = + "https://raw.githubusercontent.com/insightsengineering/hex-stickers/main/PNG/nest.png" +) +} +\arguments{ +\item{title}{(\code{character}) The browser title for the teal app} + +\item{favicon}{(\code{character}) The path for the icon for the title. +The image/icon path can be remote or the static path accessible by shiny, like the \verb{www/}} +} +\value{ +A \code{shiny.tag} containing the element that adds the title and logo to the shiny app +} +\description{ +A helper function to create the browser title along with a logo. +} diff --git a/man/create_app_id.Rd b/man/create_app_id.Rd new file mode 100644 index 0000000000..755e50497d --- /dev/null +++ b/man/create_app_id.Rd @@ -0,0 +1,25 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils.R +\name{create_app_id} +\alias{create_app_id} +\title{Application ID} +\usage{ +create_app_id(data, modules) +} +\arguments{ +\item{data}{\code{teal_data} or \code{teal_data_module} as accepted by \code{init}} + +\item{modules}{\code{teal_modules} object as accepted by \code{init}} +} +\value{ +A single character string. +} +\description{ +Creates App ID used to match filter snapshots to application. +} +\details{ +Calculate app ID that will be used to stamp filter state snapshots. +App ID is a hash of the app's data and modules. +See "transferring snapshots" section in ?snapshot. +} +\keyword{internal} diff --git a/man/eval_code.Rd b/man/eval_code.Rd deleted file mode 100644 index fd2ab5ffe3..0000000000 --- a/man/eval_code.Rd +++ /dev/null @@ -1,37 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/teal_data_module-eval_code.R -\name{eval_code} -\alias{eval_code} -\alias{eval_code,teal_data_module,character-method} -\alias{eval_code,teal_data_module,language-method} -\alias{eval_code,teal_data_module,expression-method} -\title{Evaluate the code in the qenv environment} -\usage{ -\S4method{eval_code}{teal_data_module,character}(object, code) - -\S4method{eval_code}{teal_data_module,language}(object, code) - -\S4method{eval_code}{teal_data_module,expression}(object, code) -} -\arguments{ -\item{object}{(\code{teal_data_module})} - -\item{code}{(\code{character} or \code{language}) code to evaluate. If \code{character}, comments are retained.} -} -\value{ -Returns a \code{teal_data_module} object. -} -\description{ -Given code is evaluated in the \code{qenv} environment of \code{teal_data} reactive defined in \code{teal_data_module}. -} -\examples{ -tdm <- teal_data_module( - ui = function(id) div(id = shiny::NS(id)("div_id")), - server = function(id) { - shiny::moduleServer(id, function(input, output, session) { - shiny::reactive(teal_data(IRIS = iris)) - }) - } -) -eval_code(tdm, "IRIS <- subset(IRIS, Species == 'virginica')") -} diff --git a/man/init.Rd b/man/init.Rd index 308af0d5dc..8c7c0ccf3d 100644 --- a/man/init.Rd +++ b/man/init.Rd @@ -7,7 +7,7 @@ init( data, modules, - title = NULL, + title = build_app_title(), filter = teal_slices(), header = tags$p(), footer = tags$p(), @@ -17,7 +17,7 @@ init( \arguments{ \item{data}{(\code{teal_data}, \code{teal_data_module}, \verb{named list})\cr \code{teal_data} object as returned by \code{\link[teal.data:teal_data]{teal.data::teal_data()}} or -\code{teal_data_modules} or simply a list of a named list of objects +\code{teal_data_module} or simply a list of a named list of objects (\code{data.frame} or \code{MultiAssayExperiment}).} \item{modules}{(\code{list}, \code{teal_modules} or \code{teal_module})\cr @@ -26,21 +26,21 @@ nested list of \code{teal_modules} or \code{teal_module} objects or a single will be displayed in the teal application. See \code{\link[=modules]{modules()}} and \code{\link[=module]{module()}} for more details.} -\item{title}{(\code{NULL} or \code{character})\cr -The browser window title (defaults to the host URL of the page).} +\item{title}{(\code{shiny.tag} or \code{character(1)})\cr +The browser window title. Defaults to a title "teal app" with the icon of NEST. +Can be created using the \code{build_app_title()} or +by passing a valid \code{shiny.tag} which is a head tag with title and link tag.} \item{filter}{(\code{teal_slices})\cr Specification of initial filter. Filters can be specified using \code{\link[=teal_slices]{teal_slices()}}. Old way of specifying filters through a list is deprecated and will be removed in the next release. Please fix your applications to use \code{\link[=teal_slices]{teal_slices()}}.} -\item{header}{(\code{shiny.tag} or \code{character}) \cr -the header of the app. Note shiny code placed here (and in the footer -argument) will be placed in the app's \code{ui} function so code which needs to be placed in the \code{ui} function -(such as loading \code{CSS} via \code{\link[htmltools:htmlDependency]{htmltools::htmlDependency()}}) should be included here.} +\item{header}{(\code{shiny.tag} or \code{character(1)}) \cr +The header of the app.} -\item{footer}{(\code{shiny.tag} or \code{character})\cr -the footer of the app} +\item{footer}{(\code{shiny.tag} or \code{character(1)})\cr +The footer of the app.} \item{id}{(\code{character})\cr module id to embed it, if provided, @@ -56,6 +56,10 @@ named list with \code{server} and \code{ui} function End-users: This is the most important function for you to start a teal app that is composed out of teal modules. } +\details{ +When initializing the \code{teal} app, if \code{datanames} are not set for the \code{teal_data} object, +defaults from the \code{teal_data} environment will be used. +} \examples{ app <- init( data = teal_data( diff --git a/man/module_teal.Rd b/man/module_teal.Rd index 5017667dd9..7e0a6f6811 100644 --- a/man/module_teal.Rd +++ b/man/module_teal.Rd @@ -9,9 +9,9 @@ ui_teal( id, splash_ui = tags$h2("Starting the Teal App"), - title = NULL, - header = tags$p(""), - footer = tags$p("") + title = build_app_title(), + header = tags$p(), + footer = tags$p() ) srv_teal(id, modules, teal_data_rv, filter = teal_slices()) @@ -24,16 +24,16 @@ module id} can be a splash screen or a Shiny module UI. For the latter, see \code{\link[=init]{init()}} about how to call the corresponding server function.} -\item{title}{(\code{NULL} or \code{character})\cr -The browser window title (defaults to the host URL of the page).} +\item{title}{(\code{shiny.tag} or \code{character(1)})\cr +The browser window title. Defaults to a title "teal app" with the icon of NEST. +Can be created using the \code{build_app_title()} or +by passing a valid \code{shiny.tag} which is a head tag with title and link tag.} -\item{header}{(\code{shiny.tag} or \code{character}) \cr -the header of the app. Note shiny code placed here (and in the footer -argument) will be placed in the app's \code{ui} function so code which needs to be placed in the \code{ui} function -(such as loading \code{CSS} via \code{\link[htmltools:htmlDependency]{htmltools::htmlDependency()}}) should be included here.} +\item{header}{(\code{shiny.tag} or \code{character(1)}) \cr +The header of the app.} -\item{footer}{(\code{shiny.tag} or \code{character})\cr -the footer of the app} +\item{footer}{(\code{shiny.tag} or \code{character(1)})\cr +The footer of the app.} \item{teal_data_rv}{(\code{reactive})\cr returns the \code{teal_data}, only evaluated once, \code{NULL} value is ignored} diff --git a/man/srv_teal_with_splash.Rd b/man/srv_teal_with_splash.Rd index decc5b42ee..ad1c259cd0 100644 --- a/man/srv_teal_with_splash.Rd +++ b/man/srv_teal_with_splash.Rd @@ -16,7 +16,7 @@ is then preferred to this function.} \item{data}{(\code{teal_data}, \code{teal_data_module}, \verb{named list})\cr \code{teal_data} object as returned by \code{\link[teal.data:teal_data]{teal.data::teal_data()}} or -\code{teal_data_modules} or simply a list of a named list of objects +\code{teal_data_module} or simply a list of a named list of objects (\code{data.frame} or \code{MultiAssayExperiment}).} \item{modules}{\code{teal_modules} object containing the output modules which diff --git a/man/teal_data_datanames.Rd b/man/teal_data_datanames.Rd new file mode 100644 index 0000000000..9c28c06aaf --- /dev/null +++ b/man/teal_data_datanames.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils.R +\name{teal_data_datanames} +\alias{teal_data_datanames} +\title{Wrapper on \code{teal.data::datanames}} +\usage{ +teal_data_datanames(data) +} +\arguments{ +\item{data}{(\code{teal_data})} +} +\value{ +\code{character} +} +\description{ +Special function used in internals of \code{teal} to return names of datasets even if \code{datanames} +has not been set. +} +\keyword{internal} diff --git a/man/teal_data_module.Rd b/man/teal_data_module.Rd index ba5aec55da..043e394547 100644 --- a/man/teal_data_module.Rd +++ b/man/teal_data_module.Rd @@ -1,10 +1,21 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/teal_data_module.R +% Please edit documentation in R/teal_data_module.R, +% R/teal_data_module-eval_code.R, R/teal_data_module-within.R \name{teal_data_module} \alias{teal_data_module} -\title{Data module for \code{teal} applications} +\alias{eval_code} +\alias{eval_code,teal_data_module,character-method} +\alias{eval_code,teal_data_module,language-method} +\alias{eval_code,teal_data_module,expression-method} +\alias{within} +\alias{within.teal_data_module} +\title{Data Module for \code{teal} Applications} \usage{ teal_data_module(ui, server) + +\S4method{eval_code}{teal_data_module,character}(object, code) + +\method{within}{teal_data_module}(data, expr, ...) } \arguments{ \item{ui}{(\verb{function(id)})\cr @@ -13,21 +24,42 @@ teal_data_module(ui, server) \item{server}{(\verb{function(id)})\cr \code{shiny} module \code{ui} function; must only take \code{id} argument; must return reactive expression containing \code{teal_data} object} + +\item{object}{(\code{teal_data_module})} + +\item{code}{(\code{character} or \code{language}) code to evaluate. If \code{character}, comments are retained.} + +\item{data}{(\code{teal_data_module}) object} + +\item{expr}{(\code{expression}) to evaluate. Must be inline code. See} + +\item{...}{See \code{Details}.} } \value{ -Object of class \code{teal_data_module}. +\code{teal_data_module} returns an object of class \code{teal_data_module}. + +\code{eval_code} returns a \code{teal_data_module} object with a delayed evaluation of \code{code} when the module is run. + +\code{within} returns a \code{teal_data_module} object with a delayed evaluation of \code{expr} when the module is run. } \description{ \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#experimental}{\figure{lifecycle-experimental.svg}{options: alt='[Experimental]'}}}{\strong{[Experimental]}} -Creates \code{teal_data_module} object - a \code{shiny} module to supply or modify data in a \code{teal} application. -This function creates a \code{shiny} module that allows for running data pre-processing code after the app starts. +Create a \code{teal_data_module} object and evaluate code on it with history tracking. +} +\details{ +\code{teal_data_module} creates a \code{shiny} module to supply or modify data in a \code{teal} application. +The module allows for running data pre-processing code (creation \emph{and} some modification) after the app starts. The body of the server function will be run in the app rather than in the global environment. -This means it will be run every time the app starts, so use sparingly. - +This means it will be run every time the app starts, so use sparingly.\cr Pass this module instead of a \code{teal_data} object in a call to \code{\link[=init]{init()}}. - +Note that the server function must always return a \code{teal_data} object wrapped in a reactive expression.\cr See vignette \code{vignette("data-as-shiny-module", package = "teal")} for more details. + +\code{eval_code} evaluates given code in the environment of the \code{teal_data} object created by the \code{teal_data_module}. +The code is added to the \verb{@code} slot of the \code{teal_data}. + +\code{within} is a convenience function for evaluating inline code inside the environment of a \code{teal_data_module}. } \examples{ data <- teal_data_module( @@ -52,4 +84,28 @@ data <- teal_data_module( }) } ) + +tdm <- teal_data_module( + ui = function(id) div(id = shiny::NS(id)("div_id")), + server = function(id) { + shiny::moduleServer(id, function(input, output, session) { + shiny::reactive(teal_data(IRIS = iris)) + }) + } +) +eval_code(tdm, "IRIS <- subset(IRIS, Species == 'virginica')") + +tdm <- teal_data_module( + ui = function(id) div(id = shiny::NS(id)("div_id")), + server = function(id) { + shiny::moduleServer(id, function(input, output, session) { + shiny::reactive(teal_data(IRIS = iris)) + }) + } +) +within(tdm, IRIS <- subset(IRIS, Species == "virginica")) + +} +\seealso{ +\code{\linkS4class{teal_data}}, \code{\link[base:with]{base::within()}}, \code{\link[teal.code:qenv]{teal.code::within.qenv()}} } diff --git a/man/teal_data_to_filtered_data.Rd b/man/teal_data_to_filtered_data.Rd index 2528930eb5..f3906674a2 100644 --- a/man/teal_data_to_filtered_data.Rd +++ b/man/teal_data_to_filtered_data.Rd @@ -4,7 +4,7 @@ \alias{teal_data_to_filtered_data} \title{Create a \code{FilteredData}} \usage{ -teal_data_to_filtered_data(x, datanames = teal.data::datanames(x)) +teal_data_to_filtered_data(x, datanames = teal_data_datanames(x)) } \arguments{ \item{x}{(\code{teal_data}) object} diff --git a/man/ui_teal_with_splash.Rd b/man/ui_teal_with_splash.Rd index 35fcf371fd..90dce7e32d 100644 --- a/man/ui_teal_with_splash.Rd +++ b/man/ui_teal_with_splash.Rd @@ -7,9 +7,9 @@ ui_teal_with_splash( id, data, - title, - header = tags$p("Add Title Here"), - footer = tags$p("Add Footer Here") + title = build_app_title(), + header = tags$p(), + footer = tags$p() ) } \arguments{ @@ -18,19 +18,19 @@ module id} \item{data}{(\code{teal_data}, \code{teal_data_module}, \verb{named list})\cr \code{teal_data} object as returned by \code{\link[teal.data:teal_data]{teal.data::teal_data()}} or -\code{teal_data_modules} or simply a list of a named list of objects +\code{teal_data_module} or simply a list of a named list of objects (\code{data.frame} or \code{MultiAssayExperiment}).} -\item{title}{(\code{NULL} or \code{character})\cr -The browser window title (defaults to the host URL of the page).} +\item{title}{(\code{shiny.tag} or \code{character(1)})\cr +The browser window title. Defaults to a title "teal app" with the icon of NEST. +Can be created using the \code{build_app_title()} or +by passing a valid \code{shiny.tag} which is a head tag with title and link tag.} -\item{header}{(\code{shiny.tag} or \code{character}) \cr -the header of the app. Note shiny code placed here (and in the footer -argument) will be placed in the app's \code{ui} function so code which needs to be placed in the \code{ui} function -(such as loading \code{CSS} via \code{\link[htmltools:htmlDependency]{htmltools::htmlDependency()}}) should be included here.} +\item{header}{(\code{shiny.tag} or \code{character(1)}) \cr +The header of the app.} -\item{footer}{(\code{shiny.tag} or \code{character})\cr -the footer of the app} +\item{footer}{(\code{shiny.tag} or \code{character(1)})\cr +The footer of the app.} } \description{ \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#stable}{\figure{lifecycle-stable.svg}{options: alt='[Stable]'}}}{\strong{[Stable]}} diff --git a/man/validate_app_title_tag.Rd b/man/validate_app_title_tag.Rd new file mode 100644 index 0000000000..d646bfda31 --- /dev/null +++ b/man/validate_app_title_tag.Rd @@ -0,0 +1,15 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils.R +\name{validate_app_title_tag} +\alias{validate_app_title_tag} +\title{Function for validating the title parameter of \code{teal::init}} +\usage{ +validate_app_title_tag(shiny_tag) +} +\arguments{ +\item{shiny_tag}{(\code{shiny.tag}) Object to validate for a valid title.} +} +\description{ +Checks if the input of the title from \code{teal::init} will create a valid title and favicon tag. +} +\keyword{internal} diff --git a/man/within.teal_data_module.Rd b/man/within.teal_data_module.Rd deleted file mode 100644 index 98e7a4087f..0000000000 --- a/man/within.teal_data_module.Rd +++ /dev/null @@ -1,55 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/teal_data_module-eval_code.R -\name{within.teal_data_module} -\alias{within.teal_data_module} -\title{Code Tracking With \code{qenv} Object} -\usage{ -\method{within}{teal_data_module}(data, expr, ...) -} -\arguments{ -\item{data}{(\code{teal_data_module}) object} - -\item{expr}{(\code{expression}) to evaluate. Must be inline code, see \verb{Using language objects...}} - -\item{...}{see \code{Details}} -} -\value{ -Returns a \code{teal_data_module} object with a delayed evaluation of \code{expr} -when module. -} -\description{ -Convenience function for evaluating inline code inside the environment of a -\code{teal_data_module} -} -\details{ -\code{qenv()} instantiates a \code{qenv} with an empty environment. -Any changes must be made by evaluating code in it with \code{eval_code} or \code{within}, thereby ensuring reproducibility. - -\code{new_qenv()} (\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} and not recommended) -can instantiate a \code{qenv} object with data in the environment and code registered. - -\code{eval_code} evaluates given code in the \code{qenv} environment and appends it to the \code{code} slot. -Thus, if the \code{qenv} had been instantiated empty, contents of the environment are always a result of the stored code. - -\code{get_code} retrieves the code stored in the \code{qenv}. \code{...} passes arguments to methods. - -\code{within} is a convenience function for evaluating inline code inside the environment of a \code{qenv}. -It is a method for the \code{base} generic that wraps \code{eval_code} to provide a simplified way of passing code. -\code{within} accepts only inline expressions (both simple and compound) and allows for injecting values into \code{expr} -through the \code{...} argument: -as \code{name:value} pairs are passed to \code{...}, \code{name} in \code{expr} will be replaced with \code{value}. -} -\examples{ -tdm <- teal_data_module( - ui = function(id) div(id = shiny::NS(id)("div_id")), - server = function(id) { - shiny::moduleServer(id, function(input, output, session) { - shiny::reactive(teal_data(IRIS = iris)) - }) - } -) -within(tdm, IRIS <- subset(IRIS, Species == "virginica")) -} -\seealso{ -\code{\link[base:with]{base::within()}}, \code{\link[=teal_data_module]{teal_data_module()}} -} diff --git a/staged_dependencies.yaml b/staged_dependencies.yaml index 6ec27ef219..f28f2f80ab 100644 --- a/staged_dependencies.yaml +++ b/staged_dependencies.yaml @@ -15,9 +15,6 @@ upstream_repos: insightsengineering/teal.slice: repo: insightsengineering/teal.slice host: https://github.com - insightsengineering/teal.transform: - repo: insightsengineering/teal.transform - host: https://github.com insightsengineering/teal.logger: repo: insightsengineering/teal.logger host: https://github.com diff --git a/tests/testthat/setup-logger.R b/tests/testthat/setup-logger.R new file mode 100644 index 0000000000..1a7b3e5c5f --- /dev/null +++ b/tests/testthat/setup-logger.R @@ -0,0 +1 @@ +logger::log_appender(function(...) {}, namespace = "teal") diff --git a/tests/testthat/test-init.R b/tests/testthat/test-init.R index 53bd16aada..8a7a56f134 100644 --- a/tests/testthat/test-init.R +++ b/tests/testthat/test-init.R @@ -77,30 +77,30 @@ testthat::test_that("init filter accepts `teal_slices`", { testthat::expect_no_error(init(data = list(iris = iris), modules = modules(example_module()), filter = fs)) testthat::expect_error( init(data = list(iris = iris), modules = modules(example_module()), filter = unclass(fs)), - "Assertion failed" + "Assertion on 'filter' failed" ) }) testthat::test_that("init throws when data has no datanames", { testthat::expect_error( init(data = teal_data(), modules = list(example_module())), - "has no datanames" + "`data` object has no datanames and its environment is empty" ) }) testthat::test_that("init throws when incompatible module's datanames", { msg <- "Module 'example teal module' uses datanames not available in 'data'" - testthat::expect_output( - testthat::expect_error( - init(data = teal_data(mtcars = mtcars), modules = list(example_module(datanames = "iris"))), - msg + testthat::expect_error( + init( + data = teal_data(mtcars = mtcars), + modules = list(example_module(datanames = "iris")) ), - msg + "Module 'example teal module' uses datanames not available in 'data'" ) }) testthat::test_that("init throws when incompatible filter's datanames", { - testthat::expect_output( + testthat::expect_warning( init( data = teal_data(mtcars = mtcars), modules = modules(example_module()), diff --git a/tests/testthat/test-module_nested_tabs.R b/tests/testthat/test-module_nested_tabs.R index 6ef8a8c3ad..3f07fc6a87 100644 --- a/tests/testthat/test-module_nested_tabs.R +++ b/tests/testthat/test-module_nested_tabs.R @@ -147,36 +147,30 @@ out <- shiny::testServer( reporter = teal.reporter::Reporter$new() ), expr = { - # to adjust input modules to the active modules (server_args is dropped when NULL) - test_module1$server_args <- NULL - test_module2$server_args <- NULL - test_module3$server_args <- NULL - test_module4$server_args <- NULL - testthat::test_that("modules_reactive is a list of reactives", { - expect_is(modules_reactive, "list") - expect_is(modules_reactive$tab1, "reactive") - expect_is(modules_reactive$tab2, "reactive") + testthat::expect_is(modules_reactive, "list") + testthat::expect_is(modules_reactive$tab1, "reactive") + testthat::expect_is(modules_reactive$tab2, "reactive") }) testthat::test_that("modules_reactive returns modules according to selection in the nested tabs", { session$setInputs(`tab1-active_tab` = "test2") # active tab in tab1 session$setInputs(`tab2-active_tab` = "test3") # active tab in tab2 nested_active_modules <- lapply(modules_reactive, function(child) child()) - expect_identical(nested_active_modules, list(tab1 = test_module2, tab2 = test_module3)) + testthat::expect_identical(nested_active_modules, list(tab1 = test_module2, tab2 = test_module3)) session$setInputs(`tab1-active_tab` = "test1") # active tab in tab1 session$setInputs(`tab2-active_tab` = "test4") # active tab in tab2 nested_active_modules <- lapply(modules_reactive, function(child) child()) - expect_identical(nested_active_modules, list(tab1 = test_module1, tab2 = test_module4)) + testthat::expect_identical(nested_active_modules, list(tab1 = test_module1, tab2 = test_module4)) }) testthat::test_that("Change of this tab returns active module from this tab", { session$setInputs(`active_tab` = "tab1") - expect_identical(get_active_module(), test_module1) + testthat::expect_identical(get_active_module(), test_module1) session$setInputs(`active_tab` = "tab2") - expect_identical(get_active_module(), test_module4) + testthat::expect_identical(get_active_module(), test_module4) }) } ) @@ -233,7 +227,7 @@ testthat::test_that("srv_nested_tabs.teal_module passes data to the server modul moduleServer(id, function(input, output, session) checkmate::assert_list(data, "reactive")) }) - testthat::expect_error( + testthat::expect_no_error( shiny::testServer( app = srv_nested_tabs, args = list( @@ -243,8 +237,7 @@ testthat::test_that("srv_nested_tabs.teal_module passes data to the server modul reporter = teal.reporter::Reporter$new() ), expr = NULL - ), - NA + ) ) }) @@ -255,7 +248,7 @@ testthat::test_that("srv_nested_tabs.teal_module passes (deprecated) datasets to }) ) - testthat::expect_error( + testthat::expect_no_error( shiny::testServer( app = srv_nested_tabs, args = list( @@ -265,8 +258,7 @@ testthat::test_that("srv_nested_tabs.teal_module passes (deprecated) datasets to reporter = teal.reporter::Reporter$new() ), expr = NULL - ), - NA + ) ) }) @@ -276,7 +268,7 @@ testthat::test_that("srv_nested_tabs.teal_module passes server_args to the ...", moduleServer(id, function(input, output, session) stopifnot(identical(list(...), server_args))) }) - testthat::expect_error( + testthat::expect_no_error( shiny::testServer( app = srv_nested_tabs, args = list( @@ -286,8 +278,7 @@ testthat::test_that("srv_nested_tabs.teal_module passes server_args to the ...", reporter = teal.reporter::Reporter$new() ), expr = NULL - ), - NA + ) ) }) @@ -348,7 +339,7 @@ testthat::test_that("srv_nested_tabs.teal_module passes filter_panel_api to the moduleServer(id, function(input, output, session) checkmate::assert_class(filter_panel_api, "FilterPanelAPI")) }) - testthat::expect_error( + testthat::expect_no_error( shiny::testServer( app = srv_nested_tabs, args = list( @@ -358,8 +349,7 @@ testthat::test_that("srv_nested_tabs.teal_module passes filter_panel_api to the reporter = teal.reporter::Reporter$new() ), expr = NULL - ), - NA + ) ) }) @@ -377,7 +367,7 @@ testthat::test_that(".datasets_to_data returns data which is filtered", { d1_filtered <- data[["d1"]] testthat::expect_equal(d1_filtered, data.frame(id = 1:2, pk = 2:3, val = 1:2)) d2_filtered <- data[["d2"]] - testthat::expect_equal(d2_filtered, data.frame(id = 1:5, value = 1:5)) + testthat::expect_equal(d2_filtered, data.frame(id = 2:3, value = 2:3)) }) @@ -432,7 +422,7 @@ testthat::test_that("calculate_hashes takes a FilteredData and vector of datanam ) ) - testthat::expect_error(calculate_hashes(datanames = c("ADSL", "ADAE", "ADTTE"), datasets = datasets), NA) + testthat::expect_no_error(calculate_hashes(datanames = c("ADSL", "ADAE", "ADTTE"), datasets = datasets)) }) testthat::test_that("calculate_hashes returns a named list", { diff --git a/tests/testthat/test-module_tabs_with_filters.R b/tests/testthat/test-module_tabs_with_filters.R index 1c9d1f8385..d732b2ef5b 100644 --- a/tests/testthat/test-module_tabs_with_filters.R +++ b/tests/testthat/test-module_tabs_with_filters.R @@ -44,9 +44,6 @@ testthat::test_that("active_module() returns module specs from active tab when f reporter = teal.reporter::Reporter$new() ), expr = { - test_module1$server_args <- NULL # because empty server_args are dropped from object in srv_nested_tabs - test_module2$server_args <- NULL - session$setInputs(`root-active_tab` = "iris_tab") testthat::expect_identical(active_module(), test_module1) session$setInputs(`root-active_tab` = "mtcars_tab") diff --git a/tests/testthat/test-module_teal_with_splash.R b/tests/testthat/test-module_teal_with_splash.R index e5b1b7a02c..8ad4e8ea99 100644 --- a/tests/testthat/test-module_teal_with_splash.R +++ b/tests/testthat/test-module_teal_with_splash.R @@ -23,7 +23,7 @@ testthat::test_that("srv_teal_with_splash throws when teal_data_module doesn't r ), expr = {} ), - "The `teal_data_module` must return a reactive expression." + "The `teal_data_module` passed to `data` must return a reactive expression." ) }) @@ -57,17 +57,21 @@ testthat::test_that("srv_teal_with_splash passes teal_data to reactive", { ) }) -testthat::test_that("srv_teal_with_splash throws when datanames are empty", { - shiny::testServer( - app = srv_teal_with_splash, - args = list( - id = "test", - data = teal_data(), - modules = modules(example_module()) +testthat::test_that("srv_teal_with_splash passes when datanames are empty with warning", { + testthat::expect_warning( + shiny::testServer( + app = srv_teal_with_splash, + args = list( + id = "test", + data = teal_data(), + modules = modules(example_module()) + ), + expr = { + testthat::expect_is(teal_data_rv_validate, "reactive") + testthat::expect_s4_class(teal_data_rv_validate(), "teal_data") + } ), - expr = { - testthat::expect_error(teal_data_rv_validate(), "Data has no datanames") - } + "`data` object has no datanames. Default datanames are set using `teal_data`'s environment." ) }) @@ -122,7 +126,7 @@ testthat::test_that( ), expr = { testthat::expect_is(teal_data_rv_validate, "reactive") - testthat::expect_error(teal_data_rv_validate(), "did not return `teal_data`") + testthat::expect_error(teal_data_rv_validate(), "failed to return `teal_data`") } ) } @@ -133,8 +137,8 @@ testthat::test_that("srv_teal_with_splash teal_data_rv_validate throws when inco app = srv_teal_with_splash, args = list( id = "test", - data = teal_data(mtcars = mtcars), - modules = modules(example_module(datanames = "iris")) + data = teal_data(mtcars = mtcars, iris = iris, npk = npk), + modules = modules(example_module(datanames = "non-existing")) ), expr = { testthat::expect_is(teal_data_rv_validate, "reactive") @@ -157,6 +161,10 @@ testthat::test_that("srv_teal_with_splash teal_data_rv_validate returns teal_dat ), expr = { testthat::expect_is(teal_data_rv_validate, "reactive") + testthat::expect_warning( + teal_data_rv_validate(), + "Filter 'iris Species' refers to dataname not available in 'data'" + ) testthat::expect_s4_class(teal_data_rv_validate(), "teal_data") } ) @@ -238,7 +246,10 @@ testthat::test_that("srv_teal_with_splash throws error when within.teal_data_mod testthat::expect_s3_class(teal_data_rv, "reactive") testthat::expect_null(teal_data_rv()) testthat::expect_s3_class(teal_data_rv_validate, "reactive") - testthat::expect_error(teal_data_rv_validate(), "`teal_data_module` did not return `teal_data` object ") + testthat::expect_error( + teal_data_rv_validate(), + "`teal_data_module` passed to `data` failed to return `teal_data` object" + ) } ) ) @@ -264,7 +275,10 @@ testthat::test_that( testthat::expect_s3_class(teal_data_rv, "reactive") testthat::expect_null(teal_data_rv()) testthat::expect_s3_class(teal_data_rv_validate, "reactive") - testthat::expect_error(teal_data_rv_validate(), "`teal_data_module` did not return `teal_data` object ") + testthat::expect_error( + teal_data_rv_validate(), + "`teal_data_module` passed to `data` failed to return `teal_data` object" + ) } ) ) diff --git a/tests/testthat/test-modules.R b/tests/testthat/test-modules.R index 7dc93f2941..547a467967 100644 --- a/tests/testthat/test-modules.R +++ b/tests/testthat/test-modules.R @@ -107,7 +107,7 @@ testthat::test_that("module requires datanames argument to be a character or NUL testthat::expect_no_error(module(datanames = "all")) testthat::expect_no_error(module(datanames = "")) testthat::expect_no_error(module(datanames = NULL)) - testthat::expect_error(module(datanames = NA_character_), "Contains missing values") + testthat::expect_error(module(server = function(id, data) NULL, datanames = NA_character_), "Contains missing values") testthat::expect_no_error(module(server = function(id, data) NULL, datanames = NULL)) }) diff --git a/tests/testthat/test-report_previewer_module.R b/tests/testthat/test-report_previewer_module.R index d5d7b900ec..5636bed5cb 100644 --- a/tests/testthat/test-report_previewer_module.R +++ b/tests/testthat/test-report_previewer_module.R @@ -1,14 +1,18 @@ 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::expect_error( + reporter_previewer_module(label = 5), "Assertion on 'label' failed: Must be of type 'string'" + ) + testthat::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::expect_no_error(r_p_m <- reporter_previewer_module(label = "My label")) + testthat::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") + testthat::expect_equal(r_p_m$label, "Report previewer") }) diff --git a/tests/testthat/test-tdata.R b/tests/testthat/test-tdata.R index 4ed51fa4f0..daf6979b19 100644 --- a/tests/testthat/test-tdata.R +++ b/tests/testthat/test-tdata.R @@ -4,7 +4,7 @@ withr::local_options(lifecycle_verbosity = "quiet") testthat::test_that("new_tdata accepts reactive and not reactive MAE and data.frames", { utils::data(miniACC, package = "MultiAssayExperiment") - testthat::expect_error( + testthat::expect_no_error( new_tdata( list( a = reactive(data.frame(x = 1:10)), @@ -12,8 +12,7 @@ testthat::test_that("new_tdata accepts reactive and not reactive MAE and data.fr c = reactive(miniACC), d = miniACC ) - ), - NA + ) ) }) @@ -35,11 +34,6 @@ testthat::test_that("new_tdata throws error if contents of data list are not of testthat::expect_error( new_tdata(list(x = 1)), "May only contain the following types: \\{data.frame,reactive,MultiAssayExperiment\\}" ) - - testthat::expect_error( - new_tdata(list(x = reactive(1))), - "Must inherit from class 'data.frame'/'MultiAssayExperiment'" - ) }) testthat::test_that("new_tdata throws error if code is not character or reactive character", { @@ -55,12 +49,12 @@ testthat::test_that("new_tdata throws error if code is not character or reactive }) testthat::test_that("new_tdata accepts character and reactive characters for code argument", { - testthat::expect_error( - new_tdata(list(x = iris, y = mtcars), code = c("x <- iris", "y <- mtcars")), NA + testthat::expect_no_error( + new_tdata(list(x = iris, y = mtcars), code = c("x <- iris", "y <- mtcars")) ) - testthat::expect_error( - new_tdata(list(x = iris, y = mtcars), code = reactive(c("x <- iris", "y <- mtcars"))), NA + testthat::expect_no_error( + new_tdata(list(x = iris, y = mtcars), code = reactive(c("x <- iris", "y <- mtcars"))) ) }) @@ -72,9 +66,8 @@ testthat::test_that("new_tdata throws error if join_keys is not of class join_ke }) testthat::test_that("new_tdata throws no error if join_keys is of class join_keys", { - testthat::expect_error( - new_tdata(list(x = iris), join_keys = teal.data::join_keys()), - NA + testthat::expect_no_error( + new_tdata(list(x = iris), join_keys = teal.data::join_keys()) ) }) @@ -101,9 +94,8 @@ testthat::test_that( ) testthat::test_that("new_tdata does not throw error with valid metadata", { - testthat::expect_error( - new_tdata(list(x = iris, y = mtcars), metadata = list(x = list(A = 1), y = list(B = 1))), - NA + testthat::expect_no_error( + new_tdata(list(x = iris, y = mtcars), metadata = list(x = list(A = 1), y = list(B = 1))) ) }) @@ -165,7 +157,7 @@ testthat::test_that("get_code returns character of code if tdata object has code testthat::test_that("get_code_tdata accepts tdata", { data <- new_tdata(data = list(iris = iris), code = "iris <- iris") - testthat::expect_error(isolate(get_code_tdata(data)), NA) + testthat::expect_no_error(isolate(get_code_tdata(data))) }) testthat::test_that("get_code_tdata throws error when input is not tdata", { diff --git a/tests/testthat/test-teal_reporter.R b/tests/testthat/test-teal_reporter.R index 79401b4d15..7df055dac0 100644 --- a/tests/testthat/test-teal_reporter.R +++ b/tests/testthat/test-teal_reporter.R @@ -1,5 +1,5 @@ testthat::test_that("TealReportCard object can be initialized", { - testthat::expect_error(TealReportCard$new(), regexp = NA) + testthat::expect_no_error(TealReportCard$new()) }) testthat::test_that("TealReportCard inherits from ReportCard", { @@ -24,7 +24,7 @@ testthat::test_that("TealReportCard$get_content returns content with metadata", testthat::test_that("TealReportCard$append_src accepts a character", { card <- TealReportCard$new() - testthat::expect_error(card$append_src("test"), regexp = NA) + testthat::expect_no_error(card$append_src("test")) }) testthat::test_that("TealReportCard$append_src returns self", { @@ -40,7 +40,7 @@ testthat::test_that("TealReportCard$append_src returns title and content", { testthat::test_that("TealReportCard$append_encodings accepts list of character", { card <- TealReportCard$new() - testthat::expect_error(card$append_encodings(list(a = "test")), NA) + testthat::expect_no_error(card$append_encodings(list(a = "test"))) }) testthat::test_that("TealReportCard$append_encodings returns self", { @@ -60,11 +60,10 @@ testthat::test_that("TealReportCard$append_fs accepts only a teal_slices", { testthat::expect_error(card$append_fs(c(a = 1, b = 2)), regexp = "Assertion on 'fs' failed: Must inherit from class 'teal_slices', but has class 'numeric'." ) - testthat::expect_error( + testthat::expect_no_error( card$append_fs( teal.slice::teal_slices(teal.slice::teal_slice(dataname = "a", varname = "b")) - ), - regexp = NA + ) ) }) @@ -84,7 +83,7 @@ testthat::test_that("TealReportCard$append_fs returns title and content", { }) testthat::test_that("TealSlicesBlock$new accepts teal_slices only", { - testthat::expect_error(TealSlicesBlock$new(teal_slices()), NA) + testthat::expect_no_error(TealSlicesBlock$new(teal_slices())) testthat::expect_error(TealSlicesBlock$new(list()), "Assertion on 'content'") }) diff --git a/tests/testthat/test-teal_slices-store.R b/tests/testthat/test-teal_slices-store.R index be2d481b87..eeb28824ce 100644 --- a/tests/testthat/test-teal_slices-store.R +++ b/tests/testthat/test-teal_slices-store.R @@ -1,3 +1,45 @@ +testthat::test_that("teal_slice store/restore supports NULL and character(0) for choices and selected", { + slices_path <- withr::local_file("slices.json") + tss <- teal_slices( + teal_slice( + dataname = "data", + varname = "var", + choices = character(0), + selected = NULL + ) + ) + slices_store(tss, slices_path) + tss_restored <- teal:::slices_restore(slices_path) + + slices2_path <- withr::local_file("slices2.json") + tss2 <- teal_slices( + teal_slice( + dataname = "data", + varname = "var", + choices = NULL, + selected = NULL + ) + ) + slices_store(tss2, slices2_path) + tss2_restored <- slices_restore(slices2_path) + + slices3_path <- withr::local_file("slices3.json") + tss3 <- teal_slices( + teal_slice( + dataname = "data", + varname = "var", + choices = character(0), + selected = character(0) + ) + ) + slices_store(tss3, slices3_path) + tss3_restored <- slices_restore(slices3_path) + + teal.slice:::expect_identical_slice(tss[[1]], tss_restored[[1]]) + teal.slice:::expect_identical_slice(tss2[[1]], tss2_restored[[1]]) + teal.slice:::expect_identical_slice(tss3[[1]], tss3_restored[[1]]) +}) + testthat::test_that("teal_slice store/restore supports saving `POSIXct` timestamps in selected", { slices_path <- withr::local_file("slices.json") diff --git a/tests/testthat/test-utils.R b/tests/testthat/test-utils.R index e64f4265cf..77561d3ce9 100644 --- a/tests/testthat/test-utils.R +++ b/tests/testthat/test-utils.R @@ -36,3 +36,67 @@ testthat::test_that("report_card_template function returns TealReportCard object testthat::expect_equal(card$get_name(), "Card title") testthat::expect_length(card$get_content(), 1) }) + +test_that("teal_data_to_filtered_data return FilteredData class", { + teal_data <- teal.data::teal_data() + teal_data <- within(teal_data, iris <- head(iris)) + datanames(teal_data) <- "iris" + + testthat::expect_s3_class(teal_data_to_filtered_data(teal_data), "FilteredData") +}) + +test_that("teal_data_datanames returns names of the @env's objects when datanames not set", { + teal_data <- teal.data::teal_data() + teal_data <- within(teal_data, { + iris <- head(iris) + mtcars <- head(mtcars) + }) + testthat::expect_setequal(teal_data_datanames(teal_data), c("mtcars", "iris")) +}) + +test_that("teal_data_datanames returns datanames which are set by teal.data::datanames", { + teal_data <- teal.data::teal_data() + teal_data <- within(teal_data, { + iris <- head(iris) + mtcars <- head(mtcars) + }) + datanames(teal_data) <- "iris" + testthat::expect_equal(teal_data_datanames(teal_data), "iris") +}) + +test_that("validate_app_title_tag works on validating the title tag", { + valid_title <- tags$head( + tags$title("title"), + tags$link(rel = "icon", href = "favicon.ico"), + tags$div("Secret") + ) + + head_missing <- tags$div( + tags$title(title), + tags$link(rel = "icon", href = "favicon.ico") + ) + title_missing <- tags$head( + tags$link(rel = "icon", href = "favicon.ico") + ) + icon_missing <- tags$head( + tags$title(title) + ) + invalid_link <- tags$head( + tags$title("title"), + tags$link(href = "favicon.ico"), + tags$div("Secret") + ) + + testthat::expect_silent(validate_app_title_tag(valid_title)) + testthat::expect_error(validate_app_title_tag(head_missing)) + testthat::expect_error(validate_app_title_tag(title_missing)) + testthat::expect_error(validate_app_title_tag(icon_missing)) + testthat::expect_error(validate_app_title_tag(invalid_link)) +}) + +test_that("build_app_title builts a valid tag", { + valid_title_local <- build_app_title("title", "logo.png") + valid_title_remote <- build_app_title("title", "https://raw.githubusercontent.com/insightsengineering/hex-stickers/main/PNG/nest.png") # nolint + testthat::expect_silent(validate_app_title_tag(valid_title_local)) + testthat::expect_silent(validate_app_title_tag(valid_title_remote)) +}) diff --git a/tests/testthat/test-validate_inputs.R b/tests/testthat/test-validate_inputs.R index bb23cc55fe..333c1b8b0c 100644 --- a/tests/testthat/test-validate_inputs.R +++ b/tests/testthat/test-validate_inputs.R @@ -27,9 +27,9 @@ testthat::test_that("disabled validators raise warnings (individual validators)" } shiny::testServer(server, { - testthat::expect_output( + testthat::expect_warning( object = values(), - regexp = "\\[WARN\\].+Validator is disabled and will be omitted." + regexp = "Validator is disabled and will be omitted." ) }) }) @@ -52,9 +52,9 @@ testthat::test_that("disabled validators raise warnings (validator list)", { } shiny::testServer(server, { - testthat::expect_output( + testthat::expect_warning( object = values(), - regexp = "\\[WARN\\].+Validator is disabled and will be omitted." + regexp = "Validator is disabled and will be omitted." ) }) }) @@ -77,9 +77,9 @@ testthat::test_that("disabled validators raise warnings (nested validator list)" } shiny::testServer(server, { - testthat::expect_output( + testthat::expect_warning( object = values(), - regexp = "\\[WARN\\].+Validator is disabled and will be omitted." + regexp = "Validator is disabled and will be omitted." ) }) }) diff --git a/vignettes/blueprint/intro.Rmd b/vignettes/blueprint/intro.Rmd index dba76cf04e..0537ce52e5 100644 --- a/vignettes/blueprint/intro.Rmd +++ b/vignettes/blueprint/intro.Rmd @@ -29,7 +29,6 @@ The `teal` framework's functionality draws heavily from the following packages: |[`teal.data`](https://insightsengineering.github.io/teal.data) | provides the data structure used in all `teal` applications| |[`teal.slice`](https://insightsengineering.github.io/teal.slice) | provides the filter panel to allow dynamic filtering of data| |[`teal.code`](https://insightsengineering.github.io/teal.code) | provides a mechanism for tracking code to reproduce an analysis| -|[`teal.transform`](https://insightsengineering.github.io/teal.transform) | standardizes extracting and merging data| |[`teal.logger`](https://insightsengineering.github.io/teal.logger) | standardizes logging within `teal` framework| |[`teal.reporter`](https://insightsengineering.github.io/teal.reporter) | allows `teal` applications to generate reports| diff --git a/vignettes/blueprint/product_map.Rmd b/vignettes/blueprint/product_map.Rmd index b411671876..aec0188185 100644 --- a/vignettes/blueprint/product_map.Rmd +++ b/vignettes/blueprint/product_map.Rmd @@ -22,7 +22,6 @@ subgraph features direction LR teal.data teal.slice - teal.transform teal.code teal.logger teal.widgets diff --git a/vignettes/creating-custom-modules.Rmd b/vignettes/creating-custom-modules.Rmd index d9b9ad32c2..26102d6d32 100644 --- a/vignettes/creating-custom-modules.Rmd +++ b/vignettes/creating-custom-modules.Rmd @@ -82,8 +82,6 @@ It is of the `tdata` type and can be created using the `new_tdata` function. The `teal` framework also provides: - A way to create modules which then generate the R code needed to reproduce their outputs; these modules use the [`teal.code`](https://insightsengineering.github.io/teal.code/) package. -- A way extract from and merge related datasets using the [`teal.transform`](https://insightsengineering.github.io/teal.transform/) package. -- A way to allow app creators to customize your modules also using `teal.transform`. The annotated example below demonstrates these features within a simple histogram module, allowing app developers to choose the data and columns that their app users can select for display in a histogram. diff --git a/vignettes/getting-started-with-teal.Rmd b/vignettes/getting-started-with-teal.Rmd index 50873c00c4..3c918f3160 100644 --- a/vignettes/getting-started-with-teal.Rmd +++ b/vignettes/getting-started-with-teal.Rmd @@ -120,4 +120,3 @@ The packages which are of most interest when defining `teal`applications are: * [`teal.data`](https://insightsengineering.github.io/teal.data/): defining data for `teal` application. * [`teal.slice`](https://insightsengineering.github.io/teal.slice/): defining data filtering before passing into `teal` modules. -* [`teal.transform`](https://insightsengineering.github.io/teal.transform/): defining the way arguments are passed into `teal` modules.