Skip to content

Commit

Permalink
Merge pull request #121 from pepfar-datim/dev
Browse files Browse the repository at this point in the history
Release 0.7.0
  • Loading branch information
JordanBalesBAO authored Jul 18, 2023
2 parents cd7e37e + 88b5103 commit 31c0be4
Show file tree
Hide file tree
Showing 7 changed files with 286 additions and 9 deletions.
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Suggests:
rmarkdown,
assertthat,
testthat (>= 2.1.0),
lintr(>= 3.0.0)
lintr (>= 3.0.0)
License: GPL-3 + file LICENSE
Encoding: UTF-8
LazyData: true
Expand Down
2 changes: 1 addition & 1 deletion R/getMetadataEndpoint.R
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
237 changes: 237 additions & 0 deletions inst/shiny-examples/OAuth/server.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
### 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 for the log in
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();
}
});'

### Initiate logging
logger <- flog.logger()
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
# 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
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,
authorize = "authorize",
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,
memo_authorized = FALSE,
uuid = NULL)

### Logout
observeEvent(input$logout, {
req(input$logout)
# 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
user_input$authenticated <- FALSE
user_input$user_name <- ""
user_input$authorized <- FALSE
user_input$d2_session <- NULL
d2_default_session <- NULL
gc()
session$reload()
})

### 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(
fluidRow(
column(width = 2, offset = 5,
br(), br(), br(), br(),
uiOutput("uiLogin")
)
)
)
} else {
#References the UI code for after log in, could be combined
uiOutput("authenticated")
}
})

### 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('<center><img src="pepfar.png"></center>')), #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")
),

)
})

### 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"))
)
})

#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)) { # 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))
} 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 = "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) |
grepl(
"jtzbVV4ZmdP",
user_input$d2_session$me$userCredentials$userRoles
)
flog.info(
paste0(
"User ",
user_input$d2_session$me$userCredentials$username,
" logged in."
),
name = "datimutils"
)


flog.info(
paste0(
"User ",
user_input$d2_session$me$userCredentials$username,
" logged in."
),
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
)
})
})
5 changes: 5 additions & 0 deletions inst/shiny-examples/OAuth/ui.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
library(shiny)

shinyUI(
uiOutput("ui")
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"categoryOptions": [
{
"name": "10-14",
"id": "jcGQdcpPSJP"
},
{
"name": "35-39",
"id": "R32YPF38CJJ"
}
]
}
8 changes: 8 additions & 0 deletions tests/testthat/test-getMetadataEndpoint.r
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
})
29 changes: 22 additions & 7 deletions vignettes/Introduction-to-datimutils.Rmd
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 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*.

Expand Down Expand Up @@ -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

Expand All @@ -100,6 +100,20 @@ 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 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

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.
Expand All @@ -119,7 +133,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 <- "
Expand Down Expand Up @@ -250,6 +264,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<!-- "This XML file does not appear to have any style information associated with it. The document tree is shown below."--> :

```{=html}
{
"organisationUnits": [
Expand Down Expand Up @@ -365,9 +380,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(
Expand All @@ -378,6 +392,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",
Expand Down

0 comments on commit 31c0be4

Please sign in to comment.