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