Download existing docx object from R Shiny app - r

I am working on an R Shiny app where a user supplies information that modifies an existing Word document for the user to download. I've had trouble getting R Shiny to download the resulting new Word document. I've tried regular hyperlinks, and that doesn't seem to work.
(Edit: After typing up this post, I came across how to download files with hyperlinks. I forgot files need to be placed inside a www folder, as specified here: Shiny hyperlink relative path to a file. So although I can get my Shiny App to work using this approach, I'd still like to know why my example below is not working).
I came across Github Issue #145 (https://github.com/davidgohel/officer/issues/145) which almost has the solution. But the pptx being downloaded is created from scratch whereas I want to start from an EXISTING Word docx.
In my code example, there are 3 downloadHandler buttons:
Uses the original pptx download code example from Github Issue #145
The second modifies the above to download a docx
The third button is my attempt to download an existing and modified docx file
The third button is not working as I had hoped. If I had to guess, I think it has to do with my template being from from read_docx. It looks like it creates some temporary file behind the scenes. But I don't know where to go from here.
For completeness, here are some related links:
Reporters package to download docx report from shiny (uses ReporeRs which is older than officer R package)
Writing word documents with the officer package: How to combine several rdocx objects? (Helpful if merging existing docx to the tempfile in my example)
downloadHandler reference: https://shiny.rstudio.com/reference/shiny/latest/downloadHandler.html
# -------- Example code ------------
library(shiny)
library(officer)
library(mschart)
library(dplyr)
# Create template folder and file. (Irrelevant if already exists.)
dir.create("www")
read_docx() %>%
body_add_par("My template file") %>%
print(., target = "www/template.docx")
# Existing file as Template
mytemplate <- read_docx(path = "www/template.docx")
# For Button 1
gen_pptx <- function(chart, file) {
read_pptx() %>%
add_slide(layout = "Title and Content", master = "Office Theme") %>%
ph_with_chart(chart = chart) %>%
print(target = file)
}
chart <- data.frame(x = letters[1:3], y = 1:3) %>%
ms_barchart(x = "x", y = "y")
# For button 2
gen_docx <- function(file) {
read_docx() %>%
body_add_par("Hello World") %>%
print(target = file)
}
# For button 3
gen_docx2 <- function(file, doc) {
file %>%
body_add_par("Hello World") %>%
body_add_docx(src = doc) %>%
print(target = file)
}
ui <- fluidPage(
titlePanel("Example"),
downloadButton("chart", "Get Chart"),
downloadButton("document", "Get New Doc"),
downloadButton("document2", "Get Doc from Template"),
tags$hr(),
tags$p("Example hyperlink works"),
tags$a(href='template.docx', target='_blank', 'Can only download from www folder', download = 'template.docx')
)
server <- function(input, output) {
output$chart <- downloadHandler(
filename = function() paste0("chart_", Sys.Date(), ".pptx"),
content = function(file) {
file_pptx <- tempfile(fileext = ".pptx")
gen_pptx(chart, file_pptx)
file.rename( from = file_pptx, to = file )
}
)
output$document <- downloadHandler(
filename = function() paste0("doc_", Sys.Date(), ".docx"),
content = function(file) {
file_docx <- tempfile(fileext = ".docx")
gen_docx(file_docx)
file.rename( from = file_docx, to = file )
}
)
output$document2 <- downloadHandler(
filename = function() paste0("doc_", Sys.Date(), ".docx"),
content = function(file) {
file_docx <- tempfile(fileext = ".docx")
gen_docx2(file_docx, mytemplate)
file.rename( from = file_docx, to = file )
}
)
}
shinyApp(ui = ui, server = server)

Do not use print to generate docx file in helper function gen_docx2.
Instead, you should use print as a last step in content function to return file to the downloadHandler.
Simple example:
gen_docx2 <- function(file, doc) {
file %>%
body_add_par("Hello World") %>%
body_add_docx(src = doc)
}
notice the function above does not print anything
outputdocument2 <- downloadHandler(
filename = function() {
paste0("doc_", Sys.Date(), ".docx")
},
content = function(file) {
doc <- read_docx() %>% gen_docx2(file_docx, mytemplate)
print(doc, target = file)
}
)
Use read_docx() to generate temporary word file and then use print(doc, target = file) in the end to pass it to the downloadHandler. The function file.rename is not needed.

Related

How to download rhandsontable output from an rshiny to a shadow document

I'm creating an application to track soccer statistics for a high school team. The coach wants it to be like an excel sheet, where player data can be inputted and saved to be looked at later. I've figured out how to do that with rhandsontable, but I can't get it to download to my shadow document from my r shiny. I was able to get the shadow doc to work before adding the rhandsontable, but it is no longer working. I'm attaching the code from the server that I'm using to create the table. The player_names is a csv with columns for player name, team level, goals scored,.... other stats. I'm first looking at only one team level (ie varsity), then making the table. It works great in the shiny, but I don't how to save it from there.
output$hot <- renderRHandsontable({
player_names %>%
filter(Team == input$team_level) %>%
rhandsontable()
})
values <- reactiveValues(data = NULL)
observe({
values$data <- hot_to_r(input$hot)
})
# 3. I assign this df to a variable call df1
df1 <- reactive({
values$data
})
Code to make shadow doc:
output$downloadReport <- downloadHandler(
filename = function() {
paste("ophs_soccer", sep = ".", switch(
input$format, PDF = "pdf", HTML = "html", Word = "docx"
))
},
content = function(file) {
src <- normalizePath("shadow_page.Rmd")
# temporarily switch to the temp dir, in case you do not have write
# permission to the current working directory
owd <- setwd(tempdir())
on.exit(setwd(owd))
file.copy(src, "shadow_page.Rmd", overwrite = TRUE)
library(rmarkdown)
out <- render("shadow_page.Rmd", switch(
input$format,
PDF = pdf_document(), HTML = html_document(), Word = word_document()
))
file.rename(out, file)
I tried to create output in the shadow doc like I did with the other reactive inputs, and it did not work. I get an error when trying to download the document.

How to download multiple files from R/Shiny app?

There are a number of different Q/A's regarding this topic on SO, but none that I have been able to find that fit my use-case. I am also very surprised that RStudio / the Shiny developers themselves have not come out with some documentation on how to do this. Regardless, take this example application:
library(shiny)
library(glue)
library(tidyverse)
# Define UI for application
ui <- fluidPage(
# Application title
titlePanel("Test Multi-File Download"),
p("I hope this works!"),
downloadButton(
outputId = "download_btn",
label = "Download",
icon = icon("file-download")
)
)
# Define server logic
server <- function(input, output) {
#datasets stored in reactiveValues list
to_download <- reactiveValues(dataset1 = iris, dataset2 = airquality, dataset3 = mtcars, dataset4 = NULL)
blahblah <- iris
output$download_btn <- downloadHandler(
filename = function(){
paste("my_data_", Sys.Date(), ".csv", sep = "")
},
content = function(file){
#works
#readr::write_csv(blahblah, file)
#Attempt 1
# #create some temp directory
# temp_directory <- tempdir()
# browser()
# reactiveValuesToList(to_download) %>%
# #x is data, y is name
# imap(function(x,y){
# #browser()
# #check if data is not null
# if(!is.null(x)){
# #create file name based on name of dataset
# file_name <- glue("{y}_data.csv")
# #write file to temp directory
# readr::write_csv(x, file_name)
# }
# })
# zip::zip(
# zipfile = file,
# files = ls(temp_directory),
# root = temp_directory
# )
}
)
}
# Run the application
shinyApp(ui = ui, server = server)
I have some datasets that are stored in a reactiveValues list, and I would like the user to be able to download them all. Ideally, I'd like for them just to be able to download multiple files all at once, rather than having to zip them up, and then download a .zip file. Another option I would be okay with is to add each dataset to an Excel sheet, then download the multi-sheet Excel file.
My general thought process (on the former) is as follows:
Download button gets pressed
create some temporary directory
write (the not NULL) datasets contained in to_download reactiveValues list to this directory
zip the temp directory and download
I feel like I am very close, however I have not been able to successfully get this work yet. Any ideas?
Edit 1: I am aware of the proposed answer here, but would like to avoid using setwd() because I believe it is bad practice to mess with working directories from within a Shiny application.
A few things edited and it's working:
using dir instead of ls inside the zip::zip call to show the contents of the temp directory (ls lists R environment rather than directory contents)
as a further suggestion: making a new, unique folder inside tempdir() to ensure only relevant files are added.
library(shiny)
library(glue)
library(tidyverse)
# Define UI for application
ui <- fluidPage(
# Application title
titlePanel("Test Multi-File Download"),
p("I hope this works!"),
downloadButton(
outputId = "download_btn",
label = "Download",
icon = icon("file-download")
)
)
# Define server logic
server <- function(input, output) {
#datasets stored in reactiveValues list
to_download <- reactiveValues(dataset1 = iris, dataset2 = airquality, dataset3 = mtcars, dataset4 = NULL)
blahblah <- iris
output$download_btn <- downloadHandler(
filename = function(){
paste("my_data_", Sys.Date(), ".zip", sep = "")
},
content = function(file){
temp_directory <- file.path(tempdir(), as.integer(Sys.time()))
dir.create(temp_directory)
reactiveValuesToList(to_download) %>%
imap(function(x,y){
if(!is.null(x)){
file_name <- glue("{y}_data.csv")
readr::write_csv(x, file.path(temp_directory, file_name))
}
})
zip::zip(
zipfile = file,
files = dir(temp_directory),
root = temp_directory
)
},
contentType = "application/zip"
)
}
shinyApp(ui = ui, server = server)
In my own Shiny app I had used a multi-worksheet approach as you suggested above. An alternative setup which works to produce a multi-sheet xlsx workbook using openxlsx could be:
...
output$download_btn <- downloadHandler(
filename = function(){
paste("my_data_", Sys.Date(), ".xlsx", sep = "")
},
content = function(file){
wb <- createWorkbook()
reactiveValuesToList(to_download) %>%
imap(function(x,y){
if(!is.null(x)){
addWorksheet(wb, sheetName = y)
writeData(wb, x, sheet = y)
}
})
saveWorkbook(wb, file = file)
},
contentType = "file/xlsx"
)
...
Created on 2021-12-16 by the reprex package (v2.0.1)

RShiny - export diagrammeR plot to powerpoint

I have an RShiny application that generates a network plot using the diagrammeR package:
blc3001 <- eventReactive(input$datapath,{
data_blc <- readxl::read_xlsx(input$datapath$datapath)
blc3001 <- "digraph test{<TONS OF GRAPH CODE>"
})
I can output to Shiny using:
output$blc3001 <- renderGrViz({
grViz(blc3001())
})
And I can download as a .png using:
output$d_blc3001 <- downloadHandler(
filename = "blc3001.png",
content = function(file) {
grViz(blc3001()) %>% export_svg %>% charToRaw %>% rsvg_png(file)
},
contentType = "image/png"
)
Now I am trying to download this graph into a powerpoint slide, but am running into difficulty. I am open to any method or package, but I am currently trying to do this via the "officer" package.
output$blc3001_powerpoint <- downloadHandler(
filename = function() {
"blc3001.pptx"
},
content = function(file) {
image <- grViz(blc3001()) %>% export_svg %>% charToRaw %>% rsvg_png()
ppt_report <- read_pptx() %>%
add_slide(layout = "Title and Content", master = "Office Theme") %>%
ph_with(value = image, location = ph_location_type(type = "body"))
print(ppt_report, target = file)
}
)
Although I can download the png, when I define ppt_report, I get error:
Error in UseMethod("ph_with", value) :
no applicable method for 'ph_with' applied to an object of class "raw"
I assume this is because I specify content type is img/PNG in the image download handler, but not the powerpoint download handler. I can download the powerpoint without error if I remove "%>% charToRaw %>% rsvg_png()" from defining image, but the powerpoint slide contains the XML code rather than rendering the actual svg. If anyone can please provide any tips on how to proceed I would greatly appreciate it! Thanks!

Is there a way to load several excel files from a dropbox folder into an R-shiny app?

I am still relatively new at working in R shiny and I am trying to load several excel files into an R-shiny app. Part of the problem is that I need to be able to pull several files from a dropbox folder without specifying what the data file is called. So I need to be able to tell R to read in all the files from a dropbox folder. Also the files I am working with are in .xlsx format and I will need to read them into R as such.
I tried to do this first by using a folder on my computer desktop. I managed to get it to work using my local directory with the code below:
library(readxl)
library(tidyverse)
files <- list.files(path = "~/Desktop/data", pattern = "*.xlsx", full.names = TRUE) #read files from folder on desktop
df <- sapply(files, read_excel, simplify = FALSE) %>% #read files from the path, and bind them together
bind_rows()
I tried to adjust the code above to work with the drop_dir function in rdrop2. The code I tried is below:
library(rdrop2)
library(tidyverse)
library(readxl)
token <- drop_auth()
files <- drop_dir("!dropbox_folder", dtoken = token) #List all files in Dropbox folder MPD_03_Test
f <- files$path_display #list directory to dropbox
df <- sapply(f, read_excel, simplify = FALSE) %>% #runs the read function for all the files that are pulled
bind_rows() # .id="id creates a unique ID for each row and then binds them all together based on the ID.
When I run it the code is not loading the data files from the dropbox into R. When I run the dropbox code it just creates an empty object. Any help on where to go to figure this out will be greatly appreciated! Also I intend to use this as how I read data into and R-shiny app if that helps frame any suggestions you may have about how to approach my problem.
Thank You!
#MrGumble is correct in his comments. The files need to downloaded before being read. The drop_dir() function lists file paths on dropbox server and we can only read in data saved locally to our machine. If you have .csv files then this can be down in 1 step with the drop_read_csv() function. But since you have excel files these need to first to be downloaded explicitly with drop_download() and then read in with read_excel().
library(rdrop2)
library(tidyverse)
library(readxl)
#install.packages("xlsx")
library(xlsx)
token <- drop_auth()
#make a few excel file with iris dataset, save locally, and upload to dropbox root
iris_filenames <- paste0("iris", 1:3, ".xlsx")
walk(iris_filenames, ~write.xlsx(iris, file = .x, row.names = FALSE))
walk(iris_filenames, drop_upload)
#list all files on dropbox root and filter for only iris ones
iris_files_on_dropbox <- drop_dir(dtoken = token) %>%
filter(str_detect(name, 'iris'))
#make new filenames so we can see that the download worked correctly
#you could do overwrite = TRUE and not pass through new filenames
#see ?drop_download for all options
new_iris_filenames <- paste0("iris", 1:3, "-from-dropbox.xlsx")
#download the files first
walk2(iris_files_on_dropbox$name, new_iris_filenames, ~drop_download(path = .x, local_path = .y))
#then read them all in
df <- bind_rows(map(new_iris_filenames, read_xlsx))
Additionally, we can create our own custom function to do the download and reading in 1 step just as drop_read_csv() does by altering the source code for this function. All we need to do is change the read...() function from read.csv to read_excel and the reference to the dtoken default get_drop_token() to rdrop2:::get_drop_token() which is an un-exported function from the rdrop2 package so we need the three ':::'.
#source for drop_read_csv we can rewrite for excel files
# drop_read_csv <- function(file, dest = tempdir(), dtoken = get_dropbox_token(), ...) {
# localfile = paste0(dest, "/", basename(file))
# drop_download(file, localfile, overwrite = TRUE, dtoken = dtoken)
# utils::read.csv(localfile, ...)
# }
drop_read_excel <- function(file, dest = tempdir(), dtoken = rdrop2:::get_dropbox_token(), ...) {
localfile = paste0(dest, "/", basename(file))
drop_download(file, localfile, overwrite = TRUE, dtoken = dtoken)
readxl::read_excel(localfile, ...)
}
df2 <- bind_rows(map(iris_files_on_dropbox$name, drop_read_excel))
To work in a shiny app we first need to save the drop_auth token so we can authenticate while using the shiny app. Save this into your shiny app directory.
saveRDS(token, file = "token.rds")
Now here is a shiny app. When the 'go' button is clicked the iris excel files are downloaded and shown in the UI. We need to call drop_auth() in the global environment or global.R along with the custom drop_read_excel() function to use it.
library(shiny)
library(rdrop2)
library(tidyverse)
#saveRDS(token, file = "token.rds") into shiny app directory
#authenticate in global.R or outside of ui/server
drop_auth(rdstoken = "token.rds")
drop_read_excel <- function(file, dest = tempdir(), dtoken = rdrop2:::get_dropbox_token(), ...) {
localfile = paste0(dest, "/", basename(file))
drop_download(file, localfile, overwrite = TRUE, dtoken = dtoken)
readxl::read_excel(localfile, ...)
}
ui <- fluidPage(
actionButton("go", "go"),
tableOutput("table")
)
server <- function(input, output, session) {
df <- eventReactive(input$go, {
withProgress(message = 'Downloading from dropbox',
detail = 'This may take a while...', value = 0.5, {
iris_files_on_dropbox <- drop_dir() %>%
filter(str_detect(name, 'iris'))
setProgress(value = 0.75)
df <- bind_rows(map(iris_files_on_dropbox$name, drop_read_excel))
setProgress(value = 1)
})
return(df)
})
output$table <- renderTable({
df()
})
}
shinyApp(ui, server)

Shiny Download File based on File Path

I have a file which i generate in shiny
The user clicks a button and the file should download. However nothing happens
The function export_report generates the excel file and saves it to a location. The function then passes back the file location to the download handler so it will download the file. The problem seems to be that it isnt being returned correctly. I have tested the function (export_report) outside of shiny and it returns everything perfectly so I'm clearly doing something wrong from the shiny perspective.
The file itself is created where it is supposed to be on the server because i can download it within RStudio and see it in the file explorer. Can anyone help
# UI Section
downloadButton("downloadRpt", "Download Report")
# Server Section
output$downloadRpt <- downloadHandler(
filename = function() {
mydf <- report()
dateRange <- input$dates_report
selection <- input$selection
myfile <- export_report (mydf, selection, dateRange)
},
content = function(file) {
file.copy(myfile, file)
}
)
I have seen other examples R Shiny: Download existing file which is what my code is based on
EDIT 1: Adding the export_report function with some fake data to run it
export_report <- function(mydf,selection,dateRange) {
# Template for where the template excel file is stored
myoutputTemplate <- '/home/shiny_tutorials/Save to Database/templates/output_template.xlsx'
start_date <- dateRange[1]
end_date <- dateRange[2]
date_range <- paste(start_date ,end_date, sep = " - " )
# Load workbook the template workbook
wb <- loadWorkbook(myoutputTemplate)
# write to the workbook the data frame
writeWorksheet(wb, mydf, sheet="Details",
startRow=8, startCol=2,
header=FALSE)
# add the the customer the user selected
writeWorksheet(wb, selection, sheet="Details",
startRow=3, startCol=3,
header=FALSE)
# date
writeWorksheet(wb, date_range, sheet="Details",
startRow=5, startCol=3,
header=FALSE)
# Create The file Name
filename <- paste(selection, Sys.Date(), sep = " - ") %>%
paste(.,"xlsx", sep = ".")
# removes the % sign and extra qoutes
filename <- gsub (pattern = '\'|%','', x = filename)
# output directory
myoutput <- paste('/home/shiny_tutorials/Save to Database/output/',
filename, sep = '')
# Save workbook
saveWorkbook(wb, myoutput)
# Return File Path
myoutput
}
To call the function you can use the data below
dateRange <- c("2011-09-23","2016-09-23")
selection = "COMPANY_A"
mydf <- iris
myfile <- export_report(mydf,selection,dateRange)
EDIT 2 I have now managed to get an error out of it. When i cat(myfile) in the filename = function() { section of the code i get the error after the correct file path has been returned
Warning in rep(yes, length.out = length(ans)) :
'x' is NULL so the result will be NULL
Warning: Error in ifelse: replacement has length zero
Stack trace (innermost first):
1: runApp
Error : replacement has length zero
This error is basically because my file path does not get passed to the segment myfile so
if someone can tell me how to get the filepath generated by my function to the server section of the code below, that should fix my problem
content = function(file) {
file.copy(myfile, file)
}
Thank you to everyone who commented and clarified my thinking a bit on how the download handler works.
In the end, i created a new function which split up the export function above
The new function i used is called generate_file() which simply returns the file name
generate_file_name <- function(selection) {
# Create The file Name
filename <- paste(selection, Sys.Date(), sep = " - ") %>%
paste(.,"xlsx", sep = ".")
# removes the % sign and extra qoutes
filename <- gsub (pattern = '\'|%','', x = filename)
# output directory
myoutput <- paste('/home/shiny_tutorials/Save to Database/output/',
filename, sep = '')
# Return File Path
myoutput
}
Then in the server side
output$downloadRpt <- downloadHandler(
filename = function() {
selection <- input$company
generate_file_name(selection)
},
content = function(file) {
mydf <- report()
dateRange <- input$dates_report
selection <- input$company
export_report(mydf,selection,dateRange)
myfile <- generate_file_name(selection)
file.copy(myfile, file)
}
)
This then finds the newly created file and exports it for the user
I just checked your problem with this example code and it worked:
output$downloadData <- downloadHandler(
filename = function() {
data <- mtcars
myfile <- "data.csv"
write.csv(data, myfile)
myfile
},
content = function(file) {
print(file) //prints: [...]\\Local\\Temp\\RtmpEBYDXT\\fileab8c003878.csv
file.copy(file, file)
}
)
myfile is the filename of the downloaded file. You cannot use it in file.copy as input, this variable is out of scope. It seems that R creates a temp file name (see the print()).
Try to use the filename function to define your path or a custom file name, and the write.csv in the content part. Example code:
output$downloadData <- downloadHandler(
filename = function() {
paste(<user_name or date for eg>, ".csv", sep="")
},
content = function(file) {
write.csv(data, file)
}
)
I noticed in your comment above, you have asked how the application would generate the correct file when used by multiple users. For this part, you need to use the session.
So if your business logic functions were to come from an R file called foo.R, the server code should look something like:
shinyServer(func = function(input, output, session) {
source("./foo.R", local=TRUE)
......
And this would separate out the session for each user, thereby generating files specific to each, when downloading. Hope this gives you some pointers.

Resources