From 8695792f471084e43cce1775e4b796df0aa01d03 Mon Sep 17 00:00:00 2001 From: Jason Pickering Date: Mon, 19 Sep 2022 12:58:17 +0200 Subject: [PATCH 01/21] Add helper functions for OAUTH2 authentication (#109) * Add token to d2Session object * Add OAUTH login function * Update NAMESPACE * Documentation and cleanup * Update rocker verse R version * Update lock * Minor * Fix R6 import * Linting * Add lintr to suggests --- .circleci/config.yml | 2 +- DESCRIPTION | 14 ++++--- NAMESPACE | 2 + NEWS.md | 6 +++ R/loginToDATIM.R | 90 +++++++++++++++++++++++++++++++++++++++- man/d2Session.Rd | 21 ++++++---- man/loginToDATIMOAuth.Rd | 56 +++++++++++++++++++++++++ renv.lock | 56 ++++++++----------------- 8 files changed, 191 insertions(+), 56 deletions(-) create mode 100644 man/loginToDATIMOAuth.Rd diff --git a/.circleci/config.yml b/.circleci/config.yml index ea0566da..b72dd2a9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,7 +3,7 @@ description: Datapackr Test Suite jobs: build: docker: - - image: rocker/verse:3.6.3 + - image: rocker/verse:4.1.1 steps: - checkout - restore_cache: diff --git a/DESCRIPTION b/DESCRIPTION index acc9830b..d4ac32ee 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,8 +1,8 @@ Package: datimutils Type: Package Title: Utilities for interacting with the DATIM api from R -Version: 0.5.2 -Date: 2022-08-18 +Version: 0.5.3 +Date: 2022-08-30 Authors@R: c( person("Scott", "Jackson", email = "sjackson@baosystems.com", @@ -21,14 +21,15 @@ Authors@R: Description: General utilities for interacting with the DATIM api from R. URL: https://github.com/pepfar-datim/datimutils BugReports: https://github.com/pepfar-datim/datimutils -Depends: R (>= 3.6.0) +Depends: R (>= 4.1.1) Imports: tidyr (>= 0.8.3), jsonlite (>= 1.6), stringi (>= 1.7.6), httr (>= 1.4.0), keyring, - rlang + rlang, + R6 (>= 2.5.1) Suggests: devtools (>= 2.0.1), dplyr (>= 0.8.3), @@ -38,9 +39,10 @@ Suggests: knitr, rmarkdown, assertthat, - testthat (>= 2.1.0) + testthat (>= 2.1.0), + lintr(>= 3.0.0) License: GPL-3 + file LICENSE Encoding: UTF-8 LazyData: true -RoxygenNote: 7.2.0 +RoxygenNote: 7.2.1 VignetteBuilder: knitr diff --git a/NAMESPACE b/NAMESPACE index 549ac2df..eef31bb2 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -58,6 +58,8 @@ export(getUserGroups) export(listMechs) export(listSqlViews) export(loginToDATIM) +export(loginToDATIMOAuth) export(make_dim) export(make_fil) export(metadataFilter) +import(R6) diff --git a/NEWS.md b/NEWS.md index 6a64a849..daf41a0a 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,9 @@ +# datimutils 0.5.3 + +## New features +* Adds `loginToDATIMOAuth` function to assist with OAUTH2.0 authentication. + + # datimutils 0.5.2 ## Bug fixes diff --git a/R/loginToDATIM.R b/R/loginToDATIM.R index f9b7bfac..ec672f21 100644 --- a/R/loginToDATIM.R +++ b/R/loginToDATIM.R @@ -1,3 +1,5 @@ +#' @import R6 +#' d2Session <- R6::R6Class("d2Session", #' @title d2Session public = list( @@ -17,7 +19,10 @@ d2Session <- R6::R6Class("d2Session", handle = NULL, #' @field me dhis2 api/me response me = NULL, + #' @field max_cache_age Maximum time responses should be cached max_cache_age = NULL, + #' @field token An httr OAUTH2 token + token = NULL, #' @description #' Create a new DHISLogin object #' @param config_path Configuration file path @@ -25,16 +30,19 @@ d2Session <- R6::R6Class("d2Session", #' @param handle httr handle to be used for dhis2 #' connections #' @param me DHIS2 me response object + #' @param token OAUTH2 token initialize = function(config_path = NA_character_, base_url, handle, - me) { + me, + token) { self$config_path <- config_path self$me <- me self$user_orgunit <- me$organisationUnits$id self$base_url <- base_url self$username <- me$userCredentials$username self$handle <- handle + self$token <- token } ) ) @@ -203,7 +211,8 @@ loginToDATIM <- function(config_path = NULL, d2Session$new(config_path = config_path, base_url = base_url, handle = handle, - me = me), + me = me, + token = NULL), envir = d2_session_envir) } else if (r$status == 302L) { stop("Unable to authenticate due to DATIM currently undergoing maintenance. @@ -221,3 +230,80 @@ loginToDATIM <- function(config_path = NULL, stop("An unknowon error has occured during authentication!") } } + + + +#' @title datimutils::loginToDATIMOAuth(base_url = Sys.getenv("BASE_URL"), +#' token = token, +#' app = oauth_app, +#' api = oauth_api, +#' redirect_uri= APP_URL, +#' scope = oauth_scope, +#' d2_session_envir = parent.env(environment())) +#' +#' @param base_url URL of the DHIS2 server +#' @param token An OAUTH2.0 token object. Will be created if not supplied. +#' @param redirect_uri The redirect URI which should be used after +#' successful authentication with the server. +#' @param app An httr OAUTH app object. +#' @param api An hjttr OAUTH endpoint. +#' @param scope A character vector of scopes which should be requested. +#' @param d2_session_name the variable name for the d2Session object. +#' The default name is d2_default_session and will be used by other datimutils +#' functions by default when connecting to datim. Generally a custom name +#' should only be needed if you need to log into two seperate DHIS2 instances +#' at the same time. If you create a d2Session object with a custom name then +#' this object must be passed to other datimutils functions explicitly +#' @param d2_session_envir the environment in which to place the R6 login +#' object, default is the immediate calling environment +#' +#' @export +#' + +loginToDATIMOAuth <- function( + base_url = NULL, + token = NULL, + redirect_uri = NULL, + app = NULL, + api = NULL, + scope = NULL, + d2_session_name = "d2_default_session", + d2_session_envir = parent.frame()) { + + if (is.null(token)) { + token <- httr::oauth2.0_token( + app = app, + endpoint = api, + scope = scope, + use_basic_auth = TRUE, + oob_value = redirect_uri, + cache = FALSE + ) + } else { + token <- token #For Shiny + } + + # form url + url <- utils::URLencode(URL = paste0(base_url, "api", "/me")) + handle <- httr::handle(base_url) + #Get Request + r <- httr::GET( + url, + httr::config(token = token), + httr::timeout(60), + handle = handle + ) + + if (r$status_code != 200L) { + stop("Could not authenticate you with the server!") + } else { + me <- jsonlite::fromJSON(httr::content(r, as = "text")) + # create the session object in the calling environment of the login function + assign(d2_session_name, + d2Session$new(base_url = base_url, + handle = handle, + me = me, + token = token), + envir = d2_session_envir) + } +} diff --git a/man/d2Session.Rd b/man/d2Session.Rd index d40a8167..ef92b077 100644 --- a/man/d2Session.Rd +++ b/man/d2Session.Rd @@ -26,23 +26,27 @@ organisation unit} with the DHIS2 instance.} \item{\code{me}}{dhis2 api/me response} + +\item{\code{max_cache_age}}{Maximum time responses should be cached} + +\item{\code{token}}{An httr OAUTH2 token} } \if{html}{\out{}} } \section{Methods}{ \subsection{Public methods}{ \itemize{ -\item \href{#method-new}{\code{d2Session$new()}} -\item \href{#method-clone}{\code{d2Session$clone()}} +\item \href{#method-d2Session-new}{\code{d2Session$new()}} +\item \href{#method-d2Session-clone}{\code{d2Session$clone()}} } } \if{html}{\out{
}} -\if{html}{\out{}} -\if{latex}{\out{\hypertarget{method-new}{}}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-d2Session-new}{}}} \subsection{Method \code{new()}}{ Create a new DHISLogin object \subsection{Usage}{ -\if{html}{\out{
}}\preformatted{d2Session$new(config_path = NA_character_, base_url, handle, me)}\if{html}{\out{
}} +\if{html}{\out{
}}\preformatted{d2Session$new(config_path = NA_character_, base_url, handle, me, token)}\if{html}{\out{
}} } \subsection{Arguments}{ @@ -57,15 +61,14 @@ connections} \item{\code{me}}{DHIS2 me response object} -\item{\code{max_cache_age}}{cache expiry currently used -by datim validation} +\item{\code{token}}{OAUTH2 token} } \if{html}{\out{}} } } \if{html}{\out{
}} -\if{html}{\out{}} -\if{latex}{\out{\hypertarget{method-clone}{}}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-d2Session-clone}{}}} \subsection{Method \code{clone()}}{ The objects of this class are cloneable with this method. \subsection{Usage}{ diff --git a/man/loginToDATIMOAuth.Rd b/man/loginToDATIMOAuth.Rd new file mode 100644 index 00000000..6e72da09 --- /dev/null +++ b/man/loginToDATIMOAuth.Rd @@ -0,0 +1,56 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/loginToDATIM.R +\name{loginToDATIMOAuth} +\alias{loginToDATIMOAuth} +\title{datimutils::loginToDATIMOAuth(base_url = Sys.getenv("BASE_URL"), +token = token, +app = oauth_app, +api = oauth_api, +redirect_uri= APP_URL, +scope = oauth_scope, +d2_session_envir = parent.env(environment()))} +\usage{ +loginToDATIMOAuth( + base_url = NULL, + token = NULL, + redirect_uri = NULL, + app = NULL, + api = NULL, + scope = NULL, + d2_session_name = "d2_default_session", + d2_session_envir = parent.frame() +) +} +\arguments{ +\item{base_url}{URL of the DHIS2 server} + +\item{token}{An OAUTH2.0 token object. Will be created if not supplied.} + +\item{redirect_uri}{The redirect URI which should be used after +successful authentication with the server.} + +\item{app}{An httr OAUTH app object.} + +\item{api}{An hjttr OAUTH endpoint.} + +\item{scope}{A character vector of scopes which should be requested.} + +\item{d2_session_name}{the variable name for the d2Session object. +The default name is d2_default_session and will be used by other datimutils +functions by default when connecting to datim. Generally a custom name +should only be needed if you need to log into two seperate DHIS2 instances +at the same time. If you create a d2Session object with a custom name then +this object must be passed to other datimutils functions explicitly} + +\item{d2_session_envir}{the environment in which to place the R6 login +object, default is the immediate calling environment} +} +\description{ +datimutils::loginToDATIMOAuth(base_url = Sys.getenv("BASE_URL"), +token = token, +app = oauth_app, +api = oauth_api, +redirect_uri= APP_URL, +scope = oauth_scope, +d2_session_envir = parent.env(environment())) +} diff --git a/renv.lock b/renv.lock index a3e75fdd..ae10ec8f 100644 --- a/renv.lock +++ b/renv.lock @@ -1,6 +1,6 @@ { "R": { - "Version": "3.6.3", + "Version": "4.2.1", "Repositories": [ { "Name": "CRAN", @@ -13,18 +13,10 @@ "Package": "R6", "Version": "2.5.1", "Source": "Repository", - "Repository": "RSPM", + "Repository": "CRAN", "Hash": "470851b6d5d0ac559e9d01bb352b4021", "Requirements": [] }, - "Rcpp": { - "Package": "Rcpp", - "Version": "1.0.7", - "Source": "Repository", - "Repository": "RSPM", - "Hash": "dab19adae4440ae55aa8a9d238b246bb", - "Requirements": [] - }, "askpass": { "Package": "askpass", "Version": "1.1", @@ -45,10 +37,10 @@ }, "backports": { "Package": "backports", - "Version": "1.2.1", + "Version": "1.4.1", "Source": "Repository", "Repository": "CRAN", - "Hash": "644043219fc24e190c2f620c1a380a69", + "Hash": "c39fbec8a30d23e721980b8afb31984c", "Requirements": [] }, "base64enc": { @@ -111,7 +103,7 @@ "Package": "cachem", "Version": "1.0.6", "Source": "Repository", - "Repository": "RSPM", + "Repository": "CRAN", "Hash": "648c5b3d71e6a37e3043617489a0a0e9", "Requirements": [ "fastmap", @@ -231,7 +223,7 @@ "Package": "devtools", "Version": "2.4.3", "Source": "Repository", - "Repository": "RSPM", + "Repository": "CRAN", "Hash": "fc35e13bb582e5fe6f63f3d647a4cbe5", "Requirements": [ "callr", @@ -323,7 +315,7 @@ "Package": "fastmap", "Version": "1.1.0", "Source": "Repository", - "Repository": "RSPM", + "Repository": "CRAN", "Hash": "77bd60a6157420d4ffa93b27cf6a58b8", "Requirements": [] }, @@ -331,7 +323,7 @@ "Package": "filelock", "Version": "1.0.2", "Source": "Repository", - "Repository": "RSPM", + "Repository": "CRAN", "Hash": "38ec653c2613bed60052ba3787bd8a2c", "Requirements": [] }, @@ -384,7 +376,7 @@ "Package": "gitcreds", "Version": "0.1.1", "Source": "Repository", - "Repository": "RSPM", + "Repository": "CRAN", "Hash": "f3aefccc1cc50de6338146b62f115de8", "Requirements": [] }, @@ -424,7 +416,7 @@ "Package": "htmltools", "Version": "0.5.2", "Source": "Repository", - "Repository": "RSPM", + "Repository": "CRAN", "Hash": "526c484233f42522278ab06fb185cb26", "Requirements": [ "base64enc", @@ -465,7 +457,7 @@ "Package": "ini", "Version": "0.3.1", "Source": "Repository", - "Repository": "RSPM", + "Repository": "CRAN", "Hash": "6154ec2223172bce8162d4153cda21f7", "Requirements": [] }, @@ -666,7 +658,7 @@ "Package": "prettyunits", "Version": "1.1.1", "Source": "Repository", - "Repository": "RSPM", + "Repository": "CRAN", "Hash": "95ef9167b75dde9d2ccc3c7528393e7e", "Requirements": [] }, @@ -713,18 +705,6 @@ "rlang" ] }, - "qpdf": { - "Package": "qpdf", - "Version": "1.1", - "Source": "Repository", - "Repository": "CRAN", - "Hash": "5a2907cd44279b5861ac5f813465b5c9", - "Requirements": [ - "Rcpp", - "askpass", - "curl" - ] - }, "rappdirs": { "Package": "rappdirs", "Version": "0.3.3", @@ -839,10 +819,10 @@ }, "roxygen2": { "Package": "roxygen2", - "Version": "7.2.0", + "Version": "7.2.1", "Source": "Repository", "Repository": "CRAN", - "Hash": "b390c1d54fcd977cda48588e6172daba", + "Hash": "da1f278262e563c835345872f2fef537", "Requirements": [ "R6", "brew", @@ -873,7 +853,7 @@ "Package": "rstudioapi", "Version": "0.13", "Source": "Repository", - "Repository": "RSPM", + "Repository": "CRAN", "Hash": "06c85365a03fdaf699966cc1d3cf53ea", "Requirements": [] }, @@ -944,7 +924,7 @@ "Package": "sys", "Version": "3.4", "Source": "Repository", - "Repository": "RSPM", + "Repository": "CRAN", "Hash": "b227d13e29222b4574486cfcbde077fa", "Requirements": [] }, @@ -1138,7 +1118,7 @@ "Package": "whisker", "Version": "0.4", "Source": "Repository", - "Repository": "RSPM", + "Repository": "CRAN", "Hash": "ca970b96d894e90397ed20637a0c1bbe", "Requirements": [] }, @@ -1196,7 +1176,7 @@ "Package": "zip", "Version": "2.2.0", "Source": "Repository", - "Repository": "RSPM", + "Repository": "CRAN", "Hash": "c7eef2996ac270a18c2715c997a727c5", "Requirements": [] } From d39e4c4cbc80cdb14d90f672849a02183de4d1d4 Mon Sep 17 00:00:00 2001 From: Jason Pickering Date: Mon, 19 Sep 2022 13:00:46 +0200 Subject: [PATCH 02/21] Update DESCRIPTION --- DESCRIPTION | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index b67f84dc..48e6946e 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,8 +1,8 @@ Package: datimutils Type: Package Title: Utilities for interacting with the DATIM api from R -Version: 0.5.3 -Date: 2022-09-08 +Version: 0.5.4 +Date: 2022-09-19 Authors@R: c( From efef8085a21fea02f756276e93d30222f898aead Mon Sep 17 00:00:00 2001 From: "Jason P. Pickering" Date: Mon, 19 Sep 2022 13:14:03 +0200 Subject: [PATCH 03/21] Fix DESCRIPTION --- DESCRIPTION | 1 - 1 file changed, 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index 48e6946e..2c4b124d 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -3,7 +3,6 @@ Type: Package Title: Utilities for interacting with the DATIM api from R Version: 0.5.4 Date: 2022-09-19 - Authors@R: c( person("Scott", "Jackson", email = "sjackson@baosystems.com", From 0cebf8d01952fb5f26d5e68026682a3cba1a816c Mon Sep 17 00:00:00 2001 From: "Jason P. Pickering" Date: Mon, 19 Sep 2022 14:22:21 +0200 Subject: [PATCH 04/21] Bump version to 0.6.0 --- DESCRIPTION | 2 +- NEWS.md | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 2c4b124d..60055a67 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: datimutils Type: Package Title: Utilities for interacting with the DATIM api from R -Version: 0.5.4 +Version: 0.6.0 Date: 2022-09-19 Authors@R: c( diff --git a/NEWS.md b/NEWS.md index 4a3b0753..5e7f6420 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,4 @@ -# datimutils 0.5.4 - +# datimutils 0.6.0 ## New features * Adds `loginToDATIMOAuth` function to assist with OAUTH2.0 authentication. From 7dad2fccd3df62d9da79ca772fd57e80bfd63263 Mon Sep 17 00:00:00 2001 From: Jordan Date: Fri, 14 Oct 2022 14:52:40 -0400 Subject: [PATCH 05/21] Documentation at a stopping point until I get the app launch function going. --- vignettes/Introduction-to-datimutils.Rmd | 25 +++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/vignettes/Introduction-to-datimutils.Rmd b/vignettes/Introduction-to-datimutils.Rmd index 6d6568be..9d805eee 100644 --- a/vignettes/Introduction-to-datimutils.Rmd +++ b/vignettes/Introduction-to-datimutils.Rmd @@ -39,9 +39,9 @@ httptest::start_vignette("play.dhis2.org") ## Logging into DATIM -Before querying the DATIM api, the user must log into a DATIM instance. There are three basic ways to accomplish this with datimutils. All three methods utilize the `loginToDATIM` function. NOTE this function can log into any DHIS2 instance, not just DATIM, based on the provided baseurl. +Before querying the DATIM api, the user must log into a DATIM instance. There are four basic ways to accomplish this with datimutils. Three methods utilize the `loginToDATIM` function and the final method utilizes `loginToDATIMOAuth` . NOTE these two functions can log into any DHIS2 instance, not just DATIM, based on the provided baseurl. -By default the `loginToDATIM` function will create an R object containing details about the session. This object is named *d2_default_session*, and it is created in the environment from which the `loginToDATIM` function is called. In this vignette we are assuming the user is logging in from the global environment. All `datimutils` functions will search for the d2_default_session by default, thus enabling the user to make multiple DATIM api requests through `datimutils` without individually specifying unique login objects with each request. +By default the `loginToDATIM` and `loginToDATIMOAuth` function will create an R object containing details about the session. This object is named *d2_default_session*, and it is created in the environment from which the `loginToDATIM` or `loginToDATIMOAuth`function is called. In this vignette we are assuming the user is logging in from the global environment. All `datimutils` functions will search for the d2_default_session by default, thus enabling the user to make multiple DATIM api requests through `datimutils` without individually specifying unique login objects with each request. > Note: By default, this DHIS2 session will expire after 60 minutes. When it does, the user will need to re-authenticate @@ -59,7 +59,7 @@ loginToDATIM( ### Methods 2 and 3 - Configuration Files -The other two log in methods require the user to create a reusable configuration file in the format below. The location of this configuration file on the users's machine will be passed to the `loginToDATIM` function. If a user wants the ability to quickly log into multiple different servers, they can have a config file for each server instance. +The next two log in methods require the user to create a reusable configuration file in the format below. The location of this configuration file on the users's machine will be passed to the `loginToDATIM` function. If a user wants the ability to quickly log into multiple different servers, they can have a config file for each server instance. In this vignette, the example config file is part of the package, *play.json*. @@ -91,7 +91,7 @@ Alternatively, the the entry for password can be blank such that the file resemb The function will look in the users keyring/credential store for the aforementioned password, if not found, the user will be prompted to enter a password that will store automatically under that username and service (baseurl) for the next log in. A user may prefer storing their passwords in their operating systems credential store due to the fact it enables them to avoid storing this data in a plain text fomat on their computer, which is a potential security risk. ->Note: keyrings operate differently on different operating systems +> Note: keyrings operate differently on different operating systems In both methods, call the `loginToDATIM` function as so: \> Note: replace `play.json` with the path of your config file @@ -100,6 +100,16 @@ In both methods, call the `loginToDATIM` function as so: \> Note: replace `play. loginToDATIM(config_path = "play.json") ``` +### Method 4 - OAuth + +The final log in method is `loginToDATIMOAuth`. It uses DATIM, or your chosen DHIS2 instance, to authenticate via OAuth. This function will send the user to the chosen instance based upon the baseurl provided in order to retrieve an authentication code. + +This function is useful for Shiny web applications as the process of exchanging the code for a token is automated. While not recommended it can be used with the command line, however the code found after logging in will have to be manually entered when prompted. This code can be found at the end of the url in your browser. + +It is important to note that an Oauth client will need to be configured in your chosen DHIS2 instance in order to utilize this functionality. Depending upon your access in the aforementioned DHIS2 instance you can do this by logging in \> navigating to "System Settings" in the menu \> selecting "OAuth2 Clients" on the left \> clicking the blue "+" button. If you do not see these options please contact your system admin. You will need the name, client ID, Client Secret, and Redirect URIs to configure your Shiny application. + +An example app can be found inside this package by running enter command here. + ## Features for working with DATIM/DHIS2 metadata A DHIS2 configuration such as DATIM includes many different types of metadata; such as data elements, indicators, and organisation units. The DHIS2 web API allows a user to look up these pieces of metadata and obtain their related properties. For instance, a property of an organisation unit is its path, or the hierarchy or organization units above the organisation unit. @@ -119,7 +129,7 @@ print(data) ### Metadata helpers when a user has a vector of identifiers (e.g. uids or names) -`Datimutils` provides a number of high level metadata helpers for obtaining details (properties) of the different types of metadata. These helpers are designed assuming the user has a vector of identifiers for a particular type of metadata (e.g. uids) and wishes to look up one or more other properties for each element of the vector. There is a helper for each of the main metadata categories in DHIS2, in fact auto complete in the users R IDE will often make typing these very fast. This is the list of the numerous high level metadata helpers in `datimutils`. +`Datimutils` provides a number of high level metadata helpers for obtaining details (properties) of the different types of metadata. These helpers are designed assuming the user has a vector of identifiers for a particular type of metadata (e.g. uids) and wishes to look up one or more other properties for each element of the vector. There is a helper for each of the main metadata categories in DHIS2, in fact auto complete in the users R IDE will often make typing these very fast. This is the list of the numerous high level metadata helpers in `datimutils`. ```{r table1, echo=FALSE, message=FALSE, warnings=FALSE, results='asis'} tabl <- " @@ -250,6 +260,7 @@ head(data) ``` Though the return of these commands includes list columns which may require further processing to be used in an application, `datimutils` does an amazing job at organizing and presenting the queried data when compared to the query's raw JSON which might look like the example below with its own nested elements : + ```{=html} { "organisationUnits": [ @@ -365,9 +376,8 @@ Though the return of these commands includes list columns which may require furt ] } ``` - - In the case of returning a single column of data, `getMetadata` will try to return a vector as opposed to a data frame: + ```{r} # returns a vector data <- getMetadata( @@ -378,6 +388,7 @@ print(data) ``` many different types of calls can be made, below are two filters passed in as a vector. This call returns a nested data frame + ```{r} data <- getMetadata( end_point = "organisationUnits", From 43b8d194f524133c16f9bba92fd2a884cc80f908 Mon Sep 17 00:00:00 2001 From: Jordan Date: Fri, 14 Oct 2022 15:17:53 -0400 Subject: [PATCH 06/21] Adding example apps folder --- inst/shiny-examples/OAuth/server.R | 26 +++++++++++++++++++++++ inst/shiny-examples/OAuth/ui.R | 33 ++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 inst/shiny-examples/OAuth/server.R create mode 100644 inst/shiny-examples/OAuth/ui.R diff --git a/inst/shiny-examples/OAuth/server.R b/inst/shiny-examples/OAuth/server.R new file mode 100644 index 00000000..7720bb30 --- /dev/null +++ b/inst/shiny-examples/OAuth/server.R @@ -0,0 +1,26 @@ +# +# This is the server logic of a Shiny web application. You can run the +# application by clicking 'Run App' above. +# +# Find out more about building applications with Shiny here: +# +# http://shiny.rstudio.com/ +# + +library(shiny) + +# Define server logic required to draw a histogram +shinyServer(function(input, output) { + + output$distPlot <- renderPlot({ + + # generate bins based on input$bins from ui.R + x <- faithful[, 2] + bins <- seq(min(x), max(x), length.out = input$bins + 1) + + # draw the histogram with the specified number of bins + hist(x, breaks = bins, col = 'darkgray', border = 'white') + + }) + +}) diff --git a/inst/shiny-examples/OAuth/ui.R b/inst/shiny-examples/OAuth/ui.R new file mode 100644 index 00000000..5dd98cf8 --- /dev/null +++ b/inst/shiny-examples/OAuth/ui.R @@ -0,0 +1,33 @@ +# +# This is the user-interface definition of a Shiny web application. You can +# run the application by clicking 'Run App' above. +# +# Find out more about building applications with Shiny here: +# +# http://shiny.rstudio.com/ +# + +library(shiny) + +# Define UI for application that draws a histogram +shinyUI(fluidPage( + + # Application title + titlePanel("Old Faithful Geyser Data"), + + # Sidebar with a slider input for number of bins + sidebarLayout( + sidebarPanel( + sliderInput("bins", + "Number of bins:", + min = 1, + max = 50, + value = 30) + ), + + # Show a plot of the generated distribution + mainPanel( + plotOutput("distPlot") + ) + ) +)) From 378db40181a249225db6fae9ff07274309c286fc Mon Sep 17 00:00:00 2001 From: Jordan Date: Fri, 14 Oct 2022 16:00:48 -0400 Subject: [PATCH 07/21] Adding Shiny as a suggested package --- DESCRIPTION | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index 60055a67..0839d2c0 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -40,7 +40,8 @@ Suggests: rmarkdown, assertthat, testthat (>= 2.1.0), - lintr(>= 3.0.0) + lintr (>= 3.0.0), + shiny License: GPL-3 + file LICENSE Encoding: UTF-8 LazyData: true From 07381add6dd16321249efbcff05cf883eb7c2acc Mon Sep 17 00:00:00 2001 From: Jordan Date: Fri, 14 Oct 2022 16:01:14 -0400 Subject: [PATCH 08/21] Replicated Datapackr front end code --- inst/shiny-examples/OAuth/ui.R | 34 +++------------------------------- 1 file changed, 3 insertions(+), 31 deletions(-) diff --git a/inst/shiny-examples/OAuth/ui.R b/inst/shiny-examples/OAuth/ui.R index 5dd98cf8..8fb362ec 100644 --- a/inst/shiny-examples/OAuth/ui.R +++ b/inst/shiny-examples/OAuth/ui.R @@ -1,33 +1,5 @@ -# -# This is the user-interface definition of a Shiny web application. You can -# run the application by clicking 'Run App' above. -# -# Find out more about building applications with Shiny here: -# -# http://shiny.rstudio.com/ -# - library(shiny) -# Define UI for application that draws a histogram -shinyUI(fluidPage( - - # Application title - titlePanel("Old Faithful Geyser Data"), - - # Sidebar with a slider input for number of bins - sidebarLayout( - sidebarPanel( - sliderInput("bins", - "Number of bins:", - min = 1, - max = 50, - value = 30) - ), - - # Show a plot of the generated distribution - mainPanel( - plotOutput("distPlot") - ) - ) -)) +shinyUI( + uiOutput("ui") +) From 4e39a739fdfe15068750c9861baeef21a39f968a Mon Sep 17 00:00:00 2001 From: Jordan Date: Fri, 14 Oct 2022 16:04:09 -0400 Subject: [PATCH 09/21] Function to view these example shiny apps in the package. NOTE: Currenlty hard coded to my directories. --- R/runExample.R | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 R/runExample.R diff --git a/R/runExample.R b/R/runExample.R new file mode 100644 index 00000000..5396c85c --- /dev/null +++ b/R/runExample.R @@ -0,0 +1,30 @@ +runExample <- function(example) { + # locate all the shiny app examples that exist + #validExamples <- list.files(system.file("shiny-examples", package = "datimutils")) + validExamples <- list.files("~/Documents/Repos/datimutils/inst/shiny-examples") + + validExamplesMsg <- + paste0( + "Valid examples are: '", + paste(validExamples, collapse = "', '"), + "'") + + # if an invalid example is given, throw an error + if (missing(example) || !nzchar(example) || + !example %in% validExamples) { + stop( + 'Please run `runExample()` with a valid example app as an argument.\n', + validExamplesMsg, + call. = FALSE) + } + + # find and launch the app + #appDir <- system.file("shiny-examples", example, package = "datimutils") + appDir <- "~/Documents/Repos/datimutils/inst/shiny-examples/OAuth" + shiny::runApp(appDir, display.mode = "normal") +} + + + +# • In your package code, use `rlang::is_installed("shiny")` or `rlang::check_installed("shiny")` to test if shiny is installed +# • Then directly refer to functions with `shiny::fun()` \ No newline at end of file From 452d280fa12d7f3cd2d6a9599f48ce3e1350d043 Mon Sep 17 00:00:00 2001 From: Jordan Date: Mon, 17 Oct 2022 08:33:20 -0400 Subject: [PATCH 10/21] committing due to need to restart machine. This is bettere represented in Datapackr app --- inst/shiny-examples/OAuth/server.R | 362 +++++++++++++++++++++++++++-- 1 file changed, 345 insertions(+), 17 deletions(-) diff --git a/inst/shiny-examples/OAuth/server.R b/inst/shiny-examples/OAuth/server.R index 7720bb30..fb08cb62 100644 --- a/inst/shiny-examples/OAuth/server.R +++ b/inst/shiny-examples/OAuth/server.R @@ -1,26 +1,354 @@ -# -# This is the server logic of a Shiny web application. You can run the -# application by clicking 'Run App' above. -# -# Find out more about building applications with Shiny here: -# -# http://shiny.rstudio.com/ -# +pacman::p_load(shiny, shinyjs, shinyWidgets, magrittr, dplyr, + datimvalidation, ggplot2, datimutils, + futile.logger, paws, datapackr, scales, + DT, purrr, rpivotTable, waiter, + flextable, officer, gdtools, digest, fansi) -library(shiny) +# js ---- +# allows for using the enter button +jscode_login <- '$(document).keyup(function(e) { + var focusedElement = document.activeElement.id; + console.log(focusedElement); + if (e.key == "Enter" && focusedElement == "user_name") { + $("#password").focus(); + } else if (e.key == "Enter" && focusedElement == "password") { + $("#login_button").click(); + } +});' -# Define server logic required to draw a histogram -shinyServer(function(input, output) { +#Set the maximum file size for the upload file +options(shiny.maxRequestSize = 150 * 1024 ^ 2) +#Allow unsanitized error messages +options(shiny.sanitize.errors = FALSE) +#Initiate logging +logger <- flog.logger() - output$distPlot <- renderPlot({ +if (!file.exists(Sys.getenv("LOG_PATH"))) { + file.create(Sys.getenv("LOG_PATH")) +} - # generate bins based on input$bins from ui.R - x <- faithful[, 2] - bins <- seq(min(x), max(x), length.out = input$bins + 1) +flog.appender(appender.console(), name = "datapack") - # draw the histogram with the specified number of bins - hist(x, breaks = bins, col = 'darkgray', border = 'white') +################ OAuth Client information ##################################### +if (interactive()) { + # testing url + options(shiny.port = 3123) + APP_URL <- "http://127.0.0.1:3123/"# This will be your local host path +} else { + # deployed URL + APP_URL <- Sys.getenv("APP_URL") #This will be your shiny server path +} +{ + + oauth_app <- httr::oauth_app(Sys.getenv("OAUTH_APPNAME"), + key = Sys.getenv("OAUTH_KEYNAME"), # dhis2 = Client ID + secret = Sys.getenv("OAUTH_SECRET"), #dhis2 = Client Secret + redirect_uri = APP_URL + ) + + oauth_api <- httr::oauth_endpoint(base_url = paste0(Sys.getenv("BASE_URL"),"uaa/oauth"), + request=NULL,# Documentation says to leave this NULL for OAuth2 + authorize = "authorize", + access="token" + ) + + oauth_scope <- "ALL" +} + +has_auth_code <- function(params) { + + return(!is.null(params$code)) +} + + + +shinyServer(function(input, output, session) { + + validation_results <- reactive({ validate() }) # nolint + + ready <- reactiveValues(ok = FALSE) + + + user_input <- reactiveValues(authenticated = FALSE, + status = "", + d2_session = NULL, + memo_authorized = FALSE, + uuid = NULL) + + + observeEvent(input$file1, { + shinyjs::show("validate") + shinyjs::enable("validate") + ready$ok <- FALSE }) + + observeEvent(input$validate, { + shinyjs::disable("file1") + shinyjs::disable("validate") + ready$ok <- TRUE + }) + + observeEvent(input$reset_input, { + shinyjs::reset("side-panel") + shinyjs::enable("file1") + shinyjs::disable("validate") + shinyjs::disable("downloadDataPack") + shinyjs::disable("download_messages") + shinyjs::disable("send_paw") + shinyjs::disable("downloadValidationResults") + shinyjs::disable("compare") + ready$ok <- FALSE + }) + + + + + + observeEvent(input$logout, { + req(input$logout) + # Gets you back to the login without the authorization code at top + updateQueryString("?",mode="replace",session=session) + flog.info(paste0("User ", user_input$d2_session$me$userCredentials$username, " logged out.")) + ready$ok <- FALSE + user_input$authenticated <- FALSE + user_input$user_name <- "" + user_input$authorized <- FALSE + user_input$d2_session <- NULL + d2_default_session <- NULL + gc() + session$reload() + + }) + + output$ui <- renderUI({ + + if (user_input$authenticated == FALSE) { + ##### UI code for login page + fluidPage( + fluidRow( + column(width = 2, offset = 5, + br(), br(), br(), br(), + uiOutput("uiLogin") + ) + ) + ) + } else { + uiOutput("authenticated") + } + }) + + + # Username and password text fields, login button + output$uiLogin <- renderUI({ + + wellPanel(fluidRow( + #img(src = "pepfar.png", align = "center"), + tags$head(tags$script(HTML(jscode_login))), # enter button functionality for login button + tags$div(HTML('
')), + h4("Welcome to the DataPack Validation App. Please login with your DATIM credentials:") + ), + fluidRow( + actionButton("login_button_oauth","Log in with DATIM"), + uiOutput("ui_hasauth"), + uiOutput("ui_redirect") + ), + fluidRow( + tags$hr(), + tags$div(HTML("
  • Please be sure you fully populate the PSNUxIM", + " tab when receiving a new DataPack. Consult the user guide for further information!", + "

  • See the latest updates to the app here.

")) + ), + tags$hr(), + fluidRow(HTML(getVersionInfo()))) + }) + + output$authenticated <- renderUI({ + wiki_url <- a("Datapack User Guide", + href = "https://apps.datim.org/datapack-userguide/", + target = "_blank") + + fluidPage( + tags$head(tags$style(".shiny-notification { + position: fixed; + top: 10%; + left: 33%; + right: 33%;}")), + use_waiter(), + sidebarLayout( + sidebarPanel( + shinyjs::useShinyjs(), + id = "side-panel", + tagList(wiki_url), + tags$hr(), + fileInput( + "file1", + "Choose DataPack (Must be XLSX!):", + accept = c("application/xlsx", + ".xlsx"), + width = "240px" + ), + actionButton("validate", "Validate"), + tags$hr(), + selectInput("downloadType", "Download type", NULL), + downloadButton("downloadOutputs", "Download"), + tags$hr(), + actionButton("send_paw", "Send to PAW"), + tags$hr(), + div(style = "display: inline-block; vertical-align:top; width: 80 px;", + actionButton("reset_input", "Reset inputs")), + div(style = "display: inline-block; vertical-align:top; width: 80 px;", + actionButton("logout", "Logout")) + ), + + mainPanel(tabsetPanel( + id = "main-panel", + type = "tabs", + tabPanel("Messages", tags$ul(uiOutput("messages"))), + tabPanel("Analytics checks", tags$div(uiOutput("analytics_checks"))), + tabPanel("Indicator summary", dataTableOutput("indicator_summary"), + tags$h4("Data source: Main DataPack tabs")), + tabPanel("SNU-level summary", + dataTableOutput("snu_summary"), + tags$h4("Data source: Main DataPack tabs")), + tabPanel("Validation rules", + dataTableOutput("vr_rules"), + tags$h4("Data source: PSNUxIM tab")), + tabPanel("HTS Summary Chart", + fluidRow(column(width = 12, div(style = "height:700px", plotOutput("modality_summary")))), + fluidRow(column(width = 12, tags$h4("Data source: PSNUxIM tab")))), + tabPanel("HTS Summary Table", + dataTableOutput("modality_table"), + tags$h4("Data source: PSNUxIM tab")), + tabPanel("HTS Yield", + fluidRow(column(width = 12, div(style = "height:700px", plotOutput("modality_yield")))), + fluidRow(tags$h4("Data source: PSNUxIM tab"))), + tabPanel("HTS Recency", + dataTableOutput("hts_recency"), + tags$h4("Data source: PSNUxIM tab")), + tabPanel("VLS Testing", + fluidRow(column(width = 12, div(style = "height:700px", plotOutput("vls_summary")))), + fluidRow(column(width = 12, tags$h4("Data source: PSNUxIM tab")))), + tabPanel("Epi Cascade Pyramid", + pickerInput("epiCascadeInput", "SNU1", + choices = "", + options = list(`actions-box` = TRUE), multiple = T), + fluidRow(column(width = 12, div(style = "height:700px", plotOutput("epi_cascade")))), + fluidRow(tags$h4("Data source: SUBNATT/IMPATT data & PSNUxIM tab"))), + tabPanel("KP Cascade Pyramid", + pickerInput("kpCascadeInput", "SNU1", + choices = "", + options = list(`actions-box` = TRUE), multiple = T), + fluidRow(column(width = 12, div(style = "height:700px", plotOutput("kp_cascade")))), + fluidRow(tags$h4("Data source: Data source: SUBNATT/IMPATT data & PSNUxIM tab"))), + tabPanel("PSNUxIM Pivot", + fluidRow(column(width = 12, div(rpivotTable::rpivotTableOutput({"pivot"})))), # nolint + fluidRow(tags$h4("Data source: PSNUxIM tab"))), + tabPanel( + "Memo Tables", + fluidRow( + pickerInput( + "memo_pivot_style", + label = "Table Style", + choices = c("Prioritization", "By Agency", "By Partner", "Comparison"), + selected = "Prioritization" + ) + ), + fluidRow(column(width = 12, + div(dataTableOutput({"memo_compare"})))), # nolint + fluidRow(tags$h4("Data source: PSNUxIM tab & DATIM"))) + + )) + )) + }) + + output$ui_redirect = renderUI({ + #print(input$login_button_oauth) useful for debugging + if(!is.null(input$login_button_oauth)){ + if(input$login_button_oauth>0){ + url <- httr::oauth2.0_authorize_url(oauth_api, oauth_app, scope = oauth_scope) + redirect <- sprintf("location.replace(\"%s\");", url) + tags$script(HTML(redirect)) + } else NULL + } else NULL + }) + + ### Login Button oauth Checks + observeEvent(input$login_button_oauth > 0,{ + + #Grabs the code from the url + params <- parseQueryString(session$clientData$url_search) + #Wait until the auth code actually exists + req(has_auth_code(params)) + + #Manually create a token + token <- httr::oauth2.0_token( + app = oauth_app, + endpoint = oauth_api, + scope = oauth_scope, + use_basic_auth = TRUE, + oob_value = APP_URL, + cache = FALSE, + credentials = httr::oauth2.0_access_token(endpoint = oauth_api, + app = oauth_app, + code = params$code, + use_basic_auth = TRUE) + ) + + loginAttempt <- tryCatch({ + user_input$uuid <- uuid::UUIDgenerate() + datimutils::loginToDATIMOAuth(base_url = Sys.getenv("BASE_URL"), + token = token, + app = oauth_app, + api = oauth_api, + redirect_uri= APP_URL, + scope = oauth_scope, + d2_session_envir = parent.env(environment()) + ) }, + # This function throws an error if the login is not successful + error = function(e) { + flog.info(paste0("User ", input$user_name, " login failed. ", e$message), name = "datapack") + } + ) + + if (exists("d2_default_session")) { + + user_input$authenticated <- TRUE + user_input$d2_session <- d2_default_session$clone() + d2_default_session <- NULL + + #Need to check the user is a member of the PRIME Data Systems Group, COP Memo group, or a super user + user_input$memo_authorized <- + grepl("VDEqY8YeCEk|ezh8nmc4JbX", user_input$d2_session$me$userGroups) | + grepl( + "jtzbVV4ZmdP", + user_input$d2_session$me$userCredentials$userRoles + ) + flog.info( + paste0( + "User ", + user_input$d2_session$me$userCredentials$username, + " logged in." + ), + name = "datapack" + ) + sendEventToS3(NULL, "LOGIN", user_input = user_input) + + flog.info( + paste0( + "User ", + user_input$d2_session$me$userCredentials$username, + " logged in." + ), + name = "datapack" + ) + } + + }) + }) From 5c47f5e381c78600f61123c7153cf44241745099 Mon Sep 17 00:00:00 2001 From: Jordan Date: Thu, 20 Oct 2022 11:56:21 -0400 Subject: [PATCH 11/21] app gutted for demo purposes --- inst/shiny-examples/OAuth/server.R | 241 ++++++++--------------------- 1 file changed, 61 insertions(+), 180 deletions(-) diff --git a/inst/shiny-examples/OAuth/server.R b/inst/shiny-examples/OAuth/server.R index fb08cb62..fbb90543 100644 --- a/inst/shiny-examples/OAuth/server.R +++ b/inst/shiny-examples/OAuth/server.R @@ -1,11 +1,18 @@ -pacman::p_load(shiny, shinyjs, shinyWidgets, magrittr, dplyr, - datimvalidation, ggplot2, datimutils, - futile.logger, paws, datapackr, scales, - DT, purrr, rpivotTable, waiter, - flextable, officer, gdtools, digest, fansi) +### Libraries +library(shiny) +library(httr) +library(shinydashboard) +library(magrittr) +library(tidyverse) +library(xml2) +library(datimutils) +library(waiter) +library(futile.logger) +library(shinyWidgets) -# js ---- -# allows for using the enter button + +### js ---- +# allows for using the enter button for the log in jscode_login <- '$(document).keyup(function(e) { var focusedElement = document.activeElement.id; console.log(focusedElement); @@ -16,20 +23,11 @@ jscode_login <- '$(document).keyup(function(e) { } });' -#Set the maximum file size for the upload file -options(shiny.maxRequestSize = 150 * 1024 ^ 2) -#Allow unsanitized error messages -options(shiny.sanitize.errors = FALSE) -#Initiate logging +### Initiate logging logger <- flog.logger() +flog.appender(appender.console(), name = "datimutils") -if (!file.exists(Sys.getenv("LOG_PATH"))) { - file.create(Sys.getenv("LOG_PATH")) -} - -flog.appender(appender.console(), name = "datapack") - -################ OAuth Client information ##################################### +### OAuth Client information if (interactive()) { # testing url options(shiny.port = 3123) @@ -42,13 +40,13 @@ if (interactive()) { { oauth_app <- httr::oauth_app(Sys.getenv("OAUTH_APPNAME"), - key = Sys.getenv("OAUTH_KEYNAME"), # dhis2 = Client ID + key = Sys.getenv("OAUTH_KEYNAME"), # dhis2 = Client ID secret = Sys.getenv("OAUTH_SECRET"), #dhis2 = Client Secret redirect_uri = APP_URL ) oauth_api <- httr::oauth_endpoint(base_url = paste0(Sys.getenv("BASE_URL"),"uaa/oauth"), - request=NULL,# Documentation says to leave this NULL for OAuth2 + request=NULL, authorize = "authorize", access="token" ) @@ -61,54 +59,23 @@ has_auth_code <- function(params) { return(!is.null(params$code)) } - - +### Begin Shiny Web app items shinyServer(function(input, output, session) { validation_results <- reactive({ validate() }) # nolint ready <- reactiveValues(ok = FALSE) - user_input <- reactiveValues(authenticated = FALSE, status = "", d2_session = NULL, memo_authorized = FALSE, uuid = NULL) - - - observeEvent(input$file1, { - shinyjs::show("validate") - shinyjs::enable("validate") - ready$ok <- FALSE - }) - - observeEvent(input$validate, { - shinyjs::disable("file1") - shinyjs::disable("validate") - ready$ok <- TRUE - }) - - observeEvent(input$reset_input, { - shinyjs::reset("side-panel") - shinyjs::enable("file1") - shinyjs::disable("validate") - shinyjs::disable("downloadDataPack") - shinyjs::disable("download_messages") - shinyjs::disable("send_paw") - shinyjs::disable("downloadValidationResults") - shinyjs::disable("compare") - ready$ok <- FALSE - }) - - - - - +### Logout observeEvent(input$logout, { req(input$logout) - # Gets you back to the login without the authorization code at top + # Returns to the log in screen without the authorization code at top updateQueryString("?",mode="replace",session=session) flog.info(paste0("User ", user_input$d2_session$me$userCredentials$username, " logged out.")) ready$ok <- FALSE @@ -119,13 +86,13 @@ shinyServer(function(input, output, session) { d2_default_session <- NULL gc() session$reload() - }) - + +### Logic for which UI to display output$ui <- renderUI({ if (user_input$authenticated == FALSE) { - ##### UI code for login page + # References the UI code for log in page, could be combined fluidPage( fluidRow( column(width = 2, offset = 5, @@ -135,138 +102,42 @@ shinyServer(function(input, output, session) { ) ) } else { + #References the UI code for after log in, could be combined uiOutput("authenticated") } }) - - - # Username and password text fields, login button + +### UI code for log in screen + # Username and password text fields, log in button output$uiLogin <- renderUI({ wellPanel(fluidRow( - #img(src = "pepfar.png", align = "center"), tags$head(tags$script(HTML(jscode_login))), # enter button functionality for login button - tags$div(HTML('
')), - h4("Welcome to the DataPack Validation App. Please login with your DATIM credentials:") + #tags$div(HTML('
')), #Can add logo + h4("Welcome to the Datimutils OAuth Example App. Please login with your DATIM credentials:") ), fluidRow( actionButton("login_button_oauth","Log in with DATIM"), uiOutput("ui_hasauth"), uiOutput("ui_redirect") ), - fluidRow( - tags$hr(), - tags$div(HTML("
  • Please be sure you fully populate the PSNUxIM", - " tab when receiving a new DataPack. Consult the user guide for further information!", - "

  • See the latest updates to the app here.

")) - ), - tags$hr(), - fluidRow(HTML(getVersionInfo()))) + + ) }) - + +### UI code for after log in output$authenticated <- renderUI({ - wiki_url <- a("Datapack User Guide", - href = "https://apps.datim.org/datapack-userguide/", - target = "_blank") fluidPage( - tags$head(tags$style(".shiny-notification { - position: fixed; - top: 10%; - left: 33%; - right: 33%;}")), - use_waiter(), - sidebarLayout( - sidebarPanel( - shinyjs::useShinyjs(), - id = "side-panel", - tagList(wiki_url), - tags$hr(), - fileInput( - "file1", - "Choose DataPack (Must be XLSX!):", - accept = c("application/xlsx", - ".xlsx"), - width = "240px" - ), - actionButton("validate", "Validate"), - tags$hr(), - selectInput("downloadType", "Download type", NULL), - downloadButton("downloadOutputs", "Download"), - tags$hr(), - actionButton("send_paw", "Send to PAW"), - tags$hr(), - div(style = "display: inline-block; vertical-align:top; width: 80 px;", - actionButton("reset_input", "Reset inputs")), - div(style = "display: inline-block; vertical-align:top; width: 80 px;", - actionButton("logout", "Logout")) - ), - - mainPanel(tabsetPanel( - id = "main-panel", - type = "tabs", - tabPanel("Messages", tags$ul(uiOutput("messages"))), - tabPanel("Analytics checks", tags$div(uiOutput("analytics_checks"))), - tabPanel("Indicator summary", dataTableOutput("indicator_summary"), - tags$h4("Data source: Main DataPack tabs")), - tabPanel("SNU-level summary", - dataTableOutput("snu_summary"), - tags$h4("Data source: Main DataPack tabs")), - tabPanel("Validation rules", - dataTableOutput("vr_rules"), - tags$h4("Data source: PSNUxIM tab")), - tabPanel("HTS Summary Chart", - fluidRow(column(width = 12, div(style = "height:700px", plotOutput("modality_summary")))), - fluidRow(column(width = 12, tags$h4("Data source: PSNUxIM tab")))), - tabPanel("HTS Summary Table", - dataTableOutput("modality_table"), - tags$h4("Data source: PSNUxIM tab")), - tabPanel("HTS Yield", - fluidRow(column(width = 12, div(style = "height:700px", plotOutput("modality_yield")))), - fluidRow(tags$h4("Data source: PSNUxIM tab"))), - tabPanel("HTS Recency", - dataTableOutput("hts_recency"), - tags$h4("Data source: PSNUxIM tab")), - tabPanel("VLS Testing", - fluidRow(column(width = 12, div(style = "height:700px", plotOutput("vls_summary")))), - fluidRow(column(width = 12, tags$h4("Data source: PSNUxIM tab")))), - tabPanel("Epi Cascade Pyramid", - pickerInput("epiCascadeInput", "SNU1", - choices = "", - options = list(`actions-box` = TRUE), multiple = T), - fluidRow(column(width = 12, div(style = "height:700px", plotOutput("epi_cascade")))), - fluidRow(tags$h4("Data source: SUBNATT/IMPATT data & PSNUxIM tab"))), - tabPanel("KP Cascade Pyramid", - pickerInput("kpCascadeInput", "SNU1", - choices = "", - options = list(`actions-box` = TRUE), multiple = T), - fluidRow(column(width = 12, div(style = "height:700px", plotOutput("kp_cascade")))), - fluidRow(tags$h4("Data source: Data source: SUBNATT/IMPATT data & PSNUxIM tab"))), - tabPanel("PSNUxIM Pivot", - fluidRow(column(width = 12, div(rpivotTable::rpivotTableOutput({"pivot"})))), # nolint - fluidRow(tags$h4("Data source: PSNUxIM tab"))), - tabPanel( - "Memo Tables", - fluidRow( - pickerInput( - "memo_pivot_style", - label = "Table Style", - choices = c("Prioritization", "By Agency", "By Partner", "Comparison"), - selected = "Prioritization" - ) - ), - fluidRow(column(width = 12, - div(dataTableOutput({"memo_compare"})))), # nolint - fluidRow(tags$h4("Data source: PSNUxIM tab & DATIM"))) - - )) - )) + titlePanel("OAuth or PW Login Template"), + DT::dataTableOutput("mytable"), + actionButton("logout", + "Return to Login Page", + icon=icon("sign-out")) + ) }) - + + #UI that will display when redirected to OAuth login agent output$ui_redirect = renderUI({ #print(input$login_button_oauth) useful for debugging if(!is.null(input$login_button_oauth)){ @@ -277,8 +148,8 @@ shinyServer(function(input, output, session) { } else NULL } else NULL }) - - ### Login Button oauth Checks + +### Login Button oauth Checks observeEvent(input$login_button_oauth > 0,{ #Grabs the code from the url @@ -312,7 +183,7 @@ shinyServer(function(input, output, session) { ) }, # This function throws an error if the login is not successful error = function(e) { - flog.info(paste0("User ", input$user_name, " login failed. ", e$message), name = "datapack") + flog.info(paste0("User ", input$user_name, " login failed. ", e$message), name = "datimutils") } ) @@ -335,9 +206,9 @@ shinyServer(function(input, output, session) { user_input$d2_session$me$userCredentials$username, " logged in." ), - name = "datapack" + name = "datimutils" ) - sendEventToS3(NULL, "LOGIN", user_input = user_input) + flog.info( paste0( @@ -345,10 +216,20 @@ shinyServer(function(input, output, session) { user_input$d2_session$me$userCredentials$username, " logged in." ), - name = "datapack" + name = "datimutils" ) } }) - -}) + +########## Below this line is your standard server code ########## +### Controls Data Table + output$mytable = DT::renderDataTable({ + + df=getMetadata( + end_point = "organisationUnitGroups", + fields = "id,name", + d2_session = user_input$d2_session + ) + }) +}) \ No newline at end of file From 77c9f6b5915fc9bbe06c3190fff7eb5ce330d538 Mon Sep 17 00:00:00 2001 From: Jordan Date: Fri, 21 Oct 2022 12:12:16 -0400 Subject: [PATCH 12/21] App portion completed --- inst/shiny-examples/OAuth/server.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inst/shiny-examples/OAuth/server.R b/inst/shiny-examples/OAuth/server.R index fbb90543..61811ae4 100644 --- a/inst/shiny-examples/OAuth/server.R +++ b/inst/shiny-examples/OAuth/server.R @@ -129,7 +129,7 @@ shinyServer(function(input, output, session) { output$authenticated <- renderUI({ fluidPage( - titlePanel("OAuth or PW Login Template"), + titlePanel("Datimutils OAuth Example App"), DT::dataTableOutput("mytable"), actionButton("logout", "Return to Login Page", From 2842280107dbfd933ad0d2679808a10d332cd0e1 Mon Sep 17 00:00:00 2001 From: Jordan Date: Fri, 21 Oct 2022 12:14:13 -0400 Subject: [PATCH 13/21] no longer plan to allow user to launch an example from the package, so removed related functions. --- DESCRIPTION | 3 +-- R/runExample.R | 30 ------------------------------ 2 files changed, 1 insertion(+), 32 deletions(-) delete mode 100644 R/runExample.R diff --git a/DESCRIPTION b/DESCRIPTION index 0839d2c0..27da8387 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -40,8 +40,7 @@ Suggests: rmarkdown, assertthat, testthat (>= 2.1.0), - lintr (>= 3.0.0), - shiny + lintr (>= 3.0.0) License: GPL-3 + file LICENSE Encoding: UTF-8 LazyData: true diff --git a/R/runExample.R b/R/runExample.R deleted file mode 100644 index 5396c85c..00000000 --- a/R/runExample.R +++ /dev/null @@ -1,30 +0,0 @@ -runExample <- function(example) { - # locate all the shiny app examples that exist - #validExamples <- list.files(system.file("shiny-examples", package = "datimutils")) - validExamples <- list.files("~/Documents/Repos/datimutils/inst/shiny-examples") - - validExamplesMsg <- - paste0( - "Valid examples are: '", - paste(validExamples, collapse = "', '"), - "'") - - # if an invalid example is given, throw an error - if (missing(example) || !nzchar(example) || - !example %in% validExamples) { - stop( - 'Please run `runExample()` with a valid example app as an argument.\n', - validExamplesMsg, - call. = FALSE) - } - - # find and launch the app - #appDir <- system.file("shiny-examples", example, package = "datimutils") - appDir <- "~/Documents/Repos/datimutils/inst/shiny-examples/OAuth" - shiny::runApp(appDir, display.mode = "normal") -} - - - -# • In your package code, use `rlang::is_installed("shiny")` or `rlang::check_installed("shiny")` to test if shiny is installed -# • Then directly refer to functions with `shiny::fun()` \ No newline at end of file From b6322ad8c23da54694270b4d680edb62f49551b3 Mon Sep 17 00:00:00 2001 From: Jordan Date: Fri, 21 Oct 2022 12:43:50 -0400 Subject: [PATCH 14/21] renamed folder for easier cl navigation --- inst/{shiny-examples => shinyexamples}/OAuth/server.R | 0 inst/{shiny-examples => shinyexamples}/OAuth/ui.R | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename inst/{shiny-examples => shinyexamples}/OAuth/server.R (100%) rename inst/{shiny-examples => shinyexamples}/OAuth/ui.R (100%) diff --git a/inst/shiny-examples/OAuth/server.R b/inst/shinyexamples/OAuth/server.R similarity index 100% rename from inst/shiny-examples/OAuth/server.R rename to inst/shinyexamples/OAuth/server.R diff --git a/inst/shiny-examples/OAuth/ui.R b/inst/shinyexamples/OAuth/ui.R similarity index 100% rename from inst/shiny-examples/OAuth/ui.R rename to inst/shinyexamples/OAuth/ui.R From 1e30ab381cad26d96ccdfe2621b9a4d9fb667a71 Mon Sep 17 00:00:00 2001 From: Jordan Date: Fri, 21 Oct 2022 12:59:01 -0400 Subject: [PATCH 15/21] final updates to the vignette. --- vignettes/Introduction-to-datimutils.Rmd | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/vignettes/Introduction-to-datimutils.Rmd b/vignettes/Introduction-to-datimutils.Rmd index 9d805eee..9e0c5db6 100644 --- a/vignettes/Introduction-to-datimutils.Rmd +++ b/vignettes/Introduction-to-datimutils.Rmd @@ -59,7 +59,7 @@ loginToDATIM( ### Methods 2 and 3 - Configuration Files -The next two log in methods require the user to create a reusable configuration file in the format below. The location of this configuration file on the users's machine will be passed to the `loginToDATIM` function. If a user wants the ability to quickly log into multiple different servers, they can have a config file for each server instance. +The next two log in methods require the user to create a reusable configuration file in the format below. The location of this configuration file on the user's machine will be passed to the `loginToDATIM` function. If a user wants the ability to quickly log into multiple different servers, they can have a config file for each server instance. In this vignette, the example config file is part of the package, *play.json*. @@ -104,11 +104,15 @@ loginToDATIM(config_path = "play.json") The final log in method is `loginToDATIMOAuth`. It uses DATIM, or your chosen DHIS2 instance, to authenticate via OAuth. This function will send the user to the chosen instance based upon the baseurl provided in order to retrieve an authentication code. -This function is useful for Shiny web applications as the process of exchanging the code for a token is automated. While not recommended it can be used with the command line, however the code found after logging in will have to be manually entered when prompted. This code can be found at the end of the url in your browser. +This function is useful for Shiny web applications as the process of exchanging the code for a token is automated. While not recommended, it can be used with the command line, however the code found after logging in will have to be manually entered when prompted. This code can be found at the end of the url in your browser. It is important to note that an Oauth client will need to be configured in your chosen DHIS2 instance in order to utilize this functionality. Depending upon your access in the aforementioned DHIS2 instance you can do this by logging in \> navigating to "System Settings" in the menu \> selecting "OAuth2 Clients" on the left \> clicking the blue "+" button. If you do not see these options please contact your system admin. You will need the name, client ID, Client Secret, and Redirect URIs to configure your Shiny application. -An example app can be found inside this package by running enter command here. +An example app can be found inside this package by running the below command. The environment variables will need to be set before the app will launch, but is not required for explaining the app's architecture. + +```{r} +list.files(system.file('shiny-examples','OAuth', package = 'datimutils')) +``` ## Features for working with DATIM/DHIS2 metadata From 7e46985a93462372c3c7a7c8924ea0861ea6148b Mon Sep 17 00:00:00 2001 From: Jordan Date: Fri, 21 Oct 2022 13:15:24 -0400 Subject: [PATCH 16/21] Linting --- inst/shinyexamples/OAuth/server.R | 80 ++++++++++++------------ vignettes/Introduction-to-datimutils.Rmd | 2 +- 2 files changed, 40 insertions(+), 42 deletions(-) diff --git a/inst/shinyexamples/OAuth/server.R b/inst/shinyexamples/OAuth/server.R index 61811ae4..372b7dd3 100644 --- a/inst/shinyexamples/OAuth/server.R +++ b/inst/shinyexamples/OAuth/server.R @@ -37,35 +37,33 @@ if (interactive()) { APP_URL <- Sys.getenv("APP_URL") #This will be your shiny server path } -{ - - oauth_app <- httr::oauth_app(Sys.getenv("OAUTH_APPNAME"), + oauth_app <- httr::oauth_app(Sys.getenv("OAUTH_APPNAME"), key = Sys.getenv("OAUTH_KEYNAME"), # dhis2 = Client ID secret = Sys.getenv("OAUTH_SECRET"), #dhis2 = Client Secret redirect_uri = APP_URL ) - - oauth_api <- httr::oauth_endpoint(base_url = paste0(Sys.getenv("BASE_URL"),"uaa/oauth"), - request=NULL, + + oauth_api <- httr::oauth_endpoint(base_url = paste0(Sys.getenv("BASE_URL"), "uaa/oauth"), + request = NULL, authorize = "authorize", - access="token" + access = "token" ) - + oauth_scope <- "ALL" -} + has_auth_code <- function(params) { - + return(!is.null(params$code)) } ### Begin Shiny Web app items shinyServer(function(input, output, session) { - + validation_results <- reactive({ validate() }) # nolint - + ready <- reactiveValues(ok = FALSE) - + user_input <- reactiveValues(authenticated = FALSE, status = "", d2_session = NULL, @@ -76,7 +74,7 @@ shinyServer(function(input, output, session) { observeEvent(input$logout, { req(input$logout) # Returns to the log in screen without the authorization code at top - updateQueryString("?",mode="replace",session=session) + updateQueryString("?", mode = "replace", session = session) flog.info(paste0("User ", user_input$d2_session$me$userCredentials$username, " logged out.")) ready$ok <- FALSE user_input$authenticated <- FALSE @@ -87,10 +85,10 @@ shinyServer(function(input, output, session) { gc() session$reload() }) - -### Logic for which UI to display + +### Logic for which UI to display output$ui <- renderUI({ - + if (user_input$authenticated == FALSE) { # References the UI code for log in page, could be combined fluidPage( @@ -110,14 +108,14 @@ shinyServer(function(input, output, session) { ### UI code for log in screen # Username and password text fields, log in button output$uiLogin <- renderUI({ - + wellPanel(fluidRow( tags$head(tags$script(HTML(jscode_login))), # enter button functionality for login button #tags$div(HTML('
')), #Can add logo h4("Welcome to the Datimutils OAuth Example App. Please login with your DATIM credentials:") ), fluidRow( - actionButton("login_button_oauth","Log in with DATIM"), + actionButton("login_button_oauth", "Log in with DATIM"), uiOutput("ui_hasauth"), uiOutput("ui_redirect") ), @@ -125,23 +123,23 @@ shinyServer(function(input, output, session) { ) }) -### UI code for after log in +### UI code for after log in output$authenticated <- renderUI({ - + fluidPage( titlePanel("Datimutils OAuth Example App"), DT::dataTableOutput("mytable"), actionButton("logout", "Return to Login Page", - icon=icon("sign-out")) + icon = icon("sign-out")) ) }) #UI that will display when redirected to OAuth login agent - output$ui_redirect = renderUI({ + output$ui_redirect <- renderUI({ #print(input$login_button_oauth) useful for debugging - if(!is.null(input$login_button_oauth)){ - if(input$login_button_oauth>0){ + if (!is.null(input$login_button_oauth)) { # nolint + if (input$login_button_oauth > 0) { # nolint url <- httr::oauth2.0_authorize_url(oauth_api, oauth_app, scope = oauth_scope) redirect <- sprintf("location.replace(\"%s\");", url) tags$script(HTML(redirect)) @@ -150,13 +148,13 @@ shinyServer(function(input, output, session) { }) ### Login Button oauth Checks - observeEvent(input$login_button_oauth > 0,{ - + observeEvent(input$login_button_oauth > 0, { + #Grabs the code from the url params <- parseQueryString(session$clientData$url_search) #Wait until the auth code actually exists req(has_auth_code(params)) - + #Manually create a token token <- httr::oauth2.0_token( app = oauth_app, @@ -170,14 +168,14 @@ shinyServer(function(input, output, session) { code = params$code, use_basic_auth = TRUE) ) - + loginAttempt <- tryCatch({ user_input$uuid <- uuid::UUIDgenerate() datimutils::loginToDATIMOAuth(base_url = Sys.getenv("BASE_URL"), token = token, app = oauth_app, api = oauth_api, - redirect_uri= APP_URL, + redirect_uri = APP_URL, scope = oauth_scope, d2_session_envir = parent.env(environment()) ) }, @@ -186,13 +184,13 @@ shinyServer(function(input, output, session) { flog.info(paste0("User ", input$user_name, " login failed. ", e$message), name = "datimutils") } ) - + if (exists("d2_default_session")) { - + user_input$authenticated <- TRUE user_input$d2_session <- d2_default_session$clone() d2_default_session <- NULL - + #Need to check the user is a member of the PRIME Data Systems Group, COP Memo group, or a super user user_input$memo_authorized <- grepl("VDEqY8YeCEk|ezh8nmc4JbX", user_input$d2_session$me$userGroups) | @@ -208,8 +206,8 @@ shinyServer(function(input, output, session) { ), name = "datimutils" ) - - + + flog.info( paste0( "User ", @@ -219,17 +217,17 @@ shinyServer(function(input, output, session) { name = "datimutils" ) } - + }) ########## Below this line is your standard server code ########## -### Controls Data Table - output$mytable = DT::renderDataTable({ - - df=getMetadata( +### Controls Data Table + output$mytable <- DT::renderDataTable({ + + df <- getMetadata( end_point = "organisationUnitGroups", fields = "id,name", d2_session = user_input$d2_session ) }) -}) \ No newline at end of file +}) diff --git a/vignettes/Introduction-to-datimutils.Rmd b/vignettes/Introduction-to-datimutils.Rmd index 9e0c5db6..bf8adcef 100644 --- a/vignettes/Introduction-to-datimutils.Rmd +++ b/vignettes/Introduction-to-datimutils.Rmd @@ -111,7 +111,7 @@ It is important to note that an Oauth client will need to be configured in your An example app can be found inside this package by running the below command. The environment variables will need to be set before the app will launch, but is not required for explaining the app's architecture. ```{r} -list.files(system.file('shiny-examples','OAuth', package = 'datimutils')) +list.files(system.file("shiny-examples", "OAuth", package = "datimutils")) ``` ## Features for working with DATIM/DHIS2 metadata From ed9621ccd866b45d3fd37ec098595a0debda5000 Mon Sep 17 00:00:00 2001 From: sam-bao Date: Tue, 25 Oct 2022 09:16:04 +0200 Subject: [PATCH 17/21] Linting --- man/d2Session.Rd | 2 -- 1 file changed, 2 deletions(-) diff --git a/man/d2Session.Rd b/man/d2Session.Rd index 4b50abe1..ef92b077 100644 --- a/man/d2Session.Rd +++ b/man/d2Session.Rd @@ -61,9 +61,7 @@ connections} \item{\code{me}}{DHIS2 me response object} - \item{\code{token}}{OAUTH2 token} - } \if{html}{\out{}} } From dcb0e4cd1a40218a2e0ff1145cb84dd52bc1b856 Mon Sep 17 00:00:00 2001 From: sam-bao Date: Tue, 25 Oct 2022 09:41:39 +0200 Subject: [PATCH 18/21] Modify OAUTH example directory name to match what is stated in the vignette --- inst/{shinyexamples => shiny-examples}/OAuth/server.R | 0 inst/{shinyexamples => shiny-examples}/OAuth/ui.R | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename inst/{shinyexamples => shiny-examples}/OAuth/server.R (100%) rename inst/{shinyexamples => shiny-examples}/OAuth/ui.R (100%) diff --git a/inst/shinyexamples/OAuth/server.R b/inst/shiny-examples/OAuth/server.R similarity index 100% rename from inst/shinyexamples/OAuth/server.R rename to inst/shiny-examples/OAuth/server.R diff --git a/inst/shinyexamples/OAuth/ui.R b/inst/shiny-examples/OAuth/ui.R similarity index 100% rename from inst/shinyexamples/OAuth/ui.R rename to inst/shiny-examples/OAuth/ui.R From e8a442fca45b59583668cd3872515c52aa64a7cb Mon Sep 17 00:00:00 2001 From: Jordan Date: Wed, 26 Oct 2022 12:49:52 -0400 Subject: [PATCH 19/21] Debugged the port issue. Left comment as to how to go about fixing it when using coding a shiny app using the ui.R and serve.R format. --- inst/shiny-examples/OAuth/server.R | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/inst/shiny-examples/OAuth/server.R b/inst/shiny-examples/OAuth/server.R index 372b7dd3..4e959a7c 100644 --- a/inst/shiny-examples/OAuth/server.R +++ b/inst/shiny-examples/OAuth/server.R @@ -29,8 +29,12 @@ flog.appender(appender.console(), name = "datimutils") ### OAuth Client information if (interactive()) { - # testing url + # NOTE: The line below must be ran manually to set the port + # OR this line can be added to .Rprofile. + # This is not an issue when using a single file version of shiny, ie app.R + # The order by which the files execute is the reasoning behind this. options(shiny.port = 3123) + # testing url APP_URL <- "http://127.0.0.1:3123/"# This will be your local host path } else { # deployed URL From 84479213479920b48651173cf8e6773633533c9e Mon Sep 17 00:00:00 2001 From: Jordan Date: Wed, 26 Oct 2022 13:03:20 -0400 Subject: [PATCH 20/21] lint --- inst/shiny-examples/OAuth/server.R | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/inst/shiny-examples/OAuth/server.R b/inst/shiny-examples/OAuth/server.R index 4e959a7c..593bc572 100644 --- a/inst/shiny-examples/OAuth/server.R +++ b/inst/shiny-examples/OAuth/server.R @@ -29,9 +29,9 @@ flog.appender(appender.console(), name = "datimutils") ### OAuth Client information if (interactive()) { - # NOTE: The line below must be ran manually to set the port - # OR this line can be added to .Rprofile. - # This is not an issue when using a single file version of shiny, ie app.R + # NOTE: The line below must be ran manually to set the port + # OR this line can be added to .Rprofile. + # This is not an issue when using a single file version of shiny, ie app.R # The order by which the files execute is the reasoning behind this. options(shiny.port = 3123) # testing url From 55f17e91c2fdfa742b2de5856fa006605e88fb24 Mon Sep 17 00:00:00 2001 From: Fausto Lopez <92821116+flopez-bao@users.noreply.github.com> Date: Mon, 15 May 2023 09:37:04 -0400 Subject: [PATCH 21/21] Update getMetadataEndpoint.R (#114) * Update getMetadataEndpoint.R - added removal of nas to return NA value instead of erroring out * linting -fix linting * Update getAnalytics.Rd * add unit test add unit test pointed at play for NA handling --------- Co-authored-by: sam-bao --- R/getMetadataEndpoint.R | 2 +- man/getAnalytics.Rd | 2 +- .../2.37.2/api/categoryOptions.json-28e9ae.json | 12 ++++++++++++ tests/testthat/test-getMetadataEndpoint.r | 8 ++++++++ 4 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 tests/testthat/play.dhis2.org/2.37.2/api/categoryOptions.json-28e9ae.json diff --git a/R/getMetadataEndpoint.R b/R/getMetadataEndpoint.R index 238e6e0c..ffee209c 100644 --- a/R/getMetadataEndpoint.R +++ b/R/getMetadataEndpoint.R @@ -149,7 +149,7 @@ duplicateResponse <- function(resp, expand, by) { unique_values <- unique(values) #break up url to multiple calls if needed - if (sum(nchar(unique_values)) > 2000) { + if (sum(nchar(unique_values), na.rm = TRUE) > 2000) { values_list <- .splitUrlComponent(unique_values, 2000) diff --git a/man/getAnalytics.Rd b/man/getAnalytics.Rd index f91ccf59..e482208f 100644 --- a/man/getAnalytics.Rd +++ b/man/getAnalytics.Rd @@ -19,7 +19,7 @@ getAnalytics( return_names = FALSE, d2_session = dynGet("d2_default_session", inherits = TRUE), retry = 1, - timeout = 60, + timeout = 180, verbose = FALSE, quiet = TRUE ) diff --git a/tests/testthat/play.dhis2.org/2.37.2/api/categoryOptions.json-28e9ae.json b/tests/testthat/play.dhis2.org/2.37.2/api/categoryOptions.json-28e9ae.json new file mode 100644 index 00000000..a23ced81 --- /dev/null +++ b/tests/testthat/play.dhis2.org/2.37.2/api/categoryOptions.json-28e9ae.json @@ -0,0 +1,12 @@ +{ + "categoryOptions": [ + { + "name": "10-14", + "id": "jcGQdcpPSJP" + }, + { + "name": "35-39", + "id": "R32YPF38CJJ" + } + ] +} diff --git a/tests/testthat/test-getMetadataEndpoint.r b/tests/testthat/test-getMetadataEndpoint.r index 23296176..af3b97e7 100644 --- a/tests/testthat/test-getMetadataEndpoint.r +++ b/tests/testthat/test-getMetadataEndpoint.r @@ -730,3 +730,11 @@ test_that( testthat::expect_s3_class(resp, "data.frame") rm(resp) }) + +# test NAs are handled properly +httptest::with_mock_api({ + # na value is passed to the api to test handling + age_option_uid <- c(NA, "R32YPF38CJJ", "jcGQdcpPSJP") + res <- datimutils::getCatOptions(age_option_uid, d2_session = play2372) + testthat::expect_identical(res, c(NA, "35-39", "10-14")) +})