diff --git a/.github/workflows/deploy-quiz.yml b/.github/workflows/deploy-quiz.yml index 9c39a64..51f6a08 100644 --- a/.github/workflows/deploy-quiz.yml +++ b/.github/workflows/deploy-quiz.yml @@ -1,9 +1,9 @@ name: Deploy Quiz to shinyapps.io on: - push: - branches: [main, dev] - workflow_dispatch: # Allow manual deployment + pull_request: + types: [opened, synchronize, ready_for_review] + workflow_dispatch: jobs: deploy: diff --git a/DESCRIPTION b/DESCRIPTION index 3aa495c..b92e383 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -23,7 +23,8 @@ Imports: rmarkdown, shiny, bslib, - rsconnect + rsconnect, + yaml Suggests: pkgdown Remotes: diff --git a/README.md b/README.md index 1566db8..488dcae 100644 --- a/README.md +++ b/README.md @@ -6,26 +6,25 @@ This directory contains interactive learnr quizzes for the openwashdata course a ## Structure -- `app.R` - Quiz landing page that links to all deployed quizzes +- `app.R` - Quiz landing page that automatically displays all configured quizzes +- `build.R` - Deployment script with helper functions for automated deployment +- `config.R` - Shared configuration file defining all available quizzes - `modules/` - Directory containing all quiz files - `md-01-quiz.Rmd` - Module 1 quiz on Quarto basics (learnr tutorial) - - `_github_username.Rmd` - Reusable component for GitHub username input + - `_github_username.Rmd` - Reusable component for GitHub username input with CSV data - `_submission.Rmd` - Reusable component for quiz submission + - `github_usernames.csv` - Student GitHub username database - Additional quiz files can be added as `md-XX-quiz.Rmd` ## Required Packages Make sure these packages are installed: +All required packages are defined in `DESCRIPTION` and automatically installed during deployment. For local development: + ```r -install.packages(c("learnr", "tidyverse", "gapminder", "knitr", "rsconnect", "httr", "digest")) - -# Install packages from GitHub -if (!requireNamespace("remotes", quietly = TRUE)) { - install.packages("remotes") -} -remotes::install_github("rstudio/gradethis") -remotes::install_github("rundel/learnrhash") +# Install from DESCRIPTION file +devtools::install_github("ds4owd-dev/quiz") ``` ## ShinyApps.io Authentication @@ -53,7 +52,6 @@ The quizzes now include automatic submission to Google Forms for tracking studen - **GitHub username collection**: Students enter their GitHub username at the start - **Learnrhash generation**: Quiz responses are encoded using learnrhash - **Automatic submission**: Results are submitted to Google Form via POST request -- **Error handling**: Provides fallback hash if submission fails - **Modular components**: Reusable username and submission components ### Google Form Setup @@ -66,15 +64,6 @@ The quizzes now include automatic submission to Google Forms for tracking studen - Hash column: Complete learnrhash for decoding - Username column: GitHub username for filtering - Module column: Module identifier (fetched from tutorial ID metadata) -- **Authentication**: None required - form accepts responses from anyone with the link - -### Advantages of Google Forms -- **No authentication needed**: Forms can accept anonymous submissions -- **Tidy data format**: Each field in its own column for easy analysis -- **Reliable**: Google handles all the backend infrastructure -- **Easy to view**: Responses automatically appear in Google Sheets -- **Error-resistant**: Works even with network issues -- **Easy filtering**: Separate columns for username and module ## Deployment Process @@ -97,13 +86,11 @@ Quizzes are automatically deployed to shinyapps.io via GitHub Actions when chang - `SHINYAPPS_SECRET`: The secret from shinyapps.io 3. **Add new quizzes to automated deployment**: - - Edit `build.R` and add your new quiz: + - Edit `config.R` and add your new quiz: ```r - # Deploy new quiz module - rsconnect::deployDoc( - doc = "modules/md-02-quiz.Rmd", - appName = "openwashdata-module2-quiz", - forceUpdate = TRUE + quiz_names <- c( + "md-01-quiz", + "md-02-quiz" # Add new quiz here ) ``` @@ -118,49 +105,26 @@ The GitHub Action (`.github/workflows/deploy-quiz.yml`): ### Manual Deployment -This quiz system uses a two-part deployment approach: - -### 1. Deploy Individual Quizzes - -Each quiz is deployed as a separate learnr tutorial using `deployDoc()`: - -```r -# Deploy Module 1 quiz -rsconnect::deployDoc( - doc = "modules/md-01-quiz.Rmd", - appName = "openwashdata-module1-quiz", - forceUpdate = TRUE -) -``` +For manual deployment, simply run the build script which handles everything automatically: -For additional quizzes: ```r -# Deploy Module 2 quiz (when created) -rsconnect::deployDoc( - doc = "modules/md-02-quiz.Rmd", - appName = "openwashdata-module2-quiz", - forceUpdate = TRUE -) +# Deploy all quizzes and landing page +source("build.R") ``` -### 2. Deploy the Landing Page - -The landing page is deployed as a regular Shiny app using `deployApp()`: - -```r -rsconnect::deployApp( - appName = "openwashdata-quiz-hub", - forceUpdate = TRUE -) -``` +The deployment system features: +- **Automatic file bundling**: CSV files and dependencies are automatically included +- **Streamlined process**: One script deploys everything configured in `config.R` ## Adding New Quizzes -To add a new quiz module: +The system now uses automatic configuration - adding a new quiz is simple: -1. **Create the quiz file** (e.g., `modules/md-02-quiz.Rmd`) using this template: +### 1. Create the Quiz File -```r +Create `modules/md-02-quiz.Rmd` with the standardized YAML header: + +```yaml --- title: "Module 2: Your Title" output: learnr::tutorial @@ -169,68 +133,34 @@ description: "Your quiz description" tutorial: id: "module2-your-id" --- +``` -`​``{r setup, include=FALSE} -library(learnr) -library(tidyverse) -library(gradethis) -library(learnrhash) -library(httr) - -tutorial_options( - exercise.eval = FALSE, - exercise.checker = gradethis::grade_learnr -) - -knitr::opts_chunk$set(echo = FALSE) - -# Google Form setup -form_url <- "https://docs.google.com/forms/d/e/1FAIpQLScnw9R8wMU5SfFqNVXGeEkiIygLTB_Dc6jWBmbwEeHuekBDzg/formResponse" -`​`` - -## Introduction - -Your introduction text here. +Add your quiz content following the existing pattern, including: +- GitHub username collection: `{r github-username, child='_github_username.Rmd'}` +- Quiz submission: `{r submission-section, child='_submission.Rmd'}` -`​``{r github-username, child='_github_username.Rmd'} -`​`` +### 2. Update Configuration -## Your Quiz Content +Edit `config.R` to include your new quiz: -Add your questions and exercises here... +```r +quiz_names <- c( + "md-01-quiz", + "md-02-quiz" # Add new quiz here +) +``` -`​``{r submission-section, child='_submission.Rmd'} -`​`` +### 3. Deploy -## Summary +Run the build script to deploy everything: -Your summary here. +```r +source("build.R") ``` -2. **Deploy the quiz**: - ```r - rsconnect::deployDoc( - doc = "md-02-quiz.Rmd", - appName = "openwashdata-module2-quiz", - forceUpdate = TRUE - ) - ``` -3. **Update the landing page** by adding the new quiz to the `quizzes` list in `app.R`: - ```r - list( - id = "module2", - title = "Module 2: Your Title", - description = "Quiz description", - url = "https://your-account.shinyapps.io/openwashdata-module2-quiz/", - available = TRUE - ) - ``` -4. **Redeploy the landing page**: - ```r - rsconnect::deployApp( - appName = "openwashdata-quiz-hub", - forceUpdate = TRUE - ) - ``` + +This will: +- Automatically deploy the new quiz +- Update the landing page to show the new quiz ## Local Testing diff --git a/app.R b/app.R index 8bece61..5557542 100644 --- a/app.R +++ b/app.R @@ -2,25 +2,61 @@ library(shiny) library(bslib) +library(yaml) -# Define available quizzes with their deployed URLs -quizzes <- list( - list( - id = "module1", - title = "Module 1: Quarto Basics", - description = "Test your understanding of Quarto basics for openwashdata package documentation", - url = "https://hjj91u-nicolo-massari.shinyapps.io/openwashdata-module1-quiz/", - available = TRUE - ) - # Add more quizzes here as you deploy them - # list( - # id = "module2", - # title = "Module 2: Data Visualization", - # description = "Learn about creating effective visualizations", - # url = "https://hjj91u-nicolo-massari.shinyapps.io/openwashdata-module2-quiz/", - # available = FALSE - # ) -) +# Load configuration +source("config.R") + +# Function to extract quiz metadata from Rmd files +extract_quiz_metadata <- function(quiz_name) { + rmd_path <- file.path("modules", paste0(quiz_name, ".Rmd")) + + if (!file.exists(rmd_path)) { + return(NULL) + } + + # Read the Rmd file and extract YAML header + rmd_content <- readLines(rmd_path) + yaml_start <- which(rmd_content == "---")[1] + yaml_end <- which(rmd_content == "---")[2] + + if (is.na(yaml_start) || is.na(yaml_end) || yaml_start >= yaml_end) { + return(NULL) + } + + yaml_content <- paste(rmd_content[(yaml_start + 1):(yaml_end - 1)], collapse = "\n") + yaml_data <- yaml::yaml.load(yaml_content) + + # Extract title and description + title <- yaml_data$title %||% paste("Quiz:", quiz_name) + description <- yaml_data$description %||% "Interactive quiz module" + + return(list(title = title, description = description)) +} + +# Auto-generate quiz list from quiz names +generate_quiz_list <- function(quiz_names) { + # Generate quiz list with metadata + quiz_list <- lapply(quiz_names, function(quiz_name) { + metadata <- extract_quiz_metadata(quiz_name) + + # Generate URL based on quiz name + quiz_url <- paste0(base_url, quiz_name, "/") + + list( + id = quiz_name, + title = metadata$title %||% paste("Quiz:", quiz_name), + description = metadata$description %||% "Interactive quiz module", + url = quiz_url, + available = TRUE + ) + }) + + return(quiz_list) +} + +# Generate quiz list automatically from config +quizzes <- generate_quiz_list(quiz_names) # UI ui <- page_navbar( diff --git a/build.R b/build.R index b5eeea2..97df4b8 100644 --- a/build.R +++ b/build.R @@ -1,18 +1,28 @@ -rsconnect::deployApp( - appName = "openwashdata-quiz-hub", - forceUpdate = TRUE -) +# Load configuration +source("config.R") + +# HELPER -rsconnect::deployDoc( - doc = "modules/md-01-quiz.Rmd", - appName = "openwashdata-module1-quiz", +deploy_quiz <- function(module_name) { + module_path <- paste0(file.path("modules", module_name), ".Rmd") + rsconnect::deployDoc( + doc = module_path, + appName = module_name, + forceUpdate = TRUE, + logLevel = "verbose" + ) +} + + +# QUIZ DEPLOYMENT + +rsconnect::deployApp( + appName = main_app_name, forceUpdate = TRUE ) -# Example of what to add when creating new quiz: +for (quiz in quiz_names) { + deploy_quiz(quiz) +} -#rsconnect::deployDoc( -# doc = "modules/md-02-quiz.Rmd", -# appName = "openwashdata-module2-quiz", -# forceUpdate = TRUE -#) +# To add new quizzes, edit config.R diff --git a/config.R b/config.R new file mode 100644 index 0000000..44b9ee4 --- /dev/null +++ b/config.R @@ -0,0 +1,16 @@ +# Quiz Configuration +# Shared configuration file for build.R and app.R + +# List of quiz modules to deploy and display +quiz_names <- c( + "md-01-quiz" + # Add new quizzes here: + # "md-02-quiz", + # "md-03-quiz" +) + +# Base URL for deployed quizzes +base_url <- "https://hjj91u-nicolo-massari.shinyapps.io/" + +# Main app configuration +main_app_name <- "openwashdata-quiz-hub" \ No newline at end of file diff --git a/modules/_github_username.Rmd b/modules/_github_username.Rmd index e44e018..ace5310 100644 --- a/modules/_github_username.Rmd +++ b/modules/_github_username.Rmd @@ -1,7 +1,11 @@ ```{r github-username-setup, include=FALSE} # Read GitHub usernames from CSV tryCatch({ - csv_path <- system.file("extdata", "github_usernames.csv", package = "quiz") + # Try deployed path first, then fallback to local path + csv_path <- "github_usernames.csv" + if (!file.exists(csv_path)) { + csv_path <- "modules/github_usernames.csv" + } github_users <- read.csv(csv_path, stringsAsFactors = FALSE) # Create choices with display format: "First Last (username)" username_choices <- setNames( @@ -41,7 +45,11 @@ div( # Read GitHub usernames in server context username_choices <- reactive({ tryCatch({ - csv_path <- system.file("extdata", "github_usernames.csv", package = "quiz") + # Try deployed path first, then fallback to local path + csv_path <- "github_usernames.csv" + if (!file.exists(csv_path)) { + csv_path <- "modules/github_usernames.csv" + } github_users <- read.csv(csv_path, stringsAsFactors = FALSE) # Create choices with display format: "First Last (username)" setNames( diff --git a/inst/extdata/github_usernames.csv b/modules/github_usernames.csv similarity index 100% rename from inst/extdata/github_usernames.csv rename to modules/github_usernames.csv diff --git a/modules/md-01-quiz.Rmd b/modules/md-01-quiz.Rmd index f31cab6..461b3ae 100644 --- a/modules/md-01-quiz.Rmd +++ b/modules/md-01-quiz.Rmd @@ -1,8 +1,12 @@ --- -title: "Module 1 Quiz: Quarto Basics for openwashdata" +title: "Module 1: Quarto Basics" output: learnr::tutorial runtime: shiny_prerendered description: "Test your understanding of Quarto basics for openwashdata package documentation" +resource_files: + - github_usernames.csv + - _github_username.Rmd + - _submission.Rmd tutorial: id: "module1-quarto-basics" --- @@ -43,10 +47,10 @@ This quiz tests your understanding of Quarto basics for openwashdata package doc ```{r yaml-question} question("Which of the following is the correct YAML header for an openwashdata package README?", - answer("---\noutput: html_document\n---"), - answer("---\noutput: github_document\n---", correct = TRUE), - answer("---\nformat: html\n---"), - answer("---\nformat: markdown\n---"), + answer("`---`\n`output: html_document`\n`---`"), + answer("`---`\n`output: github_document`\n`---`", correct = TRUE), + answer("`---`\n`format: html`\n`---`"), + answer("`---`\n`format: markdown`\n`---`"), allow_retry = TRUE, incorrect = "Think about what output format is typically used for GitHub README files.", correct = "Correct! The github_document output format is used for README files that will be displayed on GitHub."