From d8742edb0506f436e035a9c61a2c89294dd47ffa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Nicosia?= <116816597+AurelienNicosiaULaval@users.noreply.github.com> Date: Fri, 5 Jun 2026 12:49:26 -0400 Subject: [PATCH] Add course pilot example --- DESCRIPTION | 2 +- NEWS.md | 6 + README.md | 32 ++ _pkgdown.yml | 1 + inst/examples/course-pilot/README.md | 106 +++++++ .../course-pilot/config-csv/courses.csv | 2 + .../course-pilot/config-csv/questions.csv | 7 + .../course-pilot/config-csv/students.csv | 5 + .../course-pilot/config-csv/tutorials.csv | 2 + inst/examples/course-pilot/inspect-results.R | 114 +++++++ inst/examples/course-pilot/pilot-data.R | 16 + inst/examples/course-pilot/simulate-results.R | 129 ++++++++ .../course-pilot/tracking-strategy.md | 59 ++++ inst/examples/course-pilot/tutorial.Rmd | 299 ++++++++++++++++++ tests/testthat/test-course-pilot.R | 82 +++++ vignettes/course-pilot.Rmd | 104 ++++++ 16 files changed, 965 insertions(+), 1 deletion(-) create mode 100644 inst/examples/course-pilot/README.md create mode 100644 inst/examples/course-pilot/config-csv/courses.csv create mode 100644 inst/examples/course-pilot/config-csv/questions.csv create mode 100644 inst/examples/course-pilot/config-csv/students.csv create mode 100644 inst/examples/course-pilot/config-csv/tutorials.csv create mode 100644 inst/examples/course-pilot/inspect-results.R create mode 100644 inst/examples/course-pilot/pilot-data.R create mode 100644 inst/examples/course-pilot/simulate-results.R create mode 100644 inst/examples/course-pilot/tracking-strategy.md create mode 100644 inst/examples/course-pilot/tutorial.Rmd create mode 100644 tests/testthat/test-course-pilot.R create mode 100644 vignettes/course-pilot.Rmd diff --git a/DESCRIPTION b/DESCRIPTION index b82e6ca..b8da121 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: learnrTrackR Title: Track Simulated learnr Tutorial Attempts -Version: 0.1.1 +Version: 0.1.1.9000 Authors@R: person("Aurélien", "Nicosia", , "116816597+AurelienNicosiaULaval@users.noreply.github.com", role = c("aut", "cre")) diff --git a/NEWS.md b/NEWS.md index c853714..c46196a 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,9 @@ +# learnrTrackR 0.1.1.9000 + +- Added a course pilot example showing a realistic tracked `learnr` tutorial, + CSV configuration, simulated learner results, Moodle export, and teacher + report generation. + # learnrTrackR 0.1.1 - Added `setup_learnr_tracking()` and `open_learnr_tracking_db()` to simplify diff --git a/README.md b/README.md index eb58647..394aea6 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,8 @@ store simulated attempts and export results for inspection. - Provide a minimal example and unit tests. - Provide a minimal `learnr` and `gradethis` prototype for tracked questions and code exercises. +- Provide a course pilot example with CSV configuration, simulated learner + results, Moodle export, and teacher report generation. ## What the MVP does not do yet @@ -224,6 +226,36 @@ load_tracking_config(con, "tracking.yml") The minimal `learnr` example includes both a YAML configuration file and a CSV configuration directory. +## Course pilot example + +A course-like pilot is available in `inst/examples/course-pilot/`. It contains +a simulated descriptive statistics tutorial with tracked `learnr` questions, +tracked `gradethis` code exercises, CSV configuration files, a cohort +simulation script, Moodle export, and teacher report generation. + +To create simulated pilot results: + +```r +source("inst/examples/course-pilot/simulate-results.R") +source("inst/examples/course-pilot/inspect-results.R") +``` + +The tutorial can also be launched manually: + +```r +Sys.setenv( + LEARNRTRACKR_DB = file.path(tempdir(), "learnrtrackr-course-pilot.sqlite"), + LEARNRTRACKR_STUDENT_ID = "student_demo", + LEARNRTRACKR_GROUP_ID = "A" +) + +learnr::run_tutorial( + "inst/examples/course-pilot/tutorial.Rmd", + clean = TRUE, + as_rstudio_job = FALSE +) +``` + ## PostgreSQL prototype SQLite remains the default backend for local prototypes. For a server-backed diff --git a/_pkgdown.yml b/_pkgdown.yml index a36ecc4..d883c03 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -12,6 +12,7 @@ articles: contents: - getting-started - learnr-context + - course-pilot - teacher-workflow - moodle-export - deployment-postgresql diff --git a/inst/examples/course-pilot/README.md b/inst/examples/course-pilot/README.md new file mode 100644 index 0000000..e3808df --- /dev/null +++ b/inst/examples/course-pilot/README.md @@ -0,0 +1,106 @@ +# Course pilot example + +This example is a course-like pilot for `learnrTrackR`. It uses a simulated +descriptive statistics tutorial and shows the full teacher workflow: + +- launch a tracked `learnr` tutorial; +- load course, tutorial, student, and question metadata from CSV files; +- record tracked `learnr` questions and `gradethis` code checks; +- simulate a small cohort without manual browser interaction; +- inspect attempts, scores, gradebook rows, Moodle-ready grades, export + bundles, and a teacher report. + +The data are simulated and do not represent real students. + +## Files + +```text +course-pilot/ + tutorial.Rmd + pilot-data.R + simulate-results.R + inspect-results.R + tracking-strategy.md + config-csv/ + courses.csv + tutorials.csv + students.csv + questions.csv +``` + +## Run the tutorial + +From the package source directory, install the package locally first: + +```r +devtools::install(dependencies = FALSE) +``` + +Then launch the tutorial: + +```r +Sys.setenv( + LEARNRTRACKR_DB = file.path(tempdir(), "learnrtrackr-course-pilot.sqlite"), + LEARNRTRACKR_STUDENT_ID = "student_demo", + LEARNRTRACKR_GROUP_ID = "A" +) + +learnr::run_tutorial( + "inst/examples/course-pilot/tutorial.Rmd", + clean = TRUE, + as_rstudio_job = FALSE +) +``` + +For the intended pilot workflow, use one of the identifiers listed in +`config-csv/students.csv` so the group filters and teacher exports match the +configured cohort. + +## Simulate a cohort + +To create a reproducible database without manually using the browser: + +```r +source("inst/examples/course-pilot/simulate-results.R") +``` + +This creates a SQLite database at `LEARNRTRACKR_DB`, or at a temporary path when +the environment variable is not set. + +## Inspect teacher outputs + +After running the tutorial or the simulation: + +```r +source("inst/examples/course-pilot/inspect-results.R") +``` + +The script writes: + +- attempts CSV; +- scores CSV; +- gradebook CSV; +- Moodle-ready CSV; +- rich export bundle; +- HTML teacher report, when `rmarkdown` is installed. + +The default output directory is: + +```r +file.path(dirname(Sys.getenv("LEARNRTRACKR_DB")), "course-pilot-outputs") +``` + +You can override it with: + +```r +Sys.setenv(LEARNRTRACKR_OUTPUT_DIR = "course-pilot-outputs") +``` + +## Open the dashboard + +```r +learnrTrackR::run_dashboard(Sys.getenv("LEARNRTRACKR_DB"), group_id = "A") +``` + +The dashboard remains a local inspection tool. It is not an institutional +authentication layer. diff --git a/inst/examples/course-pilot/config-csv/courses.csv b/inst/examples/course-pilot/config-csv/courses.csv new file mode 100644 index 0000000..55099a5 --- /dev/null +++ b/inst/examples/course-pilot/config-csv/courses.csv @@ -0,0 +1,2 @@ +course_id,course_label,semester +stat_intro,Introduction to statistics with R,pilot diff --git a/inst/examples/course-pilot/config-csv/questions.csv b/inst/examples/course-pilot/config-csv/questions.csv new file mode 100644 index 0000000..6da6572 --- /dev/null +++ b/inst/examples/course-pilot/config-csv/questions.csv @@ -0,0 +1,7 @@ +tutorial_id,question_id,question_label,question_type,max_score +stat_descriptive_pilot,q1_variable_type,Identify a quantitative variable,radio,1 +stat_descriptive_pilot,q2_robust_summaries,Choose robust summary statistics,checkbox,1 +stat_descriptive_pilot,q3_n_rows,Count data rows,numeric,1 +stat_descriptive_pilot,q4_highest_median,Identify the highest median region,text,1 +stat_descriptive_pilot,q5_mean_study_hours,Compute mean study hours,code,1 +stat_descriptive_pilot,q6_region_summary,Summarise study hours by region,code,2 diff --git a/inst/examples/course-pilot/config-csv/students.csv b/inst/examples/course-pilot/config-csv/students.csv new file mode 100644 index 0000000..de5e22c --- /dev/null +++ b/inst/examples/course-pilot/config-csv/students.csv @@ -0,0 +1,5 @@ +student_id,student_label,email,group_id +student_demo,Demo learner,,A +student_a01,Student A01,,A +student_a02,Student A02,,A +student_b01,Student B01,,B diff --git a/inst/examples/course-pilot/config-csv/tutorials.csv b/inst/examples/course-pilot/config-csv/tutorials.csv new file mode 100644 index 0000000..c1301f3 --- /dev/null +++ b/inst/examples/course-pilot/config-csv/tutorials.csv @@ -0,0 +1,2 @@ +tutorial_id,course_id,tutorial_label,version +stat_descriptive_pilot,stat_intro,Descriptive statistics pilot tutorial,0.1.1.9000 diff --git a/inst/examples/course-pilot/inspect-results.R b/inst/examples/course-pilot/inspect-results.R new file mode 100644 index 0000000..8faae5c --- /dev/null +++ b/inst/examples/course-pilot/inspect-results.R @@ -0,0 +1,114 @@ +run_course_pilot_inspection <- function() { + tutorial_id <- "stat_descriptive_pilot" + db_path <- Sys.getenv( + "LEARNRTRACKR_DB", + unset = file.path(tempdir(), "learnrtrackr-course-pilot.sqlite") + ) + group_id <- Sys.getenv("LEARNRTRACKR_GROUP_ID", unset = "") + group_filter <- if (nzchar(group_id)) group_id else NULL + output_dir <- Sys.getenv( + "LEARNRTRACKR_OUTPUT_DIR", + unset = file.path(dirname(db_path), "course-pilot-outputs") + ) + + if (!file.exists(db_path)) { + stop( + "No tracking database was found at: ", + db_path, + "\nRun simulate-results.R or launch the tutorial before inspecting results.", + call. = FALSE + ) + } + + if (!dir.exists(output_dir)) { + dir.create(output_dir, recursive = TRUE) + } + + con <- learnrTrackR::connect_tracking_db(db_path) + on.exit(DBI::dbDisconnect(con), add = TRUE) + + attempts <- learnrTrackR::get_attempts(con, tutorial_id = tutorial_id) + scores <- learnrTrackR::compute_scores(con, tutorial_id = tutorial_id, rule = "last") + grades <- learnrTrackR::gradebook(con, tutorial_id = tutorial_id, rule = "last") + dashboard <- learnrTrackR::dashboard_data( + con, + tutorial_id = tutorial_id, + group_id = group_filter, + rule = "last" + ) + report <- learnrTrackR::teacher_report_data( + con, + tutorial_id = tutorial_id, + group_id = group_filter, + rule = "last" + ) + + attempts_path <- file.path(output_dir, "course-pilot-attempts.csv") + scores_path <- file.path(output_dir, "course-pilot-scores.csv") + gradebook_path <- file.path(output_dir, "course-pilot-gradebook.csv") + moodle_path <- file.path(output_dir, "course-pilot-moodle.csv") + bundle_dir <- file.path(output_dir, "course-pilot-bundle") + report_path <- file.path(output_dir, "course-pilot-teacher-report.html") + + learnrTrackR::export_results(con, attempts_path, type = "attempts", tutorial_id = tutorial_id) + learnrTrackR::export_results(con, scores_path, type = "scores", tutorial_id = tutorial_id) + learnrTrackR::export_results(con, gradebook_path, type = "gradebook", tutorial_id = tutorial_id) + learnrTrackR::export_moodle_grades( + con, + moodle_path, + tutorial_id = tutorial_id, + grade_item = "Descriptive statistics pilot" + ) + bundle_paths <- learnrTrackR::export_tracking_bundle( + con, + bundle_dir, + tutorial_id = tutorial_id, + group_id = group_filter + ) + + if (requireNamespace("rmarkdown", quietly = TRUE)) { + learnrTrackR::generate_teacher_report( + con, + report_path, + tutorial_id = tutorial_id, + group_id = group_filter, + rule = "last" + ) + } else { + report_path <- NA_character_ + } + + print(attempts) + print(scores) + print(grades) + print(dashboard$summary) + print(report$summary) + + message("Wrote attempts to: ", attempts_path) + message("Wrote scores to: ", scores_path) + message("Wrote gradebook to: ", gradebook_path) + message("Wrote Moodle-ready grades to: ", moodle_path) + message("Wrote rich export bundle to: ", bundle_dir) + message("Wrote teacher report to: ", report_path) + print(bundle_paths) + + invisible( + list( + attempts = attempts, + scores = scores, + gradebook = grades, + dashboard = dashboard, + report = report, + paths = list( + attempts = attempts_path, + scores = scores_path, + gradebook = gradebook_path, + moodle = moodle_path, + bundle = bundle_dir, + report = report_path + ) + ) + ) +} + +run_course_pilot_inspection() diff --git a/inst/examples/course-pilot/pilot-data.R b/inst/examples/course-pilot/pilot-data.R new file mode 100644 index 0000000..f09cd8f --- /dev/null +++ b/inst/examples/course-pilot/pilot-data.R @@ -0,0 +1,16 @@ +pilot_survey <- tibble::tibble( + learner_case = paste0("case_", sprintf("%02d", 1:12)), + region = c( + "Quebec City", "Quebec City", "Quebec City", "Quebec City", + "Montreal", "Montreal", "Montreal", "Montreal", + "Sherbrooke", "Sherbrooke", "Sherbrooke", "Sherbrooke" + ), + program = c( + "Statistics", "Data science", "Statistics", "Mathematics", + "Data science", "Statistics", "Mathematics", "Data science", + "Statistics", "Mathematics", "Data science", "Statistics" + ), + study_hours = c(6.5, 7.0, 5.5, 8.0, 4.0, 5.0, 6.0, 5.5, 7.5, 8.5, 6.5, 7.0), + commute_minutes = c(20, 15, 25, 30, 45, 35, 50, 40, 25, 20, 30, 35), + quiz_score = c(78, 82, 74, 88, 70, 73, 76, 79, 85, 90, 81, 84) +) diff --git a/inst/examples/course-pilot/simulate-results.R b/inst/examples/course-pilot/simulate-results.R new file mode 100644 index 0000000..a9f70e5 --- /dev/null +++ b/inst/examples/course-pilot/simulate-results.R @@ -0,0 +1,129 @@ +course_pilot_script_file <- tryCatch( + { + script_file <- sys.frame(1)$ofile + if (is.null(script_file)) { + "" + } else { + normalizePath(script_file, mustWork = FALSE) + } + }, + error = function(cnd) "" +) + +resolve_course_pilot_dir <- function(script_file = course_pilot_script_file) { + env_dir <- Sys.getenv("LEARNRTRACKR_EXAMPLE_DIR", unset = "") + if (nzchar(env_dir)) { + return(env_dir) + } + + installed_dir <- system.file("examples/course-pilot", package = "learnrTrackR") + if (nzchar(installed_dir)) { + return(installed_dir) + } + + if (nzchar(script_file)) { + return(dirname(script_file)) + } + + "." +} + +run_course_pilot_simulation <- function() { + example_dir <- resolve_course_pilot_dir() + source(file.path(example_dir, "pilot-data.R")) + + tutorial_id <- "stat_descriptive_pilot" + db_path <- Sys.getenv( + "LEARNRTRACKR_DB", + unset = file.path(tempdir(), "learnrtrackr-course-pilot.sqlite") + ) + config_dir <- file.path(example_dir, "config-csv") + + con <- learnrTrackR::init_tracking_db(db_path, overwrite = TRUE) + on.exit(DBI::dbDisconnect(con), add = TRUE) + + learnrTrackR::load_tracking_config(con, config_dir) + + record_attempt <- function(student_id, + question_id, + submitted_answer, + grade_status, + score, + max_score, + feedback, + minute_offset) { + learnrTrackR::track_attempt( + con = con, + student_id = student_id, + tutorial_id = tutorial_id, + question_id = question_id, + submitted_answer = submitted_answer, + grade_status = grade_status, + score = score, + max_score = max_score, + feedback = feedback, + timestamp = as.POSIXct( + "2026-01-15 09:00:00", + tz = "America/Toronto" + ) + 60 * minute_offset, + require_registered_student = TRUE + ) + } + + record_attempt("student_demo", "q1_variable_type", "study_hours", "correct", 1, 1, "Correct.", 1) + record_attempt("student_demo", "q2_robust_summaries", "Median; Interquartile range", "correct", 1, 1, "Correct.", 2) + record_attempt("student_demo", "q3_n_rows", "12", "correct", 1, 1, "Correct.", 3) + record_attempt("student_demo", "q4_highest_median", "Sherbrooke", "correct", 1, 1, "Correct.", 4) + record_attempt("student_demo", "q5_mean_study_hours", "mean(pilot_survey$study_hours)", "correct", 1, 1, "Correct.", 5) + record_attempt( + "student_demo", + "q6_region_summary", + "pilot_survey |> group_by(region) |> summarise(n = n(), median_study_hours = median(study_hours), mean_quiz_score = mean(quiz_score), .groups = \"drop\")", + "correct", + 2, + 2, + "Correct.", + 6 + ) + + record_attempt("student_a01", "q1_variable_type", "study_hours", "correct", 1, 1, "Correct.", 7) + record_attempt("student_a01", "q2_robust_summaries", "Mean; Standard deviation", "incorrect", 0, 1, "Try again.", 8) + record_attempt("student_a01", "q3_n_rows", "12", "correct", 1, 1, "Correct.", 9) + record_attempt("student_a01", "q4_highest_median", "Quebec City", "incorrect", 0, 1, "Try again.", 10) + record_attempt("student_a01", "q4_highest_median", "Sherbrooke", "correct", 1, 1, "Correct.", 11) + record_attempt("student_a01", "q5_mean_study_hours", "median(pilot_survey$study_hours)", "incorrect", 0, 1, "Try again.", 12) + record_attempt("student_a01", "q5_mean_study_hours", "mean(pilot_survey$study_hours)", "correct", 1, 1, "Correct.", 13) + record_attempt( + "student_a01", + "q6_region_summary", + "pilot_survey |> group_by(region) |> summarise(n = n(), median_study_hours = median(study_hours), .groups = \"drop\")", + "partial", + 1, + 2, + "Partial credit.", + 14 + ) + + record_attempt("student_b01", "q1_variable_type", "study_hours", "correct", 1, 1, "Correct.", 15) + record_attempt("student_b01", "q3_n_rows", "10", "incorrect", 0, 1, "Try again.", 16) + record_attempt("student_b01", "q5_mean_study_hours", "sum(pilot_survey$study_hours)", "incorrect", 0, 1, "Try again.", 17) + record_attempt("student_a02", "q1_variable_type", "region", "incorrect", 0, 1, "Try again.", 18) + + attempts <- learnrTrackR::get_attempts(con, tutorial_id = tutorial_id) + grades <- learnrTrackR::gradebook(con, tutorial_id = tutorial_id, rule = "last") + + message("Created pilot database: ", db_path) + message("Attempts recorded: ", nrow(attempts)) + message("Gradebook:") + print(grades) + + invisible( + list( + db_path = db_path, + attempts = attempts, + gradebook = grades + ) + ) +} + +run_course_pilot_simulation() diff --git a/inst/examples/course-pilot/tracking-strategy.md b/inst/examples/course-pilot/tracking-strategy.md new file mode 100644 index 0000000..03a6187 --- /dev/null +++ b/inst/examples/course-pilot/tracking-strategy.md @@ -0,0 +1,59 @@ +# Tracking strategy for the course pilot + +The pilot uses explicit tracking. It does not intercept internal `learnr` +browser events, and it does not modify `learnr` or `gradethis`. + +## Launch context + +The setup chunk reads: + +- `LEARNRTRACKR_STUDENT_ID`; +- `LEARNRTRACKR_GROUP_ID`; +- `LEARNRTRACKR_DB`. + +It then creates a reusable context with: + +```r +learnrTrackR::setup_learnr_tracking() +``` + +The context stores the tutorial identifier, learner identifier, group, database +path, and configuration path. + +## Built-in learnr questions + +Radio, checkbox, text, and numeric questions use: + +```r +learnrTrackR::tracked_question() +``` + +Each question receives a stable `question_id` that matches +`config-csv/questions.csv`. + +## Code exercises + +Code exercise check chunks use: + +```r +learnrTrackR::track_gradethis_attempt() +``` + +The helper is called inside `gradethis::grade_this()` and records the submitted +code, grading status, score, maximum score, and feedback. + +## Teacher workflow + +The teacher workflow uses the same database to produce: + +- attempts; +- scores; +- gradebook rows; +- Moodle-ready grades; +- export bundles; +- dashboard data; +- a teacher report. + +The database created by the example is suitable for local testing and small +pilots only. A real course pilot should use a controlled deployment plan, +documented retention rules, and an approved student identification strategy. diff --git a/inst/examples/course-pilot/tutorial.Rmd b/inst/examples/course-pilot/tutorial.Rmd new file mode 100644 index 0000000..e6d8f74 --- /dev/null +++ b/inst/examples/course-pilot/tutorial.Rmd @@ -0,0 +1,299 @@ +--- +title: "Course pilot: descriptive statistics with R" +output: + learnr::tutorial: + progressive: true + allow_skip: true +runtime: shiny_prerendered +description: "A course-like learnr pilot using explicit learnrTrackR tracking, CSV configuration, and gradethis checks." +--- + +```{r setup, include=FALSE} +library(learnr) +library(gradethis) +library(learnrTrackR) +library(dplyr) +library(tibble) + +gradethis_setup() +knitr::opts_chunk$set(echo = FALSE) + +resolve_example_dir <- function() { + env_dir <- Sys.getenv("LEARNRTRACKR_EXAMPLE_DIR", unset = "") + if (nzchar(env_dir)) { + return(env_dir) + } + + installed_dir <- system.file("examples/course-pilot", package = "learnrTrackR") + if (nzchar(installed_dir)) { + return(installed_dir) + } + + input <- knitr::current_input() + if (!is.null(input) && nzchar(input)) { + return(dirname(input)) + } + + "." +} + +example_dir <- resolve_example_dir() +source(file.path(example_dir, "pilot-data.R")) + +tutorial_id <- "stat_descriptive_pilot" +tracking_env <- learnrTrackR::get_learnr_tracking_env( + default_db_path = file.path(tempdir(), "learnrtrackr-course-pilot.sqlite"), + default_group_id = "A" +) + +tracking <- learnrTrackR::setup_learnr_tracking( + tutorial_id = tutorial_id, + student_id = tracking_env$student_id, + db_path = tracking_env$db_path, + group_id = tracking_env$group_id, + config_path = file.path(example_dir, "config-csv") +) + +open_tracking_db <- function() { + learnrTrackR::open_learnr_tracking_db(tracking) +} +``` + +## Identification + +The tutorial reads the learner identifier, group, and database path from the +launch environment. For local testing, set `LEARNRTRACKR_STUDENT_ID`, +`LEARNRTRACKR_GROUP_ID`, and `LEARNRTRACKR_DB` before launching the tutorial. + +```{r show-launch} +tibble::tibble( + student_id = tracking$student_id, + group_id = tracking$group_id, + tutorial_id = tracking$tutorial_id, + db_path = tracking$db_path +) +``` + +## Data + +The data set is simulated. Each row represents one fictitious learner case from +a small course pilot. + +```{r show-data} +pilot_survey +``` + +## Question 1 + +```{r q1_variable_type, echo=FALSE} +learnrTrackR::tracked_question( + "Which variable is quantitative?", + learnr::answer("learner_case"), + learnr::answer("region"), + learnr::answer("study_hours", correct = TRUE, message = "Correct."), + learnr::answer("program"), + type = "radio", + allow_retry = TRUE, + try_again = "Try again. Look for a variable measured on a numeric scale.", + question_id = "q1_variable_type", + context = tracking, + max_score = 1 +) +``` + +## Question 2 + +```{r q2_robust_summaries, echo=FALSE} +learnrTrackR::tracked_question( + "Which summaries are usually robust to extreme values?", + learnr::answer("Mean"), + learnr::answer("Median", correct = TRUE), + learnr::answer("Standard deviation"), + learnr::answer("Interquartile range", correct = TRUE), + type = "checkbox", + allow_retry = TRUE, + try_again = "Try again. Robust summaries should not change too much when one value is extreme.", + question_id = "q2_robust_summaries", + context = tracking, + max_score = 1 +) +``` + +## Question 3 + +```{r q3_n_rows, echo=FALSE} +learnrTrackR::tracked_question( + "How many rows are in the pilot data set?", + learnr::answer(12, correct = TRUE, message = "Correct."), + type = "numeric", + allow_retry = TRUE, + min = 0, + max = 30, + step = 1, + question_id = "q3_n_rows", + context = tracking, + max_score = 1 +) +``` + +## Question 4 + +```{r q4_highest_median, echo=FALSE} +learnrTrackR::tracked_question( + "Which region has the highest median study_hours value?", + learnr::answer_fn( + function(value) { + answer <- tolower(trimws(value)) + if (answer == "sherbrooke") { + learnr::correct("Correct. Sherbrooke has the highest median study_hours value.") + } else { + learnr::incorrect("Try again. Compute the median study_hours value by region.") + } + }, + label = "highest median checker" + ), + type = "text", + allow_retry = TRUE, + question_id = "q4_highest_median", + context = tracking, + max_score = 1 +) +``` + +## Code exercise 1 + +Compute the mean of `study_hours`. + +```{r q5_mean_study_hours, exercise=TRUE} +mean(pilot_survey$study_hours) +``` + +```{r q5_mean_study_hours-solution} +mean(pilot_survey$study_hours) +``` + +```{r q5_mean_study_hours-check} +gradethis::grade_this({ + expected <- mean(pilot_survey$study_hours) + correct <- is.numeric(.result) && isTRUE(all.equal(.result, expected)) + feedback <- if (correct) { + "Correct. This is the mean study time across all rows." + } else { + "Try again. Use mean(pilot_survey$study_hours)." + } + + learnrTrackR::track_gradethis_attempt( + context = tracking, + question_id = "q5_mean_study_hours", + submitted_answer = .user_code, + correct = correct, + feedback = feedback, + max_score = 1 + ) +}) +``` + +## Code exercise 2 + +Create one row per region with: + +- `region` +- `n` +- `median_study_hours` +- `mean_quiz_score` + +```{r q6_region_summary, exercise=TRUE} +pilot_survey |> + group_by(region) |> + summarise( + n = n(), + median_study_hours = median(study_hours), + mean_quiz_score = mean(quiz_score), + .groups = "drop" + ) +``` + +```{r q6_region_summary-solution} +pilot_survey |> + group_by(region) |> + summarise( + n = n(), + median_study_hours = median(study_hours), + mean_quiz_score = mean(quiz_score), + .groups = "drop" + ) +``` + +```{r q6_region_summary-check} +gradethis::grade_this({ + expected <- pilot_survey |> + dplyr::group_by(region) |> + dplyr::summarise( + n = dplyr::n(), + median_study_hours = median(study_hours), + mean_quiz_score = mean(quiz_score), + .groups = "drop" + ) |> + dplyr::arrange(region) + + candidate <- tryCatch( + { + .result |> + as.data.frame() |> + dplyr::arrange(region) + }, + error = function(cnd) NULL + ) + + required_columns <- c("region", "n", "median_study_hours", "mean_quiz_score") + has_required_columns <- !is.null(candidate) && + all(required_columns %in% names(candidate)) + + correct <- has_required_columns && + nrow(candidate) == nrow(expected) && + isTRUE(all.equal(candidate$n, expected$n)) && + isTRUE(all.equal(candidate$median_study_hours, expected$median_study_hours)) && + isTRUE(all.equal(candidate$mean_quiz_score, expected$mean_quiz_score)) + + score <- if (correct) { + 2 + } else if (has_required_columns) { + 1 + } else { + 0 + } + + grade_status <- if (correct) { + "correct" + } else if (score > 0) { + "partial" + } else { + "incorrect" + } + + feedback <- if (correct) { + "Correct. The grouped summary matches the expected result." + } else if (has_required_columns) { + "Partial credit. The expected columns are present, but at least one value differs." + } else { + "Try again. Create the columns region, n, median_study_hours, and mean_quiz_score." + } + + learnrTrackR::track_gradethis_attempt( + context = tracking, + question_id = "q6_region_summary", + submitted_answer = .user_code, + correct = correct, + score = score, + max_score = 2, + grade_status = grade_status, + feedback = feedback + ) +}) +``` + +## Teacher outputs + +After learner attempts have been recorded, use `inspect-results.R` in this +example directory to create attempts, scores, gradebook, Moodle, bundle, and +teacher report outputs. diff --git a/tests/testthat/test-course-pilot.R b/tests/testthat/test-course-pilot.R new file mode 100644 index 0000000..77f73ff --- /dev/null +++ b/tests/testthat/test-course-pilot.R @@ -0,0 +1,82 @@ +course_pilot_example_dir <- function() { + installed_dir <- system.file("examples/course-pilot", package = "learnrTrackR") + if (nzchar(installed_dir)) { + return(installed_dir) + } + + normalizePath( + file.path("..", "..", "inst", "examples", "course-pilot"), + mustWork = TRUE + ) +} + +test_that("course pilot configuration is coherent", { + example_dir <- course_pilot_example_dir() + config <- read_tracking_config(file.path(example_dir, "config-csv")) + + expect_equal(config$courses$course_id, "stat_intro") + expect_equal(config$tutorials$tutorial_id, "stat_descriptive_pilot") + expect_equal(nrow(config$students), 4) + expect_equal(nrow(config$questions), 6) + expect_equal(sum(config$questions$max_score), 7) + expect_true(all(config$questions$tutorial_id == "stat_descriptive_pilot")) +}) + +test_that("course pilot data support the tutorial answers", { + example_dir <- course_pilot_example_dir() + source(file.path(example_dir, "pilot-data.R"), local = TRUE) + + region_summary <- pilot_survey |> + dplyr::group_by(region) |> + dplyr::summarise( + median_study_hours = median(study_hours), + .groups = "drop" + ) |> + dplyr::arrange(dplyr::desc(median_study_hours)) + + expect_equal(nrow(pilot_survey), 12) + expect_equal(region_summary$region[[1]], "Sherbrooke") + expect_equal(mean(pilot_survey$study_hours), 77 / 12) +}) + +test_that("course pilot simulation writes teacher outputs", { + example_dir <- course_pilot_example_dir() + db_path <- withr::local_tempfile(fileext = ".sqlite") + output_dir <- withr::local_tempdir() + + withr::local_envvar( + LEARNRTRACKR_EXAMPLE_DIR = example_dir, + LEARNRTRACKR_DB = db_path, + LEARNRTRACKR_OUTPUT_DIR = output_dir, + LEARNRTRACKR_GROUP_ID = "A" + ) + + invisible(capture.output(suppressMessages( + source(file.path(example_dir, "simulate-results.R"), local = new.env(parent = globalenv())) + ))) + + con <- connect_tracking_db(db_path) + withr::defer(DBI::dbDisconnect(con)) + + grades <- gradebook(con, tutorial_id = "stat_descriptive_pilot", rule = "last") + expect_equal(nrow(get_attempts(con, tutorial_id = "stat_descriptive_pilot")), 18) + expect_equal(nrow(grades), 4) + expect_equal(max(grades$max_score), 7) + + invisible(capture.output(suppressMessages( + source(file.path(example_dir, "inspect-results.R"), local = new.env(parent = globalenv())) + ))) + + expected_files <- file.path( + output_dir, + c( + "course-pilot-attempts.csv", + "course-pilot-scores.csv", + "course-pilot-gradebook.csv", + "course-pilot-moodle.csv" + ) + ) + + expect_true(all(file.exists(expected_files))) + expect_true(dir.exists(file.path(output_dir, "course-pilot-bundle"))) +}) diff --git a/vignettes/course-pilot.Rmd b/vignettes/course-pilot.Rmd new file mode 100644 index 0000000..1c30733 --- /dev/null +++ b/vignettes/course-pilot.Rmd @@ -0,0 +1,104 @@ +--- +title: "Course pilot workflow" +output: rmarkdown::html_vignette +vignette: > + %\VignetteIndexEntry{Course pilot workflow} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>" +) +``` + +# Scope + +This vignette introduces the course pilot example included with +`learnrTrackR`. The pilot is a simulated descriptive statistics tutorial with +tracked `learnr` questions, tracked `gradethis` code exercises, CSV +configuration files, and teacher outputs. + +The data are simulated. They are intended only for testing the tracking +workflow. + +# Locate the example + +```{r locate-example} +library(learnrTrackR) + +example_dir <- system.file("examples/course-pilot", package = "learnrTrackR") +example_dir +``` + +The directory contains: + +```{r list-example} +list.files(example_dir) +``` + +# Read the configuration + +```{r read-config} +config_dir <- file.path(example_dir, "config-csv") +config <- read_tracking_config(config_dir) + +config$courses +config$tutorials +config$students +config$questions +``` + +# Launch the tutorial locally + +The tutorial itself is not launched during vignette rendering. To run it +locally: + +```{r run-tutorial, eval = FALSE} +Sys.setenv( + LEARNRTRACKR_DB = file.path(tempdir(), "learnrtrackr-course-pilot.sqlite"), + LEARNRTRACKR_STUDENT_ID = "student_demo", + LEARNRTRACKR_GROUP_ID = "A" +) + +learnr::run_tutorial( + file.path(example_dir, "tutorial.Rmd"), + clean = TRUE, + as_rstudio_job = FALSE +) +``` + +# Simulate a cohort + +The example also includes a script that records simulated attempts for a small +cohort. This is useful for checking exports and reports without using the +browser. + +```{r simulate, eval = FALSE} +source(file.path(example_dir, "simulate-results.R")) +``` + +# Inspect teacher outputs + +After running the tutorial or the simulation: + +```{r inspect, eval = FALSE} +source(file.path(example_dir, "inspect-results.R")) +``` + +The inspection script writes attempts, scores, gradebook rows, Moodle-ready +grades, a rich export bundle, and an HTML teacher report when `rmarkdown` is +available. + +# Dashboard + +The same database can be opened with the local teacher dashboard: + +```{r dashboard, eval = FALSE} +run_dashboard(Sys.getenv("LEARNRTRACKR_DB"), group_id = "A") +``` + +The dashboard is a local inspection interface for prototypes. It does not +replace institutional authentication.