diff --git a/.Rbuildignore b/.Rbuildignore index c37d6891..94ecb120 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -13,3 +13,4 @@ psychTestR.pdf ^vignettes/output$ ^docs$ ^tests/testthat/output$ +^\.httr-oauth$ diff --git a/.gitignore b/.gitignore index 3a6516d2..94731bd0 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ closed.txt output docs vignettes/output +.DS_Store +.httr-oauth diff --git a/DESCRIPTION b/DESCRIPTION index 352fdda3..3d21c129 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -34,9 +34,12 @@ Imports: htm2txt, shinytest, testthat (>= 2.0.0), - stringr (>= 1.4.0) + stringr (>= 1.4.0), + withr, + uuid, + rdrop2 (>= 0.8.1) Depends: R (>= 3.4.0) -RoxygenNote: 7.1.0 +RoxygenNote: 7.1.1 Suggests: knitr, rmarkdown diff --git a/NAMESPACE b/NAMESPACE index 84fa8654..6bcded30 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -17,7 +17,9 @@ export("answer<-") export("error<-") export("results<-") export(AppTester) +export(DropboxRepository) export(I18N_STATE) +export(LocalRespository) export(NAFC_page) export(answer) export(as.timeline) @@ -50,7 +52,6 @@ export(get_local) export(get_p_id) export(get_results) export(get_session_info) -export(get_supplementary_results_dir) export(get_url_params) export(i18n) export(i18n_dict) diff --git a/NEWS.md b/NEWS.md index f949253d..e95ff041 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,5 @@ +- Removed `supplementary_results_dir`. + # psychTestR 2.19.1 - Adding an input check for conditional(). diff --git a/R/admin-panel.R b/R/admin-panel.R index d6c8d54a..653d3047 100644 --- a/R/admin-panel.R +++ b/R/admin-panel.R @@ -238,7 +238,7 @@ admin_panel.statistics.num_participants <- function(input, output, opt) { input$admin_panel.statistics.refresh input$admin_panel.statistics.open shiny::showNotification("Refreshing statistics...") - df <- tabulate_results(opt, include_pilot = FALSE) + df <- opt$repository$tabulate_results(include_pilot = FALSE) n_complete <- sum(df$complete) n_part_complete <- sum(!df$complete) shiny::p( @@ -258,11 +258,10 @@ admin_panel.statistics.latest_results <- function(input, output, opt) { output$admin_panel.statistics.latest_results <- shiny::renderUI({ input$admin_panel.statistics.refresh input$admin_panel.statistics.open - files <- tabulate_results(opt, include_pilot = FALSE) - if (nrow(files) > 0L) { - latest_file <- files$file[[which.max(files$id)]] - latest_path <- file.path(opt$results_dir, latest_file) - latest_data <- readRDS(latest_path) + results_table <- opt$repository$tabulate_results(include_pilot = FALSE) + if (nrow(results_table) > 0L) { + latest_key <- results_table$key[[which.max(results_table$id)]] + latest_data <- opt$repository$load_results(latest_key) latest_time <- as.list(latest_data)$session$current_time if (!is.null(latest_time)) { time_diff <- Sys.time() - latest_time @@ -281,11 +280,10 @@ admin_panel.statistics.average_time <- function(input, output, opt) { output$admin_panel.statistics.average_time <- shiny::renderUI({ input$admin_panel.statistics.refresh input$admin_panel.statistics.open - files <- tabulate_results(opt, include_pilot = FALSE) - files <- files[files$complete, ] - if (nrow(files) > 0L) { - data <- lapply(files$full_file, readRDS) - time_taken <- vapply(data, function(x) { + results <- opt$repository$load_all_results(include_pilot = FALSE) + results <- results[results$complete, ] + if (nrow(results) > 0L) { + time_taken <- vapply(results$data, function(x) { difftime(x$session$current_time, x$session$time_started, units = "mins") }, numeric(1)) M <- mean(time_taken) @@ -317,20 +315,26 @@ admin_panel.clear_sessions.observers <- function(state, input, opt) { } admin_panel.delete_results.actual <- function(opt) { - dir <- opt$results_archive_dir - R.utils::mkdirs(dir) - file <- paste0(format(Sys.time(), - format = "date=%Y-%m-%d&time=%H-%M-%S&tz=%Z"), - ".zip") - path <- file.path(dir, file) + # "deleted-results" + zip_key <- paste0( + format(Sys.time(), format = "date=%Y-%m-%d&time=%H-%M-%S&tz=%Z"), + ".zip" + ) shiny::showNotification("Creating results backup...") - zip_dir(dir = opt$results_dir, output_file = path) - if (file.exists(path)) { + + tmp_results_dir <- tempfile("dir") + opt$repository$download_results_dir(tmp_results_dir) + + tmp_zip_file <- tempfile(fileext = ".zip") + + zip_dir(dir = tmp_results_dir, output_file = tmp_zip_file) + opt$repository$deposit_file(tmp_zip_file, "deleted-results", zip_key) + + if (opt$repository$file_exists("deleted-results", zip_key)) { shiny::showNotification("Backup created.") - unlink(opt$results_dir, recursive = TRUE) + opt$repository$delete_folder("results") Sys.sleep(0.01) - dir.create(opt$results_dir) - dir.create(opt$supplementary_results_dir) + opt$repository$create_folder("results") shiny::showNotification("Deleted results.") } else { shiny::showNotification( @@ -444,7 +448,7 @@ admin_panel.handle_downloads.all_results.csv <- function(state, output, opt) { filename = "results.csv", content = function(file) { df <- tryCatch({ - df_all_results(opt$results_dir) %>% + df_all_results(opt) %>% list_cols_to_json() }, error = function(e) { print(e) @@ -484,8 +488,8 @@ zip_dir <- function(dir, output_file) { ) } -df_all_results <- function(results_dir) { - files <- list_results_files(results_dir, full.names = TRUE) +df_all_results <- function(opt) { + files <- get_results_files(opt, full.names = TRUE) if (length(files) == 0L) return(data.frame()) data <- lapply(files, readRDS) data_df <- lapply(data, as.data.frame) diff --git a/R/app_tester.R b/R/app_tester.R index ad50077e..816e3dd7 100644 --- a/R/app_tester.R +++ b/R/app_tester.R @@ -75,7 +75,6 @@ AppTester <- R6::R6Class( "AppTester", inherit = shinytest::ShinyDriver, public = list( - get_ui = function() { self$getAllValues(input = FALSE, output = FALSE, export = TRUE)$export$ui }, diff --git a/R/make-test.R b/R/make-test.R index 86e73799..39e90c80 100644 --- a/R/make-test.R +++ b/R/make-test.R @@ -24,10 +24,13 @@ make_test <- function(elts, opt = demo_options(), if (is.list(elts)) elts <- new_timeline(elts) check_elts(elts) check_opt(opt, elts) - shiny::shinyApp( + app <- shiny::shinyApp( ui = ui(opt = opt), server = server(elts = elts, opt = opt, custom_admin_panel = custom_admin_panel)) + app$opt <- opt + app$opt$repository$check() + app } check_opt <- function(opt, elts) { diff --git a/R/options.R b/R/options.R index b47d089c..52dff79b 100644 --- a/R/options.R +++ b/R/options.R @@ -109,7 +109,8 @@ test_options <- function(title, admin_password, logo_height = NULL, display = display_options(), allow_url_rewrite = TRUE, - advance_delay = 0) { + advance_delay = 0, + repository = LocalRespository$new(output_dir)) { stopifnot(is.character(title), is.scalar.character(admin_password), is.null.or(researcher_email, is.scalar.character), @@ -139,7 +140,8 @@ test_options <- function(title, admin_password, (is.scalar.character(logo_width) && is.scalar.character(logo_height)), is.list(display), is.scalar.logical(allow_url_rewrite), - is.scalar.numeric(advance_delay)) + is.scalar.numeric(advance_delay), + is(repository, "Repository")) # if (is.null(session_dir)) session_dir <- get_default_session_dir() if (!allow_url_rewrite && enable_resume_session) { @@ -191,10 +193,7 @@ test_options <- function(title, admin_password, } server_closed_msg <- enc2utf8(server_closed_msg) - results_dir <- file.path(output_dir, "results") - supplementary_results_dir <- file.path(results_dir, "supplementary") session_dir <- file.path(output_dir, "sessions") - results_archive_dir <- file.path(output_dir, "deleted-results") error_dir <- file.path(output_dir, "errors") list(title = title, @@ -218,11 +217,9 @@ test_options <- function(title, admin_password, allow_any_p_id_url = allow_any_p_id_url, force_p_id_from_url = force_p_id_from_url, enable_admin_panel = enable_admin_panel, + repository = repository, output_dir = output_dir, - results_dir = results_dir, - supplementary_results_dir = supplementary_results_dir, session_dir = session_dir, - results_archive_dir = results_archive_dir, error_dir = error_dir, closed_file = file.path(output_dir, "closed.txt"), session_timeout_min = session_timeout_min, @@ -379,8 +376,7 @@ test_permissions <- function(dir) { success } -OUTPUT_DIRS <- c("output_dir", "results_dir", "session_dir", - "results_archive_dir", "error_dir") +OUTPUT_DIRS <- c("output_dir", "session_dir", "error_dir") #' Check directories #' diff --git a/R/results.R b/R/results.R index 168e5a68..cef66658 100644 --- a/R/results.R +++ b/R/results.R @@ -155,3 +155,375 @@ save_result.state <- function(place, label, value) { place$passive$results <- save_result.results(place$passive$results, label, value) place } + +Repository <- R6::R6Class("Repository", public = list( + is_slow = NA, + dirs = c("results", "deleted-results"), + + initialize = function(is_slow) { + self$is_slow <- is_slow + }, + + deposit_results = function(results, key, ...) { + path <- tempfile() + saveRDS(results, path) + self$deposit_file(path, "results", key, ...) + }, + + # get_results = function(key, ...) { + # path <- tempfile() + # self$get_file(key, "results", path, ...) + # readRDS(path) + # }, + + prepare = function(...) stop("not implemented"), + deposit_file = function(local_file, dir, key, ...) stop("not implemented"), + get_file = function(dir, key, target_path, ...) stop("not implemented"), + file_exists = function(dir, key, ...) stop("not implemented"), + list_files = function(dir, ...) stop("not implemented"), + delete_file = function(dir, key, ...) stop("not implemented"), + delete_folder = function(dir, ...) stop("not implemented"), + + # May throw an error if the folder already exists + create_folder = function(dir, ...) stop("not implemented"), + + get_folder = function(dir, target_path, ...) stop("not implemented"), + + tabulate_results = function(include_pilot) { + df <- data.frame(key = self$list_files_with_pattern("results", "^id=.*\\.rds$"), stringsAsFactors = FALSE) + cols <- c("id", "p_id", "save_id", "pilot", "complete") + if (nrow(df) > 0L) { + df <- tidyr::extract( + df, "key", cols, + "(?:id=)([0-9]*)(?:&p_id=)([A-Za-z0-9]*)(?:&save_id=)([0-9]*)(?:&pilot=)([a-z]*)(?:&complete=)([a-z]*)", + remove = FALSE) + } else { + for (col in cols) df[[col]] <- character() + } + for (col in c("pilot", "complete")) + df[[col]] <- as.logical(toupper(df[[col]])) + for (col in c("id", "save_id")) + df[[col]] <- as.integer(df[[col]]) + for (col in c("p_id")) + df[[col]] <- as.character(df[[col]]) + if (!include_pilot) df <- df[!df$pilot, , drop = FALSE] + df + }, + + load_results = function(key) { + checkmate::qassert(key, "S1") + file <- tempfile() + self$get_file("results", key, file) + readRDS(file) + }, + + download_results_dir = function(target_path) { + self$get_folder("results", target_path) + }, + + load_all_results = function(include_pilot) { + df <- self$tabulate_results(include_pilot) + temp_dir <- tempfile("dir") + self$get_folder("results", temp_dir) + df$data <- lapply(df$key, function(key) readRDS(file.path(temp_dir, key))) + df + }, + + delete_results = function(key) { + self$delete_file("results", key) + }, + + count_results_excluding_participant = function(p_id) { + df <- self$tabulate_results(include_pilot = TRUE) + nrow(df) - sum(df$p_id == p_id) + }, + + list_files_with_pattern = function(dir, pattern) { + all_files <- self$list_files(dir) + grep(pattern, all_files, value = TRUE) + }, + + export_results_as_df = function() { + # this would be replacing df_all_results + stop("not implemented") + }, + + check = function() { + self$prepare() + + dir <- "results" + tmp_file_in <- tempfile() + tmp_file_out <- tempfile() + key <- uuid::UUIDgenerate() + file_content <- as.character(Sys.time()) + writeLines(file_content, tmp_file_in) + + testthat::expect( + !self$file_exists(dir, key), + failure_message = "repository$file_exists should return FALSE for non-existent files" + ) + testthat::expect( + !key %in% self$list_files(dir), + failure_message = paste0("repository$list_files() should not contain ", key, " yet") + ) + self$deposit_file(tmp_file_in, dir, key) + testthat::expect( + self$file_exists(dir, key), + failure_message = "repository$file_exists should return TRUE once a file has been deposited" + ) + testthat::expect( + key %in% self$list_files(dir), + failure_message = paste0("repository$list_files() should now contain ", key, ".") + ) + self$get_file(dir, key, tmp_file_out) + testthat::expect( + testthat::compare(readLines(tmp_file_out), file_content)$equal, + failure_message = "repository$get_file returned unexpected contents" + ) + + tmp_dir_2 <- tempfile("dir") + self$get_folder(dir, tmp_dir_2) + stopifnot(key %in% list.files(tmp_dir_2)) + + self$delete_file(dir, key) + testthat::expect( + !self$file_exists(dir, key), + failure_message = "repository$file_exists should return FALSE once a file has been deleted" + ) + } +)) + +#' @export +LocalRespository <- R6::R6Class( + "LocalRespository", + inherit = Repository, + + public = list( + root_dir = NA_character_, + + initialize = function(root_dir) { + super$initialize(is_slow = FALSE) + self$root_dir <- root_dir + }, + + prepare = function(...) { + for (dir in self$dirs) { + R.utils::mkdirs(file.path(self$root_dir, dir)) + } + }, + + path_in_repository = function(dir, key = NULL, ...) { + dir_path <- file.path(self$root_dir, dir) + if (is.null(key)) { + dir_path + } else { + file.path(dir_path, key) + } + }, + + deposit_file = function(local_file, dir, key, ...) { + file.copy(local_file, self$path_in_repository(dir, key)) + }, + + get_file = function(dir, key, target_path, ...) { + file.copy(self$path_in_repository(dir, key), target_path) + }, + + get_folder = function(dir, target_path, ...) { + dir_copy(self$path_in_repository(dir), target_path) + }, + + file_exists = function(dir, key, ...) { + file.exists(self$path_in_repository(dir, key)) + }, + + list_files = function(dir, ...) { + list.files(self$path_in_repository(dir)) + }, + + delete_file = function(dir, key) { + file.remove(self$path_in_repository(dir, key)) + }, + + delete_folder = function(dir) { + unlink(self$path_in_repository(dir), recursive = TRUE) + }, + + create_folder = function(dir, ...) { + dir.create(self$path_in_repository(dir)) + } + ) +) + +#' @export +DropboxRepository <- R6::R6Class( + "DropboxRepository", + inherit = Repository, + + public = list( + root_dir = NA_character_, + token_path = NA_character_, + token = NULL, + dropbox_key = NA_character_, + dropbox_secret = NA_character_, + + initialize = function(root_dir, + token_path = "dropbox-token.rds", + dropbox_key = "mmhfsybffdom42w", + dropbox_secret = "l8zeqqqgm1ne5z0") { + super$initialize(is_slow = TRUE) + self$root_dir <- root_dir + self$token_path <- token_path + + if (!file.exists(self$token_path)) + stop("Couldn't find any tokens at ", self$token_path, ".") + + self$token <- readRDS(self$token_path) + + # It should work just passing the token as the dtoken argument + # to the rdrop2 calls. However there is a bug in drop_create + # where dtoken is allowed. We therefore use drop_auth to + # overcome this. + rdrop2::drop_auth(rdstoken = self$token_path) + + self$dropbox_key <- dropbox_key + self$dropbox_secret <- dropbox_secret + }, + + check = function(...) { + message("Checking that Dropbox repository is accessible...") + super$check(...) + message("Dropbox check complete.") + }, + + prepare = function(...) { + # self$authenticate() + if (!self$dropbox_exists(self$root_dir)) { + stop("directory '", self$root_dir, "' not found in Dropbox, ", + "you must create this manually") + } + for (dir in self$dirs) { + full_path <- file.path(self$root_dir, dir) + if (!self$dropbox_exists(full_path)) { + rdrop2::drop_create( + full_path, + dtoken = self$token + ) + } + } + }, + + authenticate = function() { + rdrop2::drop_auth(rdstoken = self$token_path, + cache = FALSE, + key = self$dropbox_key, + secret = self$dropbox_secret) + }, + + dropbox_exists = function(path) { + # self$authenticate() + tryCatch( + rdrop2::drop_exists(path, dtoken = self$token), + error = function(e) { + if (e$message == "Conflict (HTTP 409).") FALSE else stop(e) + } + ) + }, + + path_in_repository = function(dir, key = NULL) { + dir_path <- file.path(self$root_dir, dir) + if (is.null(key)) { + dir_path + } else { + file.path(dir_path, key) + } + }, + + get_dropbox_token = function() { + if (!file.exists(self$token_path)) + stop("Couldn't find ", self$token_path, ". Do you need to call repo$new_dropbox_token()?") + readRDS(self$token_path) + }, + + # @inheritParams rdrop2::drop_auth + new_dropbox_token = function() { + token <- rdrop2::drop_auth( + new_user = TRUE, + key = self$dropbox_key, + secret = self$dropbox_secret + ) + saveRDS(token, self$token_path) + }, + + deposit_file = function(local_file, dir, key, ...) { + # self$authenticate() + tmp_dir <- tempfile("dir") + R.utils::mkdirs(tmp_dir) + new_local_path <- file.path(tmp_dir, key) + file.copy(local_file, new_local_path) + rdrop2::drop_upload( + new_local_path, + file.path(self$root_dir, dir), + autorename = FALSE, + dtoken = self$token + ) + }, + + get_file = function(dir, key, target_path, ...) { + # self$authenticate() + rdrop2::drop_download( + self$path_in_repository(dir, key), + target_path, + overwrite = TRUE, + dtoken = self$token + ) + }, + + get_folder = function(dir, target_path, ...) { + dropbox_download_folder( + self$path_in_repository(dir), + target_path, + dtoken = self$get_dropbox_token() + ) + }, + + file_exists = function(dir, key, ...) { + self$dropbox_exists(self$path_in_repository(dir, key)) + }, + + list_files = function(dir, ...) { + x <- rdrop2::drop_dir( + self$path_in_repository(dir), + dtoken = self$token + ) + if (nrow(x) == 0) { + character() + } else { + x$name + } + }, + + delete_file = function(dir, key) { + # self$authenticate() + rdrop2::drop_delete( + self$path_in_repository(dir, key), + dtoken = self$token + ) + }, + + delete_folder = function(dir) { + # self$authenticate() + rdrop2::drop_delete( + self$path_in_repository(dir), + dtoken = self$token + ) + }, + + create_folder = function(dir, ...) { + rdrop2::drop_create( + self$path_in_repository(dir), + dtoken = self$token + ) + } + ) +) diff --git a/R/server.R b/R/server.R index f2410fc4..6f92e3d4 100644 --- a/R/server.R +++ b/R/server.R @@ -75,7 +75,7 @@ setup_session <- function(state, input, output, elts, session, opt) { } max <- opt$max_num_participants if (!is.null(max)) { - results <- tabulate_results(opt, include_pilot = FALSE) + results <- opt$repository$tabulate_results(include_pilot = FALSE) num_complete <- sum(results$complete) if (num_complete + 1L > max) { error(state) <- opt$max_participants_msg diff --git a/R/state.R b/R/state.R index 6d33e5f0..e552fae7 100644 --- a/R/state.R +++ b/R/state.R @@ -55,7 +55,7 @@ STATE <- R6::R6Class( time_started = as.POSIXct(NA), num_restarts = 0L, save_id = 1L, - previous_save_path = NULL, + previous_results_key = NULL, language = as.character(NA), demo = FALSE, error = NULL, @@ -118,15 +118,15 @@ allow_url_rewrite <- function(state) { state$passive$allow_url_rewrite } -previous_save_path <- function(state) { +previous_results_key <- function(state) { stopifnot(is(state, "state")) - state$passive$previous_save_path + state$passive$previous_results_key } -`previous_save_path<-` <- function(state, value) { +`previous_results_key<-` <- function(state, value) { stopifnot(is(state, "state"), is.scalar.character(value)) - state$passive$previous_save_path <- value + state$passive$previous_results_key <- value state } diff --git a/R/statistics.R b/R/statistics.R index b82e1822..5b6c9bc8 100644 --- a/R/statistics.R +++ b/R/statistics.R @@ -12,31 +12,9 @@ # # # "id=1&p_id=PeterHarrison&save_id=1&final=true.rds" -list_results_files <- function(results_dir, full.names = FALSE) { +get_results_files <- function(opt, full.names = FALSE, target_path = tempfile("dir")) { + opt$repository$get_folder("results", target_path) pattern <- "^id=.*\\.rds$" - list.files(results_dir, pattern = pattern, full.names = full.names) + list.files(target_path, pattern = pattern, full.names = full.names) } -tabulate_results <- function(opt, include_pilot) { - stopifnot(is.scalar.logical(include_pilot)) - df <- data.frame(file = list_results_files(opt$results_dir), - stringsAsFactors = FALSE) - cols <- c("id", "p_id", "save_id", "pilot", "complete") - if (nrow(df) > 0L) { - df <- tidyr::extract( - df, "file", cols, - "(?:id=)([[0-9]]*)(?:&p_id=)([[A-Za-z0-9_]]*)(?:&save_id=)([[0-9]]*)(?:&pilot=)([[a-z]]*)(?:&complete=)([[a-z]]*)", - remove = FALSE) - } else { - for (col in cols) df[[col]] <- character() - } - for (col in c("pilot", "complete")) - df[[col]] <- as.logical(toupper(df[[col]])) - for (col in c("id", "save_id")) - df[[col]] <- as.integer(df[[col]]) - for (col in c("p_id")) - df[[col]] <- as.character(df[[col]]) - if (!include_pilot) df <- df[!df$pilot, , drop = FALSE] - df$full_file <- file.path(opt$results_dir, df$file) - df -} diff --git a/R/supplementary-results.R b/R/supplementary-results.R deleted file mode 100644 index bca154fe..00000000 --- a/R/supplementary-results.R +++ /dev/null @@ -1,22 +0,0 @@ -#' Supplementary results directory -#' -#' Returns the location of the supplementary results directory. -#' The supplementary results directory provides a place to store -#' results in addition to the results saved by the psychTestR -#' function \code{save_results_to_disk}. -#' The researcher has full control over what to put in the supplementary -#' results directory. -#' @note The supplementary results directory is typically stored -#' within the main results directory. It will be deleted if the -#' researcher presses the 'Delete all results' button in the admin panel. -#' @param opt Options list as created by \code{test_options()}. -#' @return Character scalar identifying the (relative) -#' path to the supplementary results directory. -#' @export -get_supplementary_results_dir <- function(opt) { - x <- opt$supplementary_results_dir - if (is.null(x)) stop("supplementary_results_dir missing from options list") - if (!is.scalar.character(x)) stop("wrong format for supplementary_results_dir") - R.utils::mkdirs(x) - x -} diff --git a/R/test-elements.R b/R/test-elements.R index 0e520c63..c97cc94b 100644 --- a/R/test-elements.R +++ b/R/test-elements.R @@ -1125,25 +1125,25 @@ elt_save_results_to_disk <- function(complete) { #' @param ... Further arguments are allowed but ignored (for back-compatibility). #' @export save_results_to_disk <- function(complete, state, opt, ...) { - dir <- opt$results_dir - R.utils::mkdirs(dir) - if (!test_permissions(dir)) stop( - "Insufficient permissions to write to directory ", dir, ".") - if (!is.null(previous_save_path(state))) unlink(previous_save_path(state)) - filename <- save_results_to_disk.get_filename(state, dir, complete) - path <- file.path(dir, filename) + num_previous_results <- opt$repository$count_results_excluding_participant(state$passive$p_id) + + key <- save_results_to_disk.get_key(state, complete, num_previous_results) results <- get_results(state, complete = complete, add_session_info = TRUE) - saveRDS(results, path) - if (complete) notify_new_participant(opt) - previous_save_path(state) <- path + + opt$repository$deposit_results(results, key) + if (!is.null(previous_results_key(state))) + opt$repository$delete_results(previous_results_key(state)) + + previous_results_key(state) <- key save_id(state) <- save_id(state) + 1L + + if (complete) notify_new_participant(opt) } -save_results_to_disk.get_filename <- function(state, dir, complete) { +save_results_to_disk.get_key <- function(state, complete, num_previous_results) { sprintf( "id=%s&p_id=%s&save_id=%s&pilot=%s&complete=%s.rds", - id = format(length(list_results_files(dir)) + 1L, - scientific = FALSE), + id = format(num_previous_results + 1, scientific = FALSE), p_id = format(p_id(state), scientific = FALSE), save_id = format(save_id(state), scientific = FALSE), pilot = tolower(pilot(state)), @@ -1154,7 +1154,7 @@ notify_new_participant <- function(opt) { enabled <- opt$notify_new_participant stopifnot(is.scalar.logical(enabled)) if (enabled) { - results <- tabulate_results(opt, include_pilot = FALSE) + results <- opt$repository$tabulate_results(include_pilot = FALSE) num_complete <- sum(results$complete) title <- sprintf("N = %i", num_complete) msg <- sprintf("Participant number %i%s just completed the experiment '%s'.", diff --git a/R/utils.R b/R/utils.R index f12487ba..6d808fdf 100644 --- a/R/utils.R +++ b/R/utils.R @@ -65,3 +65,83 @@ assert_global_is_null <- function(key, state) { stop("global variable <", key, "> in was not NULL") } } + +remove_trailing_slashes <- function(x) gsub("/*$", "", x) + +dir_copy <- function(dir, target_path) { + dir <- remove_trailing_slashes(dir) + target_path <- remove_trailing_slashes(target_path) + target_parent <- dirname(target_path) + + tmp_dir <- tempfile("dir") + dir.create(tmp_dir) + + old_dir_name <- basename(dir) + new_dir_name <- basename(target_path) + + file.copy( + from = dir, + to = tmp_dir, + recursive = TRUE + ) + + file.rename(file.path(tmp_dir, old_dir_name), + file.path(tmp_dir, new_dir_name)) + + file.copy( + from = file.path(tmp_dir, new_dir_name), + to = target_parent, + recursive = TRUE + ) +} + +dropbox_download_folder <- function( + path, + local_path, + unzip = TRUE, + overwrite = FALSE, + dtoken = rdrop2::drop_auth(), + progress = interactive(), + verbose = interactive()) { + + if (unzip && dir.exists(local_path)) + stop("a directory already exists at ", local_path) + if (!unzip && file.exists(local_path)) + stop("a file already exists at ", local_path) + + path <- remove_trailing_slashes(path) + local_path <- remove_trailing_slashes(local_path) + local_parent <- dirname(local_path) + original_dir_name <- basename(path) + download_path <- if (unzip) tempfile("dir") else local_path + + if (!dir.exists(local_parent)) stop("target parent directory ", local_parent, " not found") + + url <- "https://content.dropboxapi.com/2/files/download_zip" + req <- httr::POST( + url = url, + httr::config(token = dtoken), + httr::add_headers( + `Dropbox-API-Arg` = jsonlite::toJSON(list(path = paste0("/", path)), + auto_unbox = TRUE)), + if (progress) httr::progress(), + httr::write_disk(download_path, overwrite) + ) + httr::stop_for_status(req) + if (verbose) { + size <- file.size(download_path) + class(size) <- "object_size" + message(sprintf("Downloaded %s to %s: %s on disk", path, + download_path, format(size, units = "auto"))) + } + if (unzip) { + if (verbose) message("Unzipping file...") + new_dir_name <- basename(local_path) + unzip_path <- tempfile("dir") + unzip(download_path, exdir = unzip_path) + dir_copy(file.path(unzip_path, original_dir_name), + local_path) + } + + TRUE +} diff --git a/dropbox-token.rds b/dropbox-token.rds new file mode 100644 index 00000000..961be734 Binary files /dev/null and b/dropbox-token.rds differ diff --git a/inst/js/confirm-clear-sessions.js b/inst/js/confirm-clear-sessions.js index 78d830d6..93dae5ba 100644 --- a/inst/js/confirm-clear-sessions.js +++ b/inst/js/confirm-clear-sessions.js @@ -1,5 +1,7 @@ +var skip_confirm = false; + confirm_clear_sessions = function() { - if (confirm("Are you sure you want to clear all session files?")) { + if (skip_confirm || confirm("Are you sure you want to clear all session files?")) { Shiny.onInputChange("admin_panel.confirm_clear_sessions", performance.now()); } }; diff --git a/inst/js/confirm-delete-errors.js b/inst/js/confirm-delete-errors.js index b17e8ab4..07f1906e 100644 --- a/inst/js/confirm-delete-errors.js +++ b/inst/js/confirm-delete-errors.js @@ -1,5 +1,7 @@ +var skip_confirm = false; + confirm_delete_errors = function() { - if (confirm("Are you sure you want to delete all error logs?")) { + if (skip_confirm || confirm("Are you sure you want to delete all error logs?")) { Shiny.onInputChange("admin_panel.confirm_delete_errors", performance.now()); } }; diff --git a/inst/js/confirm-delete-results.js b/inst/js/confirm-delete-results.js index b504c5f4..196977ba 100644 --- a/inst/js/confirm-delete-results.js +++ b/inst/js/confirm-delete-results.js @@ -1,5 +1,7 @@ +var skip_confirm = false; + confirm_delete_results = function() { - if (confirm("Are you sure you want to delete all results?")) { + if (skip_confirm || confirm("Are you sure you want to delete all results?")) { Shiny.onInputChange("admin_panel.confirm_delete_results", performance.now()); } }; diff --git a/inst/js/confirm-resume-session.js b/inst/js/confirm-resume-session.js index 3fbe3013..42938ba5 100644 --- a/inst/js/confirm-resume-session.js +++ b/inst/js/confirm-resume-session.js @@ -1,7 +1,9 @@ +var skip_confirm = false; + confirm_resume_session = function() { if (confirm("Resuming ongoing testing session.\n" + "Please click 'OK' to confirm, " + - "or click 'Cancel' to restart as a new user.")) { + "or click 'Cancel' to restart as a new user.") || skip_confirm) { return(true); } else { hide_content(); diff --git a/man/AppTester.Rd b/man/AppTester.Rd index d32d07b8..891281f0 100644 --- a/man/AppTester.Rd +++ b/man/AppTester.Rd @@ -99,8 +99,10 @@ which you can install with \code{\link[shinytest]{installDependencies}}. \item \out{}\href{../../shinytest/html/ShinyDriver.html#method-getAppFilename}{\code{shinytest::ShinyDriver$getAppFilename()}}\out{} \item \out{}\href{../../shinytest/html/ShinyDriver.html#method-getDebugLog}{\code{shinytest::ShinyDriver$getDebugLog()}}\out{} \item \out{}\href{../../shinytest/html/ShinyDriver.html#method-getEventLog}{\code{shinytest::ShinyDriver$getEventLog()}}\out{} +\item \out{}\href{../../shinytest/html/ShinyDriver.html#method-getRelativePathToApp}{\code{shinytest::ShinyDriver$getRelativePathToApp()}}\out{} \item \out{}\href{../../shinytest/html/ShinyDriver.html#method-getSnapshotDir}{\code{shinytest::ShinyDriver$getSnapshotDir()}}\out{} \item \out{}\href{../../shinytest/html/ShinyDriver.html#method-getSource}{\code{shinytest::ShinyDriver$getSource()}}\out{} +\item \out{}\href{../../shinytest/html/ShinyDriver.html#method-getTestsDir}{\code{shinytest::ShinyDriver$getTestsDir()}}\out{} \item \out{}\href{../../shinytest/html/ShinyDriver.html#method-getTitle}{\code{shinytest::ShinyDriver$getTitle()}}\out{} \item \out{}\href{../../shinytest/html/ShinyDriver.html#method-getUrl}{\code{shinytest::ShinyDriver$getUrl()}}\out{} \item \out{}\href{../../shinytest/html/ShinyDriver.html#method-getValue}{\code{shinytest::ShinyDriver$getValue()}}\out{} @@ -122,6 +124,8 @@ which you can install with \code{\link[shinytest]{installDependencies}}. \item \out{}\href{../../shinytest/html/ShinyDriver.html#method-takeScreenshot}{\code{shinytest::ShinyDriver$takeScreenshot()}}\out{} \item \out{}\href{../../shinytest/html/ShinyDriver.html#method-uploadFile}{\code{shinytest::ShinyDriver$uploadFile()}}\out{} \item \out{}\href{../../shinytest/html/ShinyDriver.html#method-waitFor}{\code{shinytest::ShinyDriver$waitFor()}}\out{} +\item \out{}\href{../../shinytest/html/ShinyDriver.html#method-waitForShiny}{\code{shinytest::ShinyDriver$waitForShiny()}}\out{} +\item \out{}\href{../../shinytest/html/ShinyDriver.html#method-waitForValue}{\code{shinytest::ShinyDriver$waitForValue()}}\out{} } \out{} } diff --git a/man/get_supplementary_results_dir.Rd b/man/get_supplementary_results_dir.Rd deleted file mode 100644 index eb59b53d..00000000 --- a/man/get_supplementary_results_dir.Rd +++ /dev/null @@ -1,28 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/supplementary-results.R -\name{get_supplementary_results_dir} -\alias{get_supplementary_results_dir} -\title{Supplementary results directory} -\usage{ -get_supplementary_results_dir(opt) -} -\arguments{ -\item{opt}{Options list as created by \code{test_options()}.} -} -\value{ -Character scalar identifying the (relative) -path to the supplementary results directory. -} -\description{ -Returns the location of the supplementary results directory. -The supplementary results directory provides a place to store -results in addition to the results saved by the psychTestR -function \code{save_results_to_disk}. -The researcher has full control over what to put in the supplementary -results directory. -} -\note{ -The supplementary results directory is typically stored -within the main results directory. It will be deleted if the -researcher presses the 'Delete all results' button in the admin panel. -} diff --git a/man/slider_page.Rd b/man/slider_page.Rd index c36f2d80..d0f5347e 100644 --- a/man/slider_page.Rd +++ b/man/slider_page.Rd @@ -73,10 +73,10 @@ according to some simple heuristics.} \item{animate}{\code{TRUE} to show simple animation controls with default settings; \code{FALSE} not to; or a custom settings list, such as those -created using \code{\link[shiny:animationOptions]{animationOptions()}}.} +created using \code{\link[shiny:sliderInput]{animationOptions()}}.} \item{width}{The width of the input, e.g. \code{'400px'}, or \code{'100\%'}; -see \code{\link[shiny:validateCssUnit]{validateCssUnit()}}.} +see \code{\link[shiny:reexports]{validateCssUnit()}}.} \item{sep}{Separator between thousands places in numbers.} @@ -88,7 +88,7 @@ see \code{\link[shiny:validateCssUnit]{validateCssUnit()}}.} format string, to be passed to the Javascript strftime library. See \url{https://github.com/samsonjs/strftime} for more details. The allowed format specifications are very similar, but not identical, to those for R's -\code{\link[base:strftime]{base::strftime()}} function. For Dates, the default is \code{"\%F"} +\code{\link[base:strptime]{base::strftime()}} function. For Dates, the default is \code{"\%F"} (like \code{"2015-07-01"}), and for POSIXt, the default is \code{"\%F \%T"} (like \code{"2015-07-01 15:32:10"}).} diff --git a/man/test_options.Rd b/man/test_options.Rd index aeaae116..5af8a4be 100644 --- a/man/test_options.Rd +++ b/man/test_options.Rd @@ -34,7 +34,8 @@ test_options( logo_height = NULL, display = display_options(), allow_url_rewrite = TRUE, - advance_delay = 0 + advance_delay = 0, + repository = LocalRespository$new(output_dir) ) } \arguments{ diff --git a/man/trigger_button.Rd b/man/trigger_button.Rd index 1b1c6fa8..77175eca 100644 --- a/man/trigger_button.Rd +++ b/man/trigger_button.Rd @@ -23,7 +23,7 @@ you could also use any other HTML, like an image.} \item{icon}{An optional \code{\link[shiny:icon]{icon()}} to appear on the button.} \item{width}{The width of the input, e.g. \code{'400px'}, or \code{'100\%'}; -see \code{\link[shiny:validateCssUnit]{validateCssUnit()}}.} +see \code{\link[shiny:reexports]{validateCssUnit()}}.} \item{enable_after}{Number of seconds after which responses should be permitted.} diff --git a/tests/testthat/test-results.R b/tests/testthat/test-results.R index 025ebfcb..0dbfa47f 100644 --- a/tests/testthat/test-results.R +++ b/tests/testthat/test-results.R @@ -1,5 +1,26 @@ context("test_results") +test_that("repository_local", { + opt <- demo_options() + tmp_dir <- tempfile("dir") + R.utils::mkdirs(tmp_dir) + withr::with_dir(tmp_dir, opt$repository$check()) +}) + +test_that("repository_dropbox", { + skip_on_cran() + skip_on_appveyor() + skip_on_travis() + skip_if_offline() + + token_path <- "~/dropbox-token.rds" + opt <- demo_options(repository = DropboxRepository$new(token_path = token_path, + root_dir = "Archive/psychTestR-tests")) + tmp_dir <- tempfile("dir") + R.utils::mkdirs(tmp_dir) + withr::with_dir(tmp_dir, opt$repository$check()) +}) + test_that("main", { skip_on_cran() @@ -10,20 +31,19 @@ test_that("main", { app$click(answer) } - invisible(app) + app } run_participants <- function(...) { - for (answers in list(...)) { - app <- run_participant(answers) - } - app + answers <- list(...) + lapply(answers, run_participant) } - app <- run_participants(c(1, 2, 3, 4), - c(2, 4), - c(4, 4, 3), - c(3, 3, 1, 2)) + apps <- + run_participants(c(1, 2, 3, 4), + c(2, 4), + c(4, 4, 3), + c(3, 3, 1, 2)) df <- df_all_results("apps/results/output/results") @@ -33,6 +53,26 @@ test_that("main", { expect_equal(df$results.q3, c(3, NA, 3, 1) %>% as.character()) expect_equal(df$results.q4, c(4, NA, NA, 2) %>% as.character()) - app$stop() + # Try deleting results + app <- apps[[1]] + app$click("admin_login_trigger") + Sys.sleep(0.2) + app$set_inputs(admin_password = "demo") + app$click("submit_admin_password") + app$executeScript("skip_confirm = true") + app$click("admin_panel.delete_results") + expect_equal(nrow(df_all_results("apps/results/output/results")), 0) + + deleted_zip <- normalizePath(list.files("apps/results/output/deleted-results", + pattern = "\\.zip$", + full.names = TRUE)) + tmp_dir <- tempfile("dir") + R.utils::mkdirs(tmp_dir) + withr::with_dir(tmp_dir, unzip(zipfile = deleted_zip)) + + recovered_df <- df_all_results(file.path(tmp_dir, "results")) + expect_equal(df, recovered_df) + # Clean up + lapply(apps, function(x) x$stop()) }) diff --git a/todo.txt b/todo.txt new file mode 100644 index 00000000..f00dbe90 --- /dev/null +++ b/todo.txt @@ -0,0 +1,4 @@ +finish removing results_dir from statistics / admin panel +implement asynchronous code block +make Dropbox upload asynchronous +integrate deleted-results with Dropbox