diff --git a/.Rbuildignore b/.Rbuildignore index ab07760..51433e6 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -6,3 +6,4 @@ ^\.github$ ^.*\.Rproj$ ^\.Rproj\.user$ +*.html \ No newline at end of file diff --git a/.Rprofile b/.Rprofile new file mode 100644 index 0000000..45596ac --- /dev/null +++ b/.Rprofile @@ -0,0 +1 @@ +# renv disabled - using DESCRIPTION for dependencies diff --git a/.github/workflows/deploy-quiz.yml b/.github/workflows/deploy-quiz.yml new file mode 100644 index 0000000..9c39a64 --- /dev/null +++ b/.github/workflows/deploy-quiz.yml @@ -0,0 +1,57 @@ +name: Deploy Quiz to shinyapps.io + +on: + push: + branches: [main, dev] + workflow_dispatch: # Allow manual deployment + +jobs: + deploy: + runs-on: ubuntu-latest + + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup R + uses: r-lib/actions/setup-r@v2 + with: + r-version: '4.4.0' + use-public-rspm: true + + - name: Setup Pandoc + uses: r-lib/actions/setup-pandoc@v2 + + - name: Install dependencies from DESCRIPTION + uses: r-lib/actions/setup-r-dependencies@v2 + with: + dependencies: '"hard"' # Install Imports, Depends, LinkingTo + + - name: Configure rsconnect authentication + run: | + rsconnect::setAccountInfo( + name = '${{ secrets.SHINYAPPS_ACCOUNT }}', + token = '${{ secrets.SHINYAPPS_TOKEN }}', + secret = '${{ secrets.SHINYAPPS_SECRET }}' + ) + shell: Rscript {0} + + - name: Run build script + run: | + cat("🚀 Starting deployment process...\n") + source("build.R") + cat("✅ All deployments completed successfully!\n") + shell: Rscript {0} + + - name: Upload deployment logs + uses: actions/upload-artifact@v4 + if: failure() + with: + name: deployment-logs-${{ github.run_id }} + path: | + *.log + rsconnect/ + modules/rsconnect/ \ No newline at end of file diff --git a/.gitignore b/.gitignore index aca3d20..ce3638b 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,12 @@ _freeze/ docs/ !docs/*.md -*.dcf \ No newline at end of file +*.dcf +*.html + +# renv - using DESCRIPTION for dependencies instead +renv/ +renv.lock + +# Shiny deployment +rsconnect/ \ No newline at end of file diff --git a/DESCRIPTION b/DESCRIPTION index 50c3cfd..3aa495c 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -13,12 +13,19 @@ URL: https://github.com/ds4owd-dev/quiz, BugReports: https://github.com/ds4owd-dev/quiz/issues Imports: learnr, - tidyverse, + dplyr, gapminder, gt, - gradethis + gradethis, + learnrhash, + httr, + knitr, + rmarkdown, + shiny, + bslib, + rsconnect Suggests: - pkgdown, - quarto + pkgdown Remotes: - rstudio/gradethis + rstudio/gradethis, + rundel/learnrhash diff --git a/README.md b/README.md index 1343cdc..1566db8 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,46 @@ The quizzes now include automatic submission to Google Forms for tracking studen ## Deployment Process +### Automated Deployment (CI/CD) + +Quizzes are automatically deployed to shinyapps.io via GitHub Actions when changes are pushed to the `main` or `dev` branches. + +#### Setting Up Automated Deployment + +1. **Get shinyapps.io credentials**: + - Go to [shinyapps.io](https://www.shinyapps.io/) → Account → Tokens + - Click "Add Token" to generate new credentials + - Copy the `name`, `token`, and `secret` values + +2. **Configure GitHub Secrets**: + - Go to your GitHub repository → Settings → Secrets and variables → Actions + - Add these repository secrets: + - `SHINYAPPS_ACCOUNT`: Your shinyapps.io account name + - `SHINYAPPS_TOKEN`: The token from shinyapps.io + - `SHINYAPPS_SECRET`: The secret from shinyapps.io + +3. **Add new quizzes to automated deployment**: + - Edit `build.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 + ) + ``` + +#### How the CI/CD Workflow Works + +The GitHub Action (`.github/workflows/deploy-quiz.yml`): +- **Triggers**: On pushes to `main`/`dev` branches or manual workflow dispatch +- **Environment**: Sets up R 4.3.2 with required packages from DESCRIPTION +- **Authentication**: Uses repository secrets to authenticate with shinyapps.io +- **Deployment**: Executes `build.R` to deploy all applications defined there +- **Logging**: Uploads deployment logs if any failures occur + +### Manual Deployment + This quiz system uses a two-part deployment approach: ### 1. Deploy Individual Quizzes diff --git a/build.R b/build.R new file mode 100644 index 0000000..b5eeea2 --- /dev/null +++ b/build.R @@ -0,0 +1,18 @@ +rsconnect::deployApp( + appName = "openwashdata-quiz-hub", + forceUpdate = TRUE +) + +rsconnect::deployDoc( + doc = "modules/md-01-quiz.Rmd", + appName = "openwashdata-module1-quiz", + forceUpdate = TRUE +) + +# Example of what to add when creating new quiz: + +#rsconnect::deployDoc( +# doc = "modules/md-02-quiz.Rmd", +# appName = "openwashdata-module2-quiz", +# forceUpdate = TRUE +#) diff --git a/modules/_github_username.Rmd b/modules/_github_username.Rmd index 27b9cad..b8370d6 100644 --- a/modules/_github_username.Rmd +++ b/modules/_github_username.Rmd @@ -1,3 +1,112 @@ +```{r github-username-setup, include=FALSE} +# Read GitHub usernames from CSV +tryCatch({ + github_users <- read.csv("github_usernames.csv", stringsAsFactors = FALSE) + # Create choices with display format: "First Last (username)" + username_choices <- setNames( + github_users$GitHub.Username, + paste0(github_users$First.Name, " ", github_users$Last.Name, " (", github_users$GitHub.Username, ")") + ) +}, error = function(e) { + # Fallback if CSV not found + username_choices <- character(0) +}) +``` + ```{r github-username, echo=FALSE} -textInput("github_username", "GitHub Username:", placeholder = "Enter your GitHub username") +div( + selectizeInput( + "github_username", + "GitHub Username:", + choices = NULL, # Start empty for performance + options = list( + placeholder = "Start typing your name or GitHub username...", + maxItems = 1, + searchField = c("value", "text"), + create = TRUE, # Allow creating new entries + persist = TRUE, # Keep options persistent + closeAfterSelect = TRUE, + loadThrottle = 200 # Delay loading to prevent auto-selection + ) + ), + # Warning message for new usernames + div(id = "username-warning", style = "color: orange; font-size: 12px; margin-top: 5px;"), + # Confirmation for new usernames + div(id = "username-confirmation", style = "margin-top: 10px;") +) +``` + +```{r, context="server"} +# Read GitHub usernames in server context +username_choices <- reactive({ + tryCatch({ + github_users <- read.csv("modules/github_usernames.csv", stringsAsFactors = FALSE) + # Create choices with display format: "First Last (username)" + setNames( + github_users$GitHub.Username, + paste0(github_users$First.Name, " ", github_users$Last.Name, " (", github_users$GitHub.Username, ")") + ) + }, error = function(e) { + # Fallback if CSV not found + character(0) + }) +}) + +# Update selectize choices on server side - delay to prevent auto-selection +observeEvent(session$clientData, { + # Small delay to ensure UI is ready before populating choices + invalidateLater(500, session) + isolate({ + updateSelectizeInput( + session = session, + inputId = "github_username", + choices = username_choices(), + selected = character(0), # Ensure nothing is pre-selected + server = FALSE + ) + }) +}, once = TRUE) + +# Validate username and show warning/confirmation +observeEvent(input$github_username, { + if (!is.null(input$github_username) && input$github_username != "") { + # Check if username is in the approved list + is_approved <- input$github_username %in% username_choices() + + if (!is_approved) { + # Show warning for new username + output$`username-warning` <- renderUI({ + div( + style = "color: orange; font-size: 12px; margin-top: 5px;", + HTML("⚠️ Warning: This username is not in the approved list. Please confirm it's correct.") + ) + }) + + # Show confirmation checkbox + output$`username-confirmation` <- renderUI({ + div( + style = "margin-top: 10px;", + checkboxInput( + "confirm_username", + HTML(paste0("I confirm that ", input$github_username, " is my correct GitHub username")), + value = FALSE + ) + ) + }) + } else { + # Clear warnings for approved username + output$`username-warning` <- renderUI({ + div( + style = "color: green; font-size: 12px; margin-top: 5px;", + HTML("✓ Username found in approved list") + ) + }) + output$`username-confirmation` <- renderUI({}) + } + } else { + # Clear all messages when empty + output$`username-warning` <- renderUI({}) + output$`username-confirmation` <- renderUI({}) + } +}, ignoreInit = TRUE) ``` \ No newline at end of file diff --git a/modules/github_usernames.csv b/modules/github_usernames.csv new file mode 100644 index 0000000..ca90856 --- /dev/null +++ b/modules/github_usernames.csv @@ -0,0 +1,9 @@ +First Name,Last Name,Email,Registration Time,Approval Status,GitHub Username +Rainbow,Train,larnsce@gmail.com,08/28/2025 1:36:03 PM,approved,rainbow-train +John,Doe,john.doe@email.com,08/25/2025 9:15:22 AM,approved,johndoe +Jane,Smith,jane.smith@email.com,08/26/2025 2:30:45 PM,approved,janesmith +Alice,Johnson,alice.j@email.com,08/27/2025 11:20:10 AM,approved,alice-johnson +Bob,Wilson,bob.wilson@email.com,08/27/2025 3:45:33 PM,approved,bobwilson +Carol,Brown,carol.brown@email.com,08/28/2025 8:12:15 AM,approved,carol-brown +David,Miller,d.miller@email.com,08/28/2025 10:25:40 AM,approved,davidmiller +Emily,Davis,emily.davis@email.com,08/28/2025 4:18:22 PM,approved,emily-davis \ No newline at end of file diff --git a/modules/md-01-quiz.Rmd b/modules/md-01-quiz.Rmd index 48dbdf2..f31cab6 100644 --- a/modules/md-01-quiz.Rmd +++ b/modules/md-01-quiz.Rmd @@ -9,7 +9,7 @@ tutorial: ```{r setup, include=FALSE} library(learnr) -library(tidyverse) +library(dplyr) library(gapminder) library(knitr) library(gradethis) @@ -88,7 +88,7 @@ question("Which chunk options would you use to hide both code and messages when Use the gapminder dataset to create a summary table showing the average life expectancy by continent in 2007: ```{r create-table-setup} -library(tidyverse) +library(dplyr) library(gapminder) library(knitr) ``` @@ -131,7 +131,7 @@ grade_this_code() Calculate the mean GDP per capita for Switzerland in 2007: ```{r inline-code-setup} -library(tidyverse) +library(dplyr) library(gapminder) ``` @@ -164,7 +164,7 @@ grade_this_code() Create a line plot showing the life expectancy over time for African countries with a population greater than 30 million in 2007: ```{r visualization-setup} -library(tidyverse) +library(dplyr) library(gapminder) # Identify African countries with population > 30 million in 2007