diff --git a/DESCRIPTION b/DESCRIPTION index 93b0b437..887a8229 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: surveydown Title: Markdown-Based Surveys Using 'Quarto' and 'shiny' -Version: 0.6.0 +Version: 0.6.2 Authors@R: c( person(given = "John Paul", family = "Helveston", diff --git a/NEWS.md b/NEWS.md index e07b4c8f..c28cc75f 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,14 @@ # surveydown (development version) +# surveydowb 0.6.2 + +- Update: Now `ignore = TRUE` in `sd_server()` will turn off cookies, regardless of the value of `use_cookies`. + +# surveydown 0.6.1 + +- Cookies now contain not only the `session_id` but a complete JSON object of the current page questions, answers, and latest time stamp. +- Increased checkpoints of data updates. Now they are: upon starting, upon proceeding to the next page, upon submitting rating or clicking the exit button, and finally, upon abruptly quitting the survey. + # surveydown 0.6.0 - Cookies functionality fully working, applied to both local testing and online db. diff --git a/R/config.R b/R/config.R index ae68e970..acd374ef 100644 --- a/R/config.R +++ b/R/config.R @@ -559,7 +559,7 @@ check_ids <- function(page_ids, question_ids) { } # Check for restricted IDs - restricted_ids <- c("session_id", "time_start", "time_end", "exit_survey_rating") + restricted_ids <- c("session_id", "time_start", "time_end", "exit_survey_rating", "current_page") used_restricted_ids <- intersect(restricted_ids, question_ids) if (length(used_restricted_ids) > 0) { stop("Restricted question IDs found: ", paste(used_restricted_ids, collapse = ", "), diff --git a/R/server.R b/R/server.R index feaf65a7..ab11cacd 100644 --- a/R/server.R +++ b/R/server.R @@ -432,6 +432,52 @@ sd_server <- function( }) }) + # Observer to update cookies with answers + shiny::observe({ + # Get current page ID + page_id <- current_page_id() + + # Get all questions for current page + page_questions <- names(input)[names(input) %in% question_ids] + + # Create answers list + answers <- list() + last_timestamp <- NULL + max_index <- 0 + + for (q_id in page_questions) { + # Get question value + val <- input[[q_id]] + if (!is.null(val)) { + answers[[q_id]] <- val + + # If question was interacted with, check its position + if (!is.null(input[[paste0(q_id, "_interacted")]])) { + # Find this question's index in the overall sequence + current_index <- match(q_id, question_ids) + if (!is.na(current_index) && current_index > max_index) { + max_index <- current_index + last_timestamp <- list( + id = paste0("time_q_", q_id), + time = get_utc_timestamp() + ) + } + } + } + } + + # Send to client to update cookie + if (length(answers) > 0 && !is.null(db)) { # Only update cookies in db mode + page_data <- list( + answers = answers, + last_timestamp = last_timestamp + ) + session$sendCustomMessage("setAnswerData", + list(pageId = page_id, + pageData = page_data)) + } + }) + # 6. Page rendering ---- # Create reactive values for the start page ID @@ -1302,11 +1348,9 @@ admin_enable <- function(input, output, session, db) { data <- DBI::dbReadTable(db$db, paste0(db$table, "_admin_table")) #Read table value in, change it from true to false - #Add in sd_server if(survey_paused == TRUE) #Create and display a blank page that says the survey is pause - }) # Download Data button functionality @@ -1333,11 +1377,41 @@ get_local_data <- function() { return(NULL) } +get_cookie_data <- function(session, current_page_id) { + # Get stored answer data from input + answer_data <- session$input$stored_answer_data + + if (is.null(answer_data) || !length(answer_data)) { + return(NULL) + } + + # Extract data for current page + page_data <- answer_data[[current_page_id]] + if (is.null(page_data)) { + return(NULL) + } + + # Return the full page data structure including answers and timestamps + return(page_data) +} + +restore_current_page_values <- function(restore_data, session, page_filter = NULL) { + for (col in names(restore_data)) { + # Skip special columns + if (!col %in% c("session_id", "current_page", "time_start", "time_end")) { + val <- restore_data[[col]] + if (!is.null(val) && !is.na(val) && val != "") { + session$sendInputMessage(col, list(value = val, priority = "event")) + } + } + } +} + handle_data_restoration <- function(session_id, db, session, current_page_id, start_page, - question_ids, question_ts_ids, progress_updater) { + question_ids, question_ts_ids, progress_updater) { if (is.null(session_id)) return(NULL) - # Get data using sd_get_data or local CSV + # Get data based on source if (!is.null(db)) { all_data <- sd_get_data(db) } else { @@ -1351,9 +1425,8 @@ handle_data_restoration <- function(session_id, db, session, current_page_id, st if (nrow(restore_data) == 0) return(NULL) - # Rest of the function remains the same... shiny::isolate({ - # Restore page state + # 1. Restore page state (using restore_data) if ("current_page" %in% names(restore_data)) { restored_page <- restore_data[["current_page"]] if (!is.null(restored_page) && !is.na(restored_page) && nchar(restored_page) > 0) { @@ -1364,17 +1437,30 @@ handle_data_restoration <- function(session_id, db, session, current_page_id, st } else { current_page_id(start_page) } + + # Get cookie data after page state is set + answer_data <- NULL + if (!is.null(db)) { + answer_data <- get_cookie_data(session, current_page_id()) + } - # Find the last answered question for progress bar + # 2. Find the last answered question for progress bar last_index <- 0 - for (i in seq_along(question_ids)) { - q_id <- question_ids[i] - ts_id <- question_ts_ids[i] - - if (ts_id %in% names(restore_data)) { - ts_val <- restore_data[[ts_id]] - if (!is.null(ts_val) && !is.na(ts_val) && ts_val != "") { - last_index <- i + if (!is.null(db) && !is.null(answer_data) && !is.null(answer_data$last_timestamp)) { + # Use last timestamp from cookie data in DB mode + last_ts_id <- answer_data$last_timestamp$id + # Find the index of this timestamp ID in our question_ts_ids + last_index <- match(last_ts_id, question_ts_ids) + if (is.na(last_index)) last_index <- 0 + } else { + # Use restore_data for local CSV mode + for (i in seq_along(question_ids)) { + ts_id <- question_ts_ids[i] + if (ts_id %in% names(restore_data)) { + ts_val <- restore_data[[ts_id]] + if (length(ts_val) == 1 && !is.null(ts_val) && !is.na(ts_val) && ts_val != "") { + last_index <- i + } } } } @@ -1383,14 +1469,18 @@ handle_data_restoration <- function(session_id, db, session, current_page_id, st progress_updater(last_index) } - for (col in names(restore_data)) { - if (!col %in% c("session_id", "current_page", "time_start", "time_end")) { - val <- restore_data[[col]] - if (!is.null(val) && !is.na(val) && val != "") { - all_data[[col]] <- val + # 3. Restore question values + if (!is.null(db) && !is.null(answer_data) && !is.null(answer_data$answers)) { + # Use answer data from cookies for current page + for (col in names(answer_data$answers)) { + val <- answer_data$answers[[col]] + if (length(val) == 1 && !is.null(val) && !is.na(val) && val != "") { session$sendInputMessage(col, list(value = val, priority = "event")) } } + } else { + # Fall back to restore_data + restore_current_page_values(restore_data, session) } }) return(restore_data) @@ -1399,7 +1489,12 @@ handle_data_restoration <- function(session_id, db, session, current_page_id, st handle_sessions <- function(session_id, db = NULL, session, input, time_start, start_page, current_page_id, question_ids, question_ts_ids, progress_updater, use_cookies = TRUE) { - # Check 1: Cookies enabled? + # Check 1: if db is null, don't use cookies + if (is.null(db)) { + use_cookies <- FALSE + } + + # Check 2: Cookies enabled? if (!use_cookies) { return(session_id) } @@ -1409,13 +1504,14 @@ handle_sessions <- function(session_id, db = NULL, session, input, time_start, # Do the cookie check synchronously in a reactive context shiny::isolate({ - # Check 2: Cookie exists and is valid? + + # Check 3: Cookie exists and is valid? stored_id <- shiny::reactiveValuesToList(input)$stored_session_id if (!is.null(stored_id) && nchar(stored_id) > 0 && - # Check 3: Either DB connection exists or preview_data.csv is writable + # Check 4: Either DB connection exists or preview_data.csv is writable (!is.null(db) || (file.exists("preview_data.csv") && file.access("preview_data.csv", 2) == 0))) { - # Check 4: Session exists in DB or preview data? + # Check 5: Session exists in DB or preview data? restore_data <- handle_data_restoration( stored_id, db, session, current_page_id, start_page, question_ids, question_ts_ids, @@ -1423,14 +1519,17 @@ handle_sessions <- function(session_id, db = NULL, session, input, time_start, ) if (!is.null(restore_data)) { + # All checks passed - use stored session final_session_id <- stored_id session$sendCustomMessage("setCookie", list(sessionId = stored_id)) } else { + # Session not in DB - use new session session$sendCustomMessage("setCookie", list(sessionId = session_id)) } } else { + # No cookie or no DB connection - use new session session$sendCustomMessage("setCookie", list(sessionId = session_id)) } diff --git a/inst/js/cookies.js b/inst/js/cookies.js index e951e9c8..2b668867 100644 --- a/inst/js/cookies.js +++ b/inst/js/cookies.js @@ -1,4 +1,3 @@ -// cookies.js const surveydownCookies = { set: function(sessionId) { try { @@ -8,16 +7,14 @@ const surveydownCookies = { ";expires=" + date.toUTCString() + ";path=/;SameSite=Strict"; document.cookie = cookieValue; - console.log("Cookie set successfully:", cookieValue); - console.log("Current cookies:", document.cookie); + console.log("Session cookie set successfully:", cookieValue); } catch (e) { - console.error("Error setting cookie:", e); + console.error("Error setting session cookie:", e); } }, get: function() { try { - console.log("All cookies:", document.cookie); const name = "surveydown_session="; const decodedCookie = decodeURIComponent(document.cookie); const ca = decodedCookie.split(';'); @@ -27,65 +24,86 @@ const surveydownCookies = { c = c.substring(1); } if (c.indexOf(name) == 0) { - const sessionId = c.substring(name.length); - console.log("Found session cookie:", sessionId); - return sessionId; + return c.substring(name.length); } } - console.log("No session cookie found"); return null; } catch (e) { - console.error("Error getting cookie:", e); + console.error("Error getting session cookie:", e); + return null; + } + }, + + setAnswerData: function(pageId, pageData) { + try { + // Only store current page data instead of accumulating + let currentData = {}; + currentData[pageId] = pageData; // pageData contains both answers and timestamps + + const date = new Date(); + date.setTime(date.getTime() + (30 * 24 * 60 * 60 * 1000)); + const cookieValue = "surveydown_answers=" + JSON.stringify(currentData) + + ";expires=" + date.toUTCString() + + ";path=/;SameSite=Strict"; + document.cookie = cookieValue; + console.log("Answer data set for page:", pageId); + + // Update Shiny input + Shiny.setInputValue('stored_answer_data', currentData, {priority: 'event'}); + } catch (e) { + console.error("Error setting answer data:", e); + } + }, + + getAnswerData: function() { + try { + const name = "surveydown_answers="; + const decodedCookie = decodeURIComponent(document.cookie); + const ca = decodedCookie.split(';'); + for(let i = 0; i < ca.length; i++) { + let c = ca[i]; + while (c.charAt(0) == ' ') { + c = c.substring(1); + } + if (c.indexOf(name) == 0) { + const data = JSON.parse(c.substring(name.length)); + return data; + } + } + return null; + } catch (e) { + console.error("Error getting answer data:", e); return null; } } }; -// Set cookie handler Shiny.addCustomMessageHandler('setCookie', function(message) { - console.log("Received setCookie message:", message); if (message.sessionId) { surveydownCookies.set(message.sessionId); - // Verify the cookie was set - const verifyId = surveydownCookies.get(); - console.log("Verified cookie after setting:", verifyId); } }); -// Add handler for triggering input changes -Shiny.addCustomMessageHandler('triggerInputChange', function(message) { - const inputEl = $('#' + message.inputId); - if (inputEl.length) { - // For radio buttons and checkboxes - if (inputEl.is(':radio') || inputEl.is(':checkbox')) { - inputEl.prop('checked', true).trigger('change'); - } - // For select elements - else if (inputEl.is('select')) { - inputEl.val(inputEl.val()).trigger('change'); - } - // For text inputs and others - else { - inputEl.trigger('change'); - } - - // Also trigger input event for text-based inputs - if (inputEl.is('input[type="text"], textarea')) { - inputEl.trigger('input'); - } +Shiny.addCustomMessageHandler('setAnswerData', function(message) { + if (message.pageId && message.pageData) { + surveydownCookies.setAnswerData(message.pageId, message.pageData); } }); -// Initialize on document ready - with retry mechanism +// Initialize on document ready $(document).ready(function() { function initializeSession(retryCount = 0) { const sessionId = surveydownCookies.get(); - console.log("Initializing with session ID:", sessionId, "Retry count:", retryCount); - + const answerData = surveydownCookies.getAnswerData(); + if (sessionId) { Shiny.setInputValue('stored_session_id', sessionId, {priority: 'event'}); - } else if (retryCount < 3) { - // Retry after a short delay + } + if (answerData) { + Shiny.setInputValue('stored_answer_data', answerData, {priority: 'event'}); + } + + if (!sessionId && retryCount < 3) { setTimeout(() => initializeSession(retryCount + 1), 100); } } @@ -93,11 +111,15 @@ $(document).ready(function() { initializeSession(); }); -// Also handle Shiny reconnections +// Handle Shiny reconnections $(document).on('shiny:connected', function(event) { const sessionId = surveydownCookies.get(); - console.log("Shiny reconnected, session ID:", sessionId); + const answerData = surveydownCookies.getAnswerData(); + if (sessionId) { Shiny.setInputValue('stored_session_id', sessionId, {priority: 'event'}); } -}); + if (answerData) { + Shiny.setInputValue('stored_answer_data', answerData, {priority: 'event'}); + } +}); \ No newline at end of file