From 1db7b08f4ee8d924a044bf0526bc4cba4c89c526 Mon Sep 17 00:00:00 2001 From: Philippe Grosjean Date: Thu, 30 May 2024 15:37:47 +0200 Subject: [PATCH] First complete version --- .Rbuildignore | 24 + .github/.gitignore | 1 + .github/workflows/R-CMD-check.yaml | 52 ++ .github/workflows/pkgdown.yaml | 50 ++ .gitignore | 13 +- CODE_OF_CONDUCT.md | 126 +++++ DESCRIPTION | 41 ++ LICENSE | 23 +- LICENSE.md | 21 + NAMESPACE | 51 ++ NEWS.md | 3 + R/class.R | 153 ++++++ R/github.R | 15 + R/learnitprogress-package.R | 24 + R/lrs.R | 301 ++++++++++++ R/plot_grade.R | 55 +++ R/plot_progression.R | 33 ++ R/progression.R | 469 +++++++++++++++++++ R/report_progress.R | 90 ++++ R/url.R | 106 +++++ README.md | 60 ++- TODO.md | 7 + _pkgdown.yml | 21 + inst/CITATION | 24 + inst/WORDLIST | 28 ++ inst/shiny/progress-report/app.R | 462 ++++++++++++++++++ learnitprogress.Rproj | 24 + man/check_profile.Rd | 61 +++ man/class_logins.Rd | 38 ++ man/get_data.Rd | 62 +++ man/learnitprogress-package.Rd | 32 ++ man/learnr_prog.Rd | 49 ++ man/plot_grade.Rd | 21 + man/plot_progression.Rd | 19 + man/report_progress.Rd | 51 ++ pkgdown/favicon/apple-touch-icon-120x120.png | Bin 0 -> 13055 bytes pkgdown/favicon/apple-touch-icon-152x152.png | Bin 0 -> 16814 bytes pkgdown/favicon/apple-touch-icon-180x180.png | Bin 0 -> 21739 bytes pkgdown/favicon/apple-touch-icon-60x60.png | Bin 0 -> 5035 bytes pkgdown/favicon/apple-touch-icon-76x76.png | Bin 0 -> 6989 bytes pkgdown/favicon/apple-touch-icon.png | Bin 0 -> 21739 bytes pkgdown/favicon/favicon-16x16.png | Bin 0 -> 737 bytes pkgdown/favicon/favicon-32x32.png | Bin 0 -> 1710 bytes pkgdown/favicon/favicon.ico | Bin 0 -> 32038 bytes tests/spelling.R | 3 + tests/testthat.R | 13 + tests/testthat/test-lrs.R | 4 + vignettes/.gitignore | 2 + vignettes/learnitprogress.Rmd | 25 + 49 files changed, 2627 insertions(+), 30 deletions(-) create mode 100644 .Rbuildignore create mode 100644 .github/.gitignore create mode 100644 .github/workflows/R-CMD-check.yaml create mode 100644 .github/workflows/pkgdown.yaml create mode 100644 CODE_OF_CONDUCT.md create mode 100644 DESCRIPTION create mode 100644 LICENSE.md create mode 100644 NAMESPACE create mode 100644 NEWS.md create mode 100644 R/class.R create mode 100644 R/github.R create mode 100644 R/learnitprogress-package.R create mode 100644 R/lrs.R create mode 100644 R/plot_grade.R create mode 100644 R/plot_progression.R create mode 100644 R/progression.R create mode 100644 R/report_progress.R create mode 100644 R/url.R create mode 100644 TODO.md create mode 100644 _pkgdown.yml create mode 100644 inst/CITATION create mode 100644 inst/WORDLIST create mode 100644 inst/shiny/progress-report/app.R create mode 100644 learnitprogress.Rproj create mode 100644 man/check_profile.Rd create mode 100644 man/class_logins.Rd create mode 100644 man/get_data.Rd create mode 100644 man/learnitprogress-package.Rd create mode 100644 man/learnr_prog.Rd create mode 100644 man/plot_grade.Rd create mode 100644 man/plot_progression.Rd create mode 100644 man/report_progress.Rd create mode 100755 pkgdown/favicon/apple-touch-icon-120x120.png create mode 100755 pkgdown/favicon/apple-touch-icon-152x152.png create mode 100755 pkgdown/favicon/apple-touch-icon-180x180.png create mode 100755 pkgdown/favicon/apple-touch-icon-60x60.png create mode 100755 pkgdown/favicon/apple-touch-icon-76x76.png create mode 100755 pkgdown/favicon/apple-touch-icon.png create mode 100755 pkgdown/favicon/favicon-16x16.png create mode 100644 pkgdown/favicon/favicon-32x32.png create mode 100644 pkgdown/favicon/favicon.ico create mode 100644 tests/spelling.R create mode 100644 tests/testthat.R create mode 100644 tests/testthat/test-lrs.R create mode 100644 vignettes/.gitignore create mode 100644 vignettes/learnitprogress.Rmd diff --git a/.Rbuildignore b/.Rbuildignore new file mode 100644 index 0000000..62479ff --- /dev/null +++ b/.Rbuildignore @@ -0,0 +1,24 @@ +.DS_Store +.gitignore +.git +.git/* +README.md +FAQ.md +TODO.md +LICENSE.md +Makefile +.Rprofile +^\.Rproj\.user$ +^\.*\Rproj$ +^\.travis\.yml$ +^appveyor\.yml$ +^.*\.Rproj$ +^doc$ +^Meta$ +^_pkgdown\.yml$ +^docs$ +^pkgdown$ +^\.github$ +^CODE_OF_CONDUCT\.md$ +^revdep$ +data-raw diff --git a/.github/.gitignore b/.github/.gitignore new file mode 100644 index 0000000..2d19fc7 --- /dev/null +++ b/.github/.gitignore @@ -0,0 +1 @@ +*.html diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml new file mode 100644 index 0000000..0f2fe08 --- /dev/null +++ b/.github/workflows/R-CMD-check.yaml @@ -0,0 +1,52 @@ +# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples +# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + +name: R-CMD-check + +permissions: read-all + +jobs: + R-CMD-check: + runs-on: ${{ matrix.config.os }} + + name: ${{ matrix.config.os }} (${{ matrix.config.r }}) + + strategy: + fail-fast: false + matrix: + config: + - {os: macos-latest, r: 'release'} + - {os: windows-latest, r: 'release'} + - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} + - {os: ubuntu-latest, r: 'release'} + - {os: ubuntu-latest, r: 'oldrel-1'} + + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + R_KEEP_PKG_SOURCE: yes + + steps: + - uses: actions/checkout@v4 + + - uses: r-lib/actions/setup-pandoc@v2 + + - uses: r-lib/actions/setup-r@v2 + with: + r-version: ${{ matrix.config.r }} + http-user-agent: ${{ matrix.config.http-user-agent }} + use-public-rspm: true + + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packages: any::rcmdcheck + needs: check + + - uses: r-lib/actions/check-r-package@v2 + with: + upload-snapshots: true + build_args: 'c("--no-manual","--compact-vignettes=gs+qpdf")' diff --git a/.github/workflows/pkgdown.yaml b/.github/workflows/pkgdown.yaml new file mode 100644 index 0000000..c9f0165 --- /dev/null +++ b/.github/workflows/pkgdown.yaml @@ -0,0 +1,50 @@ +# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples +# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + release: + types: [published] + workflow_dispatch: + +name: pkgdown + +permissions: read-all + +jobs: + pkgdown: + runs-on: ubuntu-latest + # Only restrict concurrency for non-PR jobs + concurrency: + group: pkgdown-${{ github.event_name != 'pull_request' || github.run_id }} + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + + - uses: r-lib/actions/setup-pandoc@v2 + + - uses: r-lib/actions/setup-r@v2 + with: + use-public-rspm: true + + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packages: any::pkgdown, local::. + needs: website + + - name: Build site + run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE) + shell: Rscript {0} + + - name: Deploy to GitHub pages 🚀 + if: github.event_name != 'pull_request' + uses: JamesIves/github-pages-deploy-action@v4.5.0 + with: + clean: false + branch: gh-pages + folder: docs diff --git a/.gitignore b/.gitignore index e75435c..e16ee6e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,6 @@ # Session Data files .RData -.RDataTmp # User-specific files .Ruserdata @@ -39,11 +38,11 @@ vignettes/*.pdf # R Environment Variables .Renviron -# pkgdown site +# Locally generated pkgdown site +docs docs/ -# translation temp files -po/*~ - -# RStudio Connect folder -rsconnect/ +# Mac files +.DS_Store +doc +Meta diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..d985ea2 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,126 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at phgrosjean@sciviews.org. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][https://github.com/mozilla/inclusion]. + +For answers to common questions about this code of conduct, see the FAQ at +. Translations are available at . + +[homepage]: https://www.contributor-covenant.org diff --git a/DESCRIPTION b/DESCRIPTION new file mode 100644 index 0000000..f4e6f22 --- /dev/null +++ b/DESCRIPTION @@ -0,0 +1,41 @@ +Package: learnitprogress +Type: Package +Version: 0.9.0 +Title: Report Student Progress in 'LearnIt::R' Courses +Description: A Shiny app that connects to you LRS ("Learning Record Store" + database, as created and managed by the 'learnitdown' package) and displays a + report showing the completion of exercices and projects. This report is + intended for students to track their progress in the course. +Authors@R: c(person("Philippe", "Grosjean", role = c("aut", "cre"), + email = "phgrosjean@sciviews.org", + comment = c(ORCID = "0000-0002-2694-9471"))) +Maintainer: Philippe Grosjean +Depends: + R (>= 4.2.0) +Imports: + cowplot, + dplyr, + fs, + ggplot2, + mongolite, + RCurl, + rlang, + shiny, + utils +Suggests: + shinybusy, + shinythemes, + knitr, + rmarkdown, + spelling, + testthat (>= 3.0.0) +License: MIT + file LICENSE +URL: https://github.com/learnitr/learnitprogress, https://learnitr.github.io/learnitprogress/ +BugReports: https://github.com/learnitr/learnitprogress/issues +Roxygen: list(markdown = TRUE) +RoxygenNote: 7.2.3 +VignetteBuilder: knitr +Encoding: UTF-8 +Language: en-US +ByteCompile: yes +Config/testthat/edition: 3 diff --git a/LICENSE b/LICENSE index 03b3f77..f712e7c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,2 @@ -MIT License - -Copyright (c) 2024 learnitr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +YEAR: 2024 +COPYRIGHT HOLDER: Philippe Grosjean diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..3302a3c --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +# MIT License + +Copyright (c) 2024 Philippe Grosjean + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/NAMESPACE b/NAMESPACE new file mode 100644 index 0000000..84ac6c4 --- /dev/null +++ b/NAMESPACE @@ -0,0 +1,51 @@ +# Generated by roxygen2: do not edit by hand + +export(check_profile) +export(class_aa) +export(class_logins) +export(get_all_data) +export(get_data) +export(get_github_data) +export(get_grade_data) +export(get_grade_dir) +export(get_grid_data) +export(get_profile) +export(h5p_prog) +export(is_correct_query) +export(learnr_prog) +export(plot_grade) +export(plot_progression) +export(profile_from_query) +export(query_params_list) +export(report_progress) +export(run_progress_report) +export(shiny_prog) +export(user_login) +import(shiny) +importFrom(RCurl,url.exists) +importFrom(cowplot,theme_cowplot) +importFrom(dplyr,bind_rows) +importFrom(dplyr,filter) +importFrom(dplyr,full_join) +importFrom(dplyr,mutate) +importFrom(dplyr,select) +importFrom(fs,dir_exists) +importFrom(fs,dir_ls) +importFrom(fs,path) +importFrom(ggplot2,aes) +importFrom(ggplot2,coord_flip) +importFrom(ggplot2,geom_col) +importFrom(ggplot2,geom_hline) +importFrom(ggplot2,geom_line) +importFrom(ggplot2,geom_point) +importFrom(ggplot2,geom_smooth) +importFrom(ggplot2,geom_vline) +importFrom(ggplot2,ggplot) +importFrom(ggplot2,labs) +importFrom(ggplot2,position_nudge) +importFrom(ggplot2,scale_fill_brewer) +importFrom(ggplot2,scale_y_discrete) +importFrom(mongolite,mongo) +importFrom(rlang,.data) +importFrom(utils,URLencode) +importFrom(utils,browseURL) diff --git a/NEWS.md b/NEWS.md new file mode 100644 index 0000000..f8a6215 --- /dev/null +++ b/NEWS.md @@ -0,0 +1,3 @@ +# learnitprogress 0.9.0 + +- Code developed so far is now placed in the {learnitprogress} package. diff --git a/R/class.R b/R/class.R new file mode 100644 index 0000000..f29b32a --- /dev/null +++ b/R/class.R @@ -0,0 +1,153 @@ +# sdd1m, sdd1c, sdd2m, sdd2c, sdd3m, sdd4m, sdd5m + +#' Get a list of all user logins in a given class +#' +#' @param class The short identifier of the class +#' @param url The URL of the MongoDB database (LRS) +#' @param as.json If `TRUE`, return the result as a JSON string +#' +#' @return A character vector with the results +#' @export +class_logins <- function(class, url = getOption("learnitr.lrs_url"), + as.json = FALSE) { + # TODO: allow using this in a different context as SDD + query <- switch(class, + sdd1m = '{ "icflag": { "$in": ["S-BIOG-006", "S-BIOG-006,S-BIOG-015", "S-BIOG-027", "S-BIOG-027,S-BIOG-061"] }, "enrolled": true }', + sdd1c = '{ "icourse": "S-BIOG-921", "enrolled": true }', + sdd2m = '{ "icflag": { "$in": ["S-BIOG-015", "S-BIOG-006,S-BIOG-015", "S-BIOG-061", "S-BIOG-027,S-BIOG-061"] }, "enrolled": true }', + sdd2c = '{ "icourse": "S-BIOG-937-958-959", "enrolled": true }', + sdd3m = '{ "icourse": "S-BIOG-025", "enrolled": true }', + sdd4mold = '{ "icourse": "S-BIOG-043", "enrolled": true }', + sdd4m = '{ "icourse": "S-BIOG-077", "enrolled": true }', + stop("Only 'sdd1m/c', 'sdd2m/c', 'sdd3m', or 'sdd4m(old)' are recognized classes")) + mdb <- try(mongolite::mongo("users", url = url), silent = TRUE) + if (inherits(mdb, "try-error")) + stop("Error: impossible to connect to the users database") + res <- mdb$find(query, '{ "login" : true, "iemail" : true, "_id" : false }') + + email <- res$iemail + res <- res$login + + # Do we output the json string to be used in MongoDB directly? + if (isTRUE(as.json)) { + res2 <- paste(res, collapse = '", "') + res <- structure(paste0('{ "$in": ["', res2, '"] }'), + logins = res, n = length(res)) + } else { + res <- structure(res, email = email) + } + res +} + +# Get a selection of the apps that match a given aa +# (optionally restrict to a single module) +# tt <- paste0("A", formatC(1:12, width = 2, flag = "0")); tt +# grepl("^A0[1-6]", tt) +# grepl("^A(0[7-9])|(1[0-2])", tt) +#' @export +#' @rdname class_logins +#' @param aa The short identifier of the activity +#' @param module The module to restrict too, or `NULL` for all modules +class_aa <- function(aa, url = getOption("learnitr.lrs_url"), as.json = FALSE, + module = NULL) { + # This should be extracted from the database instead! + # 2021-2022: we also exclude XnnA that correspond to excluded H5P exercices + if (!is.null(module)) { + # We only look for exercises in a specific module + rx <- switch(aa, + sdd1mq1 = , + sdd1cq1 = , + sdd1mq2 = , + sdd1cq2 = , + sdd1mq3 = , + sdd1cq3 = '^A', + sdd2mq1 = , + sdd2cq1 = , + sdd2mq2 = , + sdd2cq2 = , + #sdd2cq3 = , + sdd2mq3 = , + sdd2cq3 = '^B', + sdd3mq1 = , + sdd3mq3 = '^C', + sdd4mq1 = '^D', + sdd5mq1 = '^E', + stop("Not implemented for this aa")) + rx <- paste0(rx, module, '[^A]') + if (isTRUE(as.json)) + rx <- paste0('{ "$regex": "', rx, '", "$options": "" }') + return(rx) + } + + # prerequisite sections and should not be counted here + if (isTRUE(as.json)) { + switch(aa, + sdd1mq1 = , + sdd1cq1 = '{ "$regex": "^A0[1-6][^A]", "$options": "" }', + sdd1mq2 = , + sdd1cq2 = '{ "$regex": "^A(0[7-9])|(1[0-2])[^A]", "$options": "" }', + sdd1mq3 = , + sdd1cq3 = '{ "$regex": "^A(0[1-9])|(1[0-2])[^A]", "$options": "" }', + sdd2mq1 = , + sdd2cq1 = '{ "$regex": "^B0[1-5][^A]", "$options": "" }', + sdd2mq2 = , + sdd2cq2 = '{ "$regex": "^B(0[6-9])|(10)[^A]", "$options": "" }', + #sdd2cq3 = '{ "$regex": "^B(09)|(1[0-2])[^A]", "$options": "" }', + sdd2mq3 = , + sdd2cq3 = '{ "$regex": "^B(0[1-9])|(10)[^A]", "$options": "" }', + sdd3mq1 = '{ "$regex": "^C0[1-6][^A]", "$options": "" }', + sdd3mq3 = '{ "$regex": "^C0[1-6][^A]", "$options": "" }', + sdd4mq1 = '{ "$regex": "^D0[1-6][^A]", "$options": "" }', + #sdd5mq1 = '{ "$regex": "^E0[1-4][^A]", "$options": "" }', + stop("Not implemented for this aa")) + } else { + switch(aa, + sdd1mq1 = , + sdd1cq1 = "^A0[1-6][^A]", + sdd1mq2 = , + sdd1cq2 = "^A(0[7-9])|(1[0-2])[^A]", + sdd1mq3 = , + sdd1cq3 = "^A(0[1-9])|(1[0-2])[^A]", + sdd2mq1 = , + sdd2cq1 = "^B0[1-5][^A]", + sdd2mq2 = , + sdd2cq2 = "^B(0[6-9])|(10)[^A]", + sdd2mq3 = , + sdd2cq3 = "^B(0[1-9])|(10)[^A]", + sdd3mq1 = "^C0[1-6][^A]", + sdd3mq3 = "^C0[1-6][^A]", + sdd4mq1 = "^D0[1-6][^A]", + #sdd5mq1 = "^E0[1-4][^A]", + stop("Not implemented for this aa")) + } +} + +# Get the user login, given its email +#' @export +#' @rdname class_logins +#' @param email The email of the student +user_login <- function(email, url = getOption("learnitr.lrs_url")) { + # Make xsure email is in lowercase + email <- tolower(email) + # Get the login associated with a given email address + # In case we have several login, return the most recent one + mdb <- try(mongolite::mongo("users", url = url), silent = TRUE) + if (inherits(mdb, "try-error")) + stop("Error: impossible to connect to the users database") + #query <- paste0('{ "iemail": "', email_moodle_case(email), '" }') + query <- paste0('{ "iemail": "', tolower(email), '" }') + fields <- '{ "login": true, "registered": true, "_id": false }' + if (!mdb$count(query)) + return(NULL) # This email is not found + res <- mdb$find(query, fields) + try(mdb$disconnect, silent = TRUE) # Not all versions have this! + # If there are several logins, return most recent one, and the list of all the + # others as an attribute + if (nrow(res) > 1) { + res <- res[order(res$registered, decreasing = TRUE), ] + logins <- res$login + structure(res$login[1], logins = logins) + } else {# Only one item + res$login + } +} diff --git a/R/github.R b/R/github.R new file mode 100644 index 0000000..5a95f4c --- /dev/null +++ b/R/github.R @@ -0,0 +1,15 @@ +# Check if a github user exists (just check if the URL exists, but could also +# be an organization and could not be indeed the user's account => check later) +#' Check a GitHub account +#' +#' @param user The user login to check on GitHub. +#' +#' @return `TRUE` if the user login exists on GitHub, `FALSE` otherwise. +#' @export +#' +#' @examples +#' is_github_user("phgrosjean") +#' is_github_user("nonexistinguser") +is_github_user <- function(user) { + RCurl::url.exists(paste0("https://github.com/", user, "/")) +} diff --git a/R/learnitprogress-package.R b/R/learnitprogress-package.R new file mode 100644 index 0000000..16fe7fa --- /dev/null +++ b/R/learnitprogress-package.R @@ -0,0 +1,24 @@ +#' @details +#' The learnitprogress package implements a Shiny application that summarizes +#' the progression of each of your course's students in their LearnIt::R +#' exercises and projects. It queries the MongoDB database that is collecting +#' your LearnIt::R activities (H5P, Shiny, learnr, GitHub projects...). +#' +#' See ... +#' @keywords internal +"_PACKAGE" + +#' @import shiny +#' @importFrom cowplot theme_cowplot +#' @importFrom dplyr bind_rows filter full_join mutate select +#' @importFrom fs dir_exists dir_ls path +#' @importFrom ggplot2 aes coord_flip geom_col geom_hline geom_line geom_point geom_smooth geom_vline ggplot labs position_nudge scale_fill_brewer scale_y_discrete +#' @importFrom mongolite mongo +#' @importFrom RCurl url.exists +#' @importFrom rlang .data +#' @importFrom utils browseURL URLencode +# The following block is used by usethis to automatically manage +# roxygen namespace tags. Modify with care! +## usethis namespace: start +## usethis namespace: end +NULL diff --git a/R/lrs.R b/R/lrs.R new file mode 100644 index 0000000..657feeb --- /dev/null +++ b/R/lrs.R @@ -0,0 +1,301 @@ +#' Get data from exercises or projects for one student +#' +#' @param collection The collection in the MongoDB database +#' @param query The JSON query on the MongoDB database +#' @param fields The fields to return +#' @param url The url to access the MongoDB database +#' @param count Do we count items instead of getting them? +#' +#' @return A data frame or a list with the data +#' @export +get_data <- function(collection, query, fields = '{}', + url = getOption("learnitr.lrs_url"), count = FALSE) { + mdb <- try(mongolite::mongo(collection, url = url), silent = TRUE) + if (inherits(mdb, "try-error")) { + stop("Error: impossible to connect to the database") + } else { + if (isTRUE(count)) { + res <- mdb$count(query) + } else { + res <- mdb$find(query, fields = fields) + } + } + mdb$disconnect() + res +} + +#' @export +#' @rdname get_data +#' @param email The email of the student +#' @param login The GitHub login of the student as alternate way to get its +#' profile (when it is not `NULL`) +get_profile <- function(email, login = NULL) { + if (is.null(login)) { + get_data("users", query = + paste0('{"iemail" : { "$eq" : "', tolower(email), '" } }'))[1, ] + } else { + get_data("users", query = + paste0('{"iemail" : { "$eq" : "', tolower(email), + '" }, "user_login": { "$eq": "', login, '" } }'))[1, ] + } +} + +#' @export +#' @rdname get_data +#' @param icourse The course identifier +#' @param module The module, or `NULL` for exercises in all modules +get_all_data <- function(email, icourse, module = NULL) { + message("Email: '", email, "'") + message("ICourse: '", icourse, "'") + if (!is.null(module)) + message("Module: '", module, "'") + if (is.null(email) || email == "") + return() + + # TODO: allow for using this outside of SDD courses + course <- "sdd1m" # Default for S-BIOG-006 & S-BIOG-027 + if (icourse == "S-BIOG-015" || icourse == "S-BIOG-061") course <- "sdd2m" + if (icourse == "S-BIOG-025") course <- "sdd3m" + if (icourse == "S-BIOG-921") course <- "sdd1c" + if (icourse == "S-BIOG-937-958-959") course <- "sdd2c" + message("Course: '", course, "'") + aa <- paste0(course, "q1") + # Special case for q2 courses + if (icourse == "S-BIOG-027") aa <- "sdd1mq2" + if (icourse == "S-BIOG-061") aa <- "sdd2mq2" + class_logs <- class_logins(course, as.json = TRUE) + message("Class query: ", class_logs) + class_apps <- class_aa(aa, as.json = TRUE, module = module) + user <- user_login(email) + message("User login: '", user, "'") + resh <- h5p_prog(user, class_logs, class_apps) + #print(nrow(resh)) + #print(head(resh)) + resl <- learnr_prog(user, class_logs, class_apps) + #print(nrow(resl)) + #print(head(resl)) + ress <- shiny_prog(user, class_logs, class_apps) + #print(nrow(ress)) + #print(head(ress)) + # We deal with all the possible cases of missing data + if (is.null(resh) && is.null(resl) && is.null(ress) ) + return(structure(list(), comment = "Pas encore d'enregistrements.")) + if (is.null(resh)) { + if (is.null(resl)) { + res <- ress + } else {# resl not null + if (is.null(ress)) { + res <- resl + } else { + res <- rbind(resl, ress) + } + } + } else {# resh not null + res <- resh + if (!is.null(resl)) + res <- rbind(res, resl) + if (!is.null(ress)) + res <- rbind(res, ress) + } + + res <- res[order(res$app), ] #res %<-% arrange(res, app) + # If not any student finish an exercise, we got NA in different places + # -> replace these by zero for the plot + res$max[is.na(res$max)] <- 0 + res$progress_max[is.na(res$progress_max)] <- 0 + res$progress[is.na(res$progress)] <- 0 + res$raw_score_max[is.na(res$raw_score_max)] <- 0 + res$raw_score_avg[is.na(res$raw_score_avg)] <- 0 + res$raw_score[is.na(res$raw_score)] <- 0 + res$count[is.na(res$count)] <- 0 + res$activity[is.na(res$activity)] <- 0 + attr(res, "user") <- user + res +} + +#' @export +#' @rdname get_data +get_github_data <- function(email) { + # Return NULL for now + NULL +} + +#' @export +#' @rdname get_data +get_grid_data <- function(email, icourse, module = NULL) { + message("Email: '", email, "'") + message("ICourse: '", icourse, "'") + if (!is.null(module)) + message("Module: '", module, "'") + if (is.null(email) || email == "") + return() + + course <- "sdd1m" # Default for S-BIOG-006 & S-BIOG-027 + if (icourse == "S-BIOG-015" || icourse == "S-BIOG-061") course <- "sdd2m" + if (icourse == "S-BIOG-025") course <- "sdd3m" + if (icourse == "S-BIOG-921") course <- "sdd1c" + if (icourse == "S-BIOG-937-958-959") course <- "sdd2c" + aa <- paste0(course, "q1") + # Special case for q2 courses + if (icourse == "S-BIOG-027") aa <- "sdd1mq2" + if (icourse == "S-BIOG-061") aa <- "sdd2mq2" + message("Course aa: '", aa, "'") + user <- user_login(email) + grade_dir <- get_grade_dir() + if (is.null(grade_dir)) + return() + # Correction grids are supposed to be in ///*.csv + grid_dir <- fs::path(grade_dir, aa, user) + message("Grids dir: '", grid_dir, "'") + if (!dir_exists(grid_dir)) + return() + grids <- dir_ls(grid_dir, glob = "*.csv") + message("Number of grids: '", length(grids), "'") + if (!length(grids)) + return() + names(grids) <- sub("\\.csv$", "", basename(grids)) + # Do we return only grids for one module? + if (!is.null(module)) { + grids_modules <- substring(names(grids), 2L, 3L) + grids <- grids[grids_modules == module] + if (!length(grids)) + return() + } + grids +} + +#' @export +#' @rdname get_data +get_grade_data <- function(email, icourse) { + message("Email: '", email, "'") + message("ICourse: '", icourse, "'") + if (is.null(email) || email == "") + return() + + course <- "sdd1m" # Default for S-BIOG-006 & S-BIOG-027 + if (icourse == "S-BIOG-015" || icourse == "S-BIOG-061") course <- "sdd2m" + if (icourse == "S-BIOG-025") course <- "sdd3m" + if (icourse == "S-BIOG-921") course <- "sdd1c" + if (icourse == "S-BIOG-937-958-959") course <- "sdd2c" + aa <- paste0(course, "q1") + # Special case for q2 courses + if (icourse == "S-BIOG-027") aa <- "sdd1mq2" + if (icourse == "S-BIOG-061") aa <- "sdd2mq2" + message("Course aa: '", aa, "'") + user <- user_login(email) + grade_dir <- get_grade_dir() + if (is.null(grade_dir)) + return() + # Data are supposed to be in //grade_.rds + grade_file <- fs::path(grade_dir, aa, paste0("grade_", user, ".rds")) + message("Grade file: '", grade_file, "'") + if (!file.exists(grade_file)) + return() + readRDS(grade_file) +} + +# TODO: allow to customise root_dir +#' @export +#' @rdname get_data +#' @param root_dir The root directory where the grades are stored. +#' @param acad_year The academic year to use, or `NULL` to use the latest one. +get_grade_dir <- function(root_dir = "/data1/grades", acad_year = NULL) { + if (!dir_exists(root_dir)) + return(NULL) + if (is.null(acad_year)) { + # Look for the latest subdir in alphabetic order and use it + sub_dirs <- dir_ls(root_dir,type = "directory") + if (!length(sub_dirs)) + return(NULL) + acad_year <- basename(sub_dirs[length(sub_dirs)]) + } + # Check that the full directory exists + grade_dir <- fs::path(root_dir, acad_year) + if (!dir.exists(grade_dir)) + return(NULL) + grade_dir +} + + +# Unused code for now ----------------------------------------------------- + +#correct <- function(x, name, old, new) { +# x[x[[name]] == old, name] <- new +# x +#} + +#correct_by_id_version <- function(x, id, version, name, new) { +# x[x$id == id & x$version == version, name] <- new +# x +#} + +#get_learnr_data <- function(email) { +# res <- get_data("learnr", +# query = +# paste0('{"email" : { "$eq" : "', email, '" }, "max" : { "$gt" : 0 } }'), +# fields = '{ "data" : false }' +# ) +# # # I need to correct XNNa_... -> XNNLa_... +# # res$app <- sub("^([A-E][0-9]{2})([a-z]_.+$)", "\\1L\\2", res$app) +# # # I need to correct B01La_rappel because it is indicated 22 questions, +# # # but there are 23 of them!!! +# # res[res$app == "B01La_rappel", "max"] <- 23 +# # Eliminate A00, B00, C00, ... +# res <- res[!grepl("^.00", res$app), ] +# # We keep only highest score for each ex (app_label) +# # So, sort score descending, make app_label, and keep only unique items +# res <- res[order(res$score, decreasing = TRUE), ] +# app_label <- paste(res$app, res$label, sep = "_") +# res <- res[!duplicated(app_label), ] +# res +#} + +#get_h5p_data <- function(email) { +# res <- get_data("h5p", +# query = +# paste0('{"email" : { "$eq" : "', tolower(email), +# '" }, "max" : { "$gt" : 0 } }'), +# fields = '{ "data" : false }' +# ) +# # Well, here names are a complete mess! Thus, lot of work to correct these! +# # Replace NA by "" in version +# res[is.na(res$version), "version"] <- "" +# #res <- correct_by_id_version(res, "8", "437dae39-40f6-4251-a056-f747a9888778", +# # "app", "A02Hb_R markdown/diff script markdown") +# #res <- correct_by_id_version(res, "8", "9d23d3a0-490c-450b-9cdd-e8d0927f28ed", +# # "app", "A02Hb_R markdown/chunk definition") +# #res <- correct_by_id_version(res, "8", "21444f3a-efed-4c92-99d6-f870628611f4", +# # "app", "A02Hb_R markdown/why chunk") +# #res <- correct_by_id_version(res, "8", "3f31e62e-9c9c-457f-9a7d-ad08060f3588", +# # "app", "") # This just collects results from chunk definition & why chunk +# #res <- correct_by_id_version(res, "10", "dc224f3d-32a7-43c8-a92a-c3d3c0694cfd", +# # "app", "A02Ha_nuage/questions") +# #res <- correct_by_id_version(res, "10", "", +# # "app", "A02Ha_nuage") +# # ... +# # Eliminate items where app == "" +# res <- res[res$app != "", ] +# # Sort by decreasing score, then eliminate duplicate app items +# res <- res[order(res$score, decreasing = TRUE), ] +# res <- res[!duplicated(res$app), ] +# # Completed items are redundant with answered items +# res <- res[res$verb != "completed", ] +# res +#} + +#get_shiny_data <- function(email) { +# res <- get_data("shiny", +# query = +# paste0('{"email" : { "$eq" : "', tolower(email), +# '" }, "max" : { "$gt" : 0 } }'), +# fields = '{ "data" : false }' +# ) +# # # Unfortunately, corrections are required for app names +# # res <- correct(res, "app", "A02a_limits", "A02Sa_limits") +# +# # Keep only highest score obtained for each item +# res <- res[order(res$version, decreasing = TRUE), ] +# res <- res[order(res$score, decreasing = TRUE), ] +# res <- res[!duplicated(res$app), ] +# res +#} diff --git a/R/plot_grade.R b/R/plot_grade.R new file mode 100644 index 0000000..b3ac963 --- /dev/null +++ b/R/plot_grade.R @@ -0,0 +1,55 @@ +#' Visualize details about the grade for one student +#' +#' @param data Grading data +#' @param show.name Indicate the name of the student in the title +#' @param give.note Indicate the final note in the title +#' +#' @return A ggplot object +#' @export +plot_grade <- function(data, show.name = TRUE, give.note = TRUE) { + email <- data[1, ]$email + name <- sub("\\.", " ", sub("@.+$", " ", email)) + login <- data[1, ]$login + score <- round(data[1, ]$score, 1) + max <- data[1, ]$max + + # First combine summary with details (leaving one empty line between the two) + scores <- bind_rows( + mutate(attr(data, "details"), app = paste0(" ", .data$app)), + data.frame(app = "---------------"), + mutate(attr(data, "summary"), score = .data$score / .data$max * 10, + max = 10, app = ifelse(.data$type == "challenge", "challenge", + paste(.data$type, "(all)"))) + ) + + if (isTRUE(show.name)) { + if (isTRUE(give.note)) { + subtitle <- paste0(name, " - ", score, "/", max) + } else { + subtitle <- paste0(name, " - ", "(pas encore de note)") + } + } else { + if (isTRUE(give.note)) { + subtitle <- paste0("Note globale : ", score, "/", max) + } else { + subtitle <- paste0("Note globale : ", "(pas encore de note)") + } + subtitle <- paste0("Note globale : ", score, "/", max) + } + + ggplot(data = scores, aes(x = .data$app, y = .data$max)) + + geom_hline(yintercept = c(0:max(data$max)), col = "gray") + + # White background + geom_col(fill = "white", col = "gray50", width = 0.9) + + # Add max + geom_col(aes(x = .data$app, y = .data$max), fill = "gray95", col = "gray50", + width = 0.9) + + # User scores + geom_col(aes(x = .data$app, y = .data$score, fill = .data$type), + col = "black", width = 0.9) + + coord_flip() + + scale_y_discrete(expand = c(0, 0)) + + cowplot::theme_cowplot(font_size = 10) + + labs(subtitle = subtitle, x = "", y = "") + + scale_fill_brewer(palette = "Dark2") +} diff --git a/R/plot_progression.R b/R/plot_progression.R new file mode 100644 index 0000000..5a28c4f --- /dev/null +++ b/R/plot_progression.R @@ -0,0 +1,33 @@ +#' Progression plot for one student (exercises H5P, Shiny & Learnr) +#' +#' @param user The user login +#' @param data The data for this user +#' +#' @return A ggplot object +#' +#' @export +plot_progression <- function(user, data) { + ggplot(data = data, aes(x = .data$app, y = .data$max)) + + geom_hline(yintercept = c(0:max(data$max)), col = "gray") + + # White background + geom_col(fill = "white", col = "gray50", width = 0.9) + + # Average and max progression (avg and max raw_score not shown here) + geom_col(aes(x = .data$app, y = .data$progress_max), fill = "gray95", + col = "gray50", width = 0.9) + + geom_col(aes(x = .data$app, y = .data$raw_score_avg), fill = "gray85", + col = "gray50", width = 0.9) + + # User's progression and raw_score + geom_col(aes(x = .data$app, y = .data$progress), fill = "red4", + col = "black", width = 0.5) + + geom_col(aes(x = .data$app, y = .data$raw_score), fill = "royalblue4", + col = "black", width = 0.5) + + # User's relative activity indicator + geom_col(aes(x = .data$app, y = .data$activity), fill = "black", + col = "black", width = 0.03, position = position_nudge(x = -0.45)) + + coord_flip() + + #scale_x_discrete(expand = c(0, 0)) + + scale_y_discrete(expand = c(0, 0)) + + cowplot::theme_cowplot(font_size = 14) + + labs(x = "", y = "") + #labs(title = paste("Progression -", user), x = "", y = "") +} diff --git a/R/progression.R b/R/progression.R new file mode 100644 index 0000000..92b8d72 --- /dev/null +++ b/R/progression.R @@ -0,0 +1,469 @@ +#' Progression in learnrs, H5P and Shiny apps +#' +#' @param user_login The GitHub login of one student +#' @param class_logins All the logins for the class +#' @param class_aa The AA identifier for this class +#' @param class_data The data for this class +#' @param url The url of the MongoDB data base +#' +#' @return A data frame with all the progression data for the user and the class +#' @export +learnr_prog <- function(user_login, class_logins, class_aa, class_data = NULL, + url = getOption("learnitr.lrs_url")) { + if (is.null(class_data)) { + class_data <- learnr_class_prog(class_logins, class_aa, url = url) + } + if (is.null(class_data)) + return(NULL) # No Shiny data for this course + user_data <- learnr_user_prog(user_login, class_aa, url = url) + if (is.null(user_data)) { + n <- nrow(class_data) + user_data <- data.frame(app = class_data$app, id = rep("", n), + items_done = rep("", n), count = rep(0, n), + progress = rep(0, n), raw_score = rep(0, n)) + } + + # Merge statistics for user and class by app + res <- dplyr::full_join(class_data, user_data) + res <- dplyr::select(res, 'app', 'id', 'items', 'items_done', 'max', + 'progress_max', 'progress', 'raw_score_max', 'raw_score_avg', 'raw_score', + 'count_all', 'count_avg', 'count') + res <- dplyr::mutate(res, + activity = pmin(1, .data$count / .data$count_avg) * + pmax(.data$max, na.rm = TRUE)) + as.data.frame(res) +} + +#' @export +#' @rdname learnr_prog +h5p_prog <- function(user_login, class_logins, class_aa, class_data = NULL, + url = getOption("learnitr.lrs_url")) { + if (is.null(class_data)) { + class_data <- h5p_class_prog(class_logins, class_aa, url = url) + } + if (is.null(class_data)) + return(NULL) # No Shiny data for this course + + user_data <- h5p_user_prog(user_login, class_aa, url = url) + if (is.null(user_data)) { + n <- nrow(class_data) + user_data <- data.frame(id = class_data$id, + items_done = rep("", n), count = rep(0, n), + progress = rep(0, n), raw_score = rep(0, n)) + } + + # Merge statistics for user and class by app + res <- dplyr::full_join(class_data, user_data) + res <- dplyr::select(res, 'app', 'id', 'items', 'items_done', 'max', + 'progress_max', 'progress', 'raw_score_max', 'raw_score_avg', 'raw_score', + 'count_all', 'count_avg', 'count') + res <- dplyr::mutate(res, + activity = pmin(1, .data$count / .data$count_avg) * + pmax(.data$max, na.rm = TRUE)) + as.data.frame(res) +} + +#' @export +#' @rdname learnr_prog +shiny_prog <- function(user_login, class_logins, class_aa, class_data = NULL, + url = getOption("learnitr.lrs_url")) { + if (is.null(class_data)) { + class_data <- shiny_class_prog(class_logins, class_aa, url = url) + } + if (is.null(class_data)) + return(NULL) # No Shiny data for this course + user_data <- shiny_user_prog(user_login, class_aa, url = url) + if (is.null(user_data)) { + n <- nrow(class_data) + user_data <- data.frame(app = class_data$app, id = rep("", n), + items = class_data$app, items_done = rep("", n), count = rep(0, n), + progress = rep(0, n), raw_score = rep(0, n)) + } + + # Merge statistics for user and class by app + res <- dplyr::full_join(class_data, user_data) + res <- dplyr::select(res, 'app', 'id', 'items', 'items_done', 'max', + 'progress_max', 'progress', 'raw_score_max', 'raw_score_avg', 'raw_score', + 'count_all', 'count_avg', 'count') + res <- dplyr::mutate(res, + activity = pmin(1, .data$count / .data$count_avg) * + pmax(.data$max, na.rm = TRUE)) + as.data.frame(res) +} + + +# Internal functions ------------------------------------------------------ + +# Get Learnr data for a whole class +learnr_class_prog <- function(class_logins, class_aa, + url = getOption("learnitr.lrs_url")) { + mdb_learnr <- try(mongolite::mongo("learnr", url = url), silent = TRUE) + if (inherits(mdb_learnr, "try-error")) + stop("Error: impossible to connect to the learnr database") + + if (!mdb_learnr$count(paste0('{ "login": ', class_logins, ', + "app": ', class_aa, ', "max": { "$gt": 0 }, + "verb": { "$in": ["answered", "submitted"] } }'))) + return(NULL) + + part1 <- mdb_learnr$aggregate(paste0('[ { + "$match": { + "login": ', class_logins, ', + "app": ', class_aa, ', + "max": { "$gt": 0 }, + "verb": { "$in": ["answered", "submitted"] } + } +}, { + "$group": { + "_id": "$app", + "count": { "$sum": 1 }, + "done": { "$addToSet": "$label" }, + "succeeded": { "$addToSet": { + "$cond": [ { "$eq": ["$score", 1] }, "$label", null ] + } }, + "max": { "$max": "$max" } + } +}, { + "$project": { + "app": "$_id", + "items": "$done", + "count_all": "$count", + "count_avg": { "$divide": ["$count", ', attr(class_logins, "n"), '] }, + "progress_max": { "$size": "$done" }, + "raw_score_max": { "$size": { "$setIntersection": ["$succeeded", "$done"] } }, + "max": "$max", + "_id": false + } +} ]')) + + # raw_score_avg + part2 <- mdb_learnr$aggregate(paste0('[ { + "$match": { + "login": ', class_logins, ', + "app": ', class_aa, ', + "max": { "$gt": 0 }, + "verb": { "$in": ["answered", "submitted"] } + } +}, { + "$group": { + "_id": { "login": "$login", "app": "$app" }, + "done": { "$addToSet": "$label" }, + "succeeded": { "$addToSet": { + "$cond": [ { "$eq": ["$score", 1]}, "$label", null ] + } } + } +}, { + "$project": { + "login": "$_id.login", + "app": "$_id.app", + "raw_score_max_by_user": { "$size": { "$setIntersection": ["$succeeded", "$done"] } }, + "_id": false + } +}, { + "$group": { + "_id": "$app", + "raw_score_avg": { "$avg": "$raw_score_max_by_user" } + } +}, { + "$project": { + "app": "$_id", + "raw_score_avg": { "$ifNull": [ "$raw_score_avg", 0] }, + "_id": false + } +} ]')) + dplyr::full_join(part1, part2) +} + +# Get learnrs progression for one student +learnr_user_prog <- function(user_login, class_aa, + url = getOption("learnitr.lrs_url")) { + mdb_learnr <- try(mongolite::mongo("learnr", url = url), silent = TRUE) + if (inherits(mdb_learnr, "try-error")) + stop("Error: impossible to connect to the learnr database") + + if (!mdb_learnr$count(paste0('{ "login": "', user_login, '", + "app": ', class_aa, ', "max": { "$gt": 0 }, + "verb": { "$in": ["answered", "submitted"] } }'))) + return(NULL) + + mdb_learnr$aggregate(paste0('[ { + "$match": { + "login": "', user_login, '", + "app": ', class_aa, ', + "max": { "$gt": 0 }, + "verb": { "$in": ["answered", "submitted"] } + } +}, { + "$group": { + "_id": "$app", + "count": { "$sum": 1 }, + "done" : { "$addToSet": "$label" }, + "succeeded": { "$addToSet": { + "$cond": [ { "$eq": ["$score", 1]}, "$label", null ] + } } + } +}, { + "$project": { + "app": "$_id", + "id": "", + "items_done": { "$setDifference": [ "$succeeded", [null] ] }, + "count": "$count", + "progress": { "$size": "$done" }, + "raw_score": { "$size": { "$setIntersection": ["$succeeded", "$done"] } }, + "_id": false + } +} ]')) +} + +# Get H5P data for a whole class +h5p_class_prog <- function(class_logins, class_aa, + url = getOption("learnitr.lrs_url")) { + mdb_h5p <- try(mongolite::mongo("h5p", url = url), silent = TRUE) + if (inherits(mdb_h5p, "try-error")) + stop("Error: impossible to connect to the H5P database") + + if (!mdb_h5p$count(paste0('{ "login": ', class_logins, ', + "app": ', class_aa, ' }'))) + return(NULL) + + part1 <- mdb_h5p$aggregate(paste0('[ { + "$match": { + "login": ', class_logins, ', + "app": ', class_aa, ' + } +}, { + "$group": { + "_id": "$id", + "apps": { "$addToSet": { + "$cond": [ { "$eq": ["$version", null] }, "$app", null ] + } }, + "count": { "$sum": { + "$cond": [ { "$in": ["$verb", ["attempted"]] }, 0, 1 ] + } } + } +}, { + "$project": { + "id": "$_id", + "app": { "$first": { "$setDifference": [ "$apps", [null] ] } }, + "count_all": "$count", + "count_avg": { "$divide": ["$count", ', attr(class_logins, "n"), '] }, + "_id": false + } +} ]')) + + part2 <- mdb_h5p$aggregate(paste0('[ { + "$match": { + "login": ', class_logins, ', + "app": ', class_aa, ', + "verb": "answered", + "app": { "$ne": "" }, + "max": { "$gt": 0 } + } +}, { + "$group": { + "_id": { "id": "$id", "app": "$app" }, + "score_max": { "$max": "$score" }, + "score_avg": { "$avg": "$score" }, + "max": { "$max": "$max" } + } +}, { + "$project": { + "id": "$_id.id", + "app": "$_id.app", + "progress": "$max", + "raw_score_max": "$score_max", + "raw_score_avg": "$score_avg", + "max": "$max", + "_id": false + } +}, { + "$group": { + "_id": "$id", + "items": { "$addToSet": "$app" }, + "progress": { "$sum": "$progress" }, + "raw_score_max": { "$sum": "$raw_score_max" }, + "raw_score_avg": { "$avg": "$raw_score_avg" }, + "max": { "$sum": "$max" } + } +}, { + "$project": { + "id": "$_id", + "items": "$items", + "progress_max": "$progress", + "raw_score_max": "$raw_score_max", + "raw_score_avg": "$raw_score_avg", + "max": "$max", + "_id": false + } +} ]')) + + res <- dplyr::full_join(part1, part2) + # Eliminate entries where count_all is zero + res <- dplyr::filter(res, .data$count_all > 0) + res +} + +# Get progression in H5P exercises for one student +h5p_user_prog <- function(user_login, class_aa, + url = getOption("learnitr.lrs_url")) { + mdb_h5p <- try(mongolite::mongo("h5p", url = url), silent = TRUE) + if (inherits(mdb_h5p, "try-error")) + stop("Error: impossible to connect to the H5P database") + + if (!mdb_h5p$count(paste0('{ "login": "', user_login, '", + "app": ', class_aa, ' }'))) + return(NULL) + + part1 <- mdb_h5p$aggregate(paste0('[ { + "$match": { + "login": "', user_login, '", + "app": ', class_aa, ' + } +}, { + "$group": { + "_id": "$id", + "count": { "$sum": { + "$cond": [ { "$in": ["$verb", ["attempted"]] }, 0, 1 ] + } } + } +}, { + "$project": { + "id": "$_id", + "count": "$count", + "_id": false + } +} ]')) + + if (!mdb_h5p$count(paste0('{ "login": "', user_login, '", + "app": ', class_aa, ', "verb": "answered", "max": { "$gt": 0 } }'))) { + # Fake data because the student did not answered to anything yet + n <- nrow(part1) + part2 <- data.frame(id = part1$id, items_done = rep("", n), + progress = rep(0, n), raw_score = rep(0, n)) + } else { + part2 <- mdb_h5p$aggregate(paste0('[ { + "$match": { + "login": "', user_login, '", + "app": ', class_aa, ', + "verb": "answered", + "max": { "$gt": 0 } + } +}, { + "$group": { + "_id": { "id": "$id", "app": "$app" }, + "progress": { "$max": "$max" }, + "raw_score": { "$max": "$score" } + } +}, { + "$project": { + "id": "$_id.id", + "app": "$_id.app", + "progress": "$progress", + "raw_score": "$raw_score", + "_id": false + } +}, { + "$group": { + "_id": "$id", + "items_done": { "$addToSet": "$app" }, + "progress": { "$sum": "$progress" }, + "raw_score": { "$sum": "$raw_score" } + } +}, { + "$project": { + "id": "$_id", + "items_done": "$items_done", + "progress": "$progress", + "raw_score": { "$ifNull": [ "$raw_score", 0] }, + "_id": false + } +} ]')) + } + + res <- dplyr::full_join(part1, part2) + # Eliminate entries where count is zero + res <- dplyr::filter(res, .data$count > 0) + res +} + +# Get Shiny apps data for a whole class +shiny_class_prog <- function(class_logins, class_aa, + url = getOption("learnitr.lrs_url")) { + mdb_shiny <- try(mongolite::mongo("shiny", url = url), silent = TRUE) + if (inherits(mdb_shiny, "try-error")) + stop("Error: impossible to connect to the shiny database") + + # Not used for now, but kept for future use... + #"progress_avg": { "$avg": { + # "$cond": [ { "$in": ["$verb", ["evaluated"] ] }, "$max", null ] + #} }, + + if (!mdb_shiny$count(paste0('{ "login": ', class_logins, ', + "app": ', class_aa, ' }'))) + return(NULL) + + mdb_shiny$aggregate(paste0('[ { + "$match": { + "login": ', class_logins, ', + "app": ', class_aa, ' + } +}, { + "$group": { + "_id": "$app", + "count": { "$sum": 1 }, + "raw_score_max": { "$max": "$score" }, + "raw_score_avg": { "$avg": "$score" }, + "max": { "$max": "$max" } + } +}, { + "$project": { + "app": "$_id", + "count_all": "$count", + "count_avg": { "$divide": ["$count", ', attr(class_logins, "n"), '] }, + "progress_max": "$max", + "raw_score_max": "$raw_score_max", + "raw_score_avg": "$raw_score_avg", + "max": "$max", + "_id": false + } +} ]')) +} + +# Get the progression in Shiny apps for one student +shiny_user_prog <- function(user_login, class_aa, + url = getOption("learnitr.lrs_url")) { + mdb_shiny <- try(mongolite::mongo("shiny", url = url), silent = TRUE) + if (inherits(mdb_shiny, "try-error")) + stop("Error: impossible to connect to the shiny database") + + if (!mdb_shiny$count(paste0('{ "login": "', user_login, '", + "app": ', class_aa, ' }'))) + return(NULL) + + mdb_shiny$aggregate(paste0('[ { + "$match": { + "login": "', user_login, '", + "app": ', class_aa, ' + } +}, { + "$group": { + "_id": "$app", + "count": { "$sum": 1 }, + "progress": { "$max": "$max" }, + "raw_score": { "$max": "$score" } + } +}, { + "$project": { + "app": "$_id", + "id": "", + "items": "$_id", + "items_done": { + "$cond": [ { "$gt": ["$progress", 0] }, ["$_id"], [] ] + }, + "count": "$count", + "progress": "$progress", + "raw_score": { "$ifNull": [ "$raw_score", 0] }, + "_id": false + } +} ]')) +} diff --git a/R/report_progress.R b/R/report_progress.R new file mode 100644 index 0000000..617cb1c --- /dev/null +++ b/R/report_progress.R @@ -0,0 +1,90 @@ +#' Show the progress report for one student +#' +#' @param email The email address of the student +#' @param second.course Should we display data for a secondary course that the +#' student follows? +#' @param course The primary course +#' @param module Should we restrict data for one module, (or all with `NULL`)? +#' @param browse Should we browse to the provided URL? +#' @param url The url to connect to the MongoDB database +#' @param port The port where the Shiny application is serving. If 0, a version +#' on a server (for instance, on Posit Connect) is used. +#' +#' @return The URL to the progress report +#' @export +#' +#' @examples +#' \dontrun{ +#' # In a different R session, run the following line: +#' learnitprogress::run_progress_report() +#' # Then... +#' options(learnitr.lrs_url = "mongodb://127.0.0.1/sdd") +#' learnitprogress::report_progress("student.email@school.edu") +#' } +report_progress <- function(email, second.course = FALSE, course = NULL, + module = NULL, browse = TRUE, url = getOption("learnitr.lrs_url"), + port = 3260) { + if (is.null(port)) + stop("The Shiny application progress report does not seem to be running") + # Get info about that user + # This is an alternate way to get user data that we use in SDD + #users <- data.io::read(svMisc::pcloud_crypto("sdd_users", "all_users.csv")) + #users <- drop_na(users, iemail) + # view_email <- tolower(email) + #user <- dplyr::filter(users, iemail == view_email) + mdb <- mongolite::mongo("users", url = url) + user <- mdb$find(paste0('{"iemail" : { "$eq" : "', tolower(email), '" } }')) + if (!NROW(user)) + stop("User not found!") + + # In user iemail, I have a lowercase version, but I need - + # email instead + user$iemail <- user$email + encode_param <- function(param, data = user) { + value <- URLencode(as.character(data[[param]]), reserved = TRUE) + paste0(param, "=", value) + } + params <- c("iemail", "iid", "ifirstname", "ilastname", "institution", + "icourse", "ictitle", "iurl", "iref") + # Possibly use first or second course from icflag + # Note: we don't update ictitle here => may be wrong! + if (!missing(second.course)) { + courses <- trimws(strsplit(user$icflag, ",", fixed = TRUE)[[1]]) + if (length(courses) > 1) { + if (isTRUE(second.course)) { + user[["icourse"]] <- courses[2] + } else {# First course + user[["icourse"]] <- courses[1] + } + } + } + search <- character(0) + for (param in params) + search <- c(search, encode_param(param)) + + # If course and module are specified, append them to the search string too + if (!is.null(course)) + search <- c(search, paste0("course=", course)) + if (!is.null(module)) + search <- c(search, paste0("module=", module)) + + if (port == 0) { + # We use the server version + # TODO: allow customizing this url + url <- paste0("https://sdd.umons.ac.be/sdd-progress-report/?", + paste(search, collapse = "&")) + } else { + # We use the local development version + url <- paste0("http://127.0.0.1:", port, "?", paste(search, collapse = "&")) + } + + if (isTRUE(browse)) + browseURL(url) + invisible(url) +} +#' @export +#' @rdname report_progress +run_progress_report <- function(port = 3260) { + shiny::runApp(system.file("shiny", "progress-report", + package = "learnitprogress"), port = port, launch.browser = FALSE) +} diff --git a/R/url.R b/R/url.R new file mode 100644 index 0000000..893a6c5 --- /dev/null +++ b/R/url.R @@ -0,0 +1,106 @@ +# Check and getURL parameters --------------------------------------------- + +#' Check student profile to identify it from URL's parameters +#' +#' @param profile Profile data gathered from the LRS +#' @param url_profile Profile data gathered from the URL +#' +#' @return `TRUE` if the profile is correct, `FALSE` otherwise. +#' @export +check_profile <- function(profile, url_profile) { + # In case of a wrong profile, return FALSE with a comment indicating what is + # wrong (stop at first error and don't check further) + incorrect <- function(comment) + structure(FALSE, comment = comment) + + # Github login not being in the URL params, it is not tested + # also, course and url are not tested as they may vary (a student may follow + # several courses, and the Moddle URL could change at any time) + if (tolower(profile$iemail) != tolower(as.character(url_profile['iemail']))) + return(incorrect("'iemail' incorrect")) + if (profile$iid != as.character(url_profile['iid'])) + return(incorrect("'iid' incorrect")) + if (tolower(profile$ifirstname) != + tolower(as.character(url_profile['ifirstname']))) + return(incorrect("'fist name' incorrect")) + if (tolower(profile$ilastname) != + tolower(as.character(url_profile['ilastname']))) + return(incorrect("'last name' incorrect")) + # Do not check institution because it is empty for a few students + #if (profile$institution != as.character(url_profile['institution'])) + # return(incorrect("'institution' incorrect")) + # Note: we don't check icourse and ictitle here, because a student may be + # registered to several courses => it may change from the stored profile + # For iref (Moodle user's UUID), this item is not accessible outside of Moodle + # (and our LRS once the user is registered). However, it makes problem for us, + # because we cannot check the report before a user registers. The magic value + # "teacher-special-access" for iref allows to by-pass that item (keep it?) + # It can be used both in the profile file and the URL for debugging purposes + #if (profile$iref != "teacher-special-access" && + # as.character(url_profile['iref']) != "teacher-special-access" && + # profile$iref != as.character(url_profile['iref'])) + # return(incorrect("'iref' incorrect")) + + # All test corrects => OK + TRUE +} + +#' URL parameters used to identify a student +#' +#' # Get the list of required parameters to identify a student from the URL. +#' +#' @return A character vector with the list of parameters +#' @details +#' In Moodle, the URL variables must be set as follows: +#' - iemail = Student's email +#' - iid = Student' ID ("Nom d'utilisateur" in French in Moodle) +#' - ifirstname = First name of the student +#' - ilastname = Last name of the student +#' - institution = Institution +#' - icourse = Identifier for the course +#' - ictitle = Title of the course +#' - iurl = URL of the Moodle server +#' - iref = Moodle ID +#' +#' @export +#' @rdname check_profile +#' +#' @examples +#' query_params_list() +query_params_list <- function() { + c("iemail", "iid", "ifirstname", "ilastname", + "institution", "icourse", "ictitle", "iurl", "iref") +} + +#' Check if all required parameters are in the query URL +#' +#' @param query The query part of the URL (text after ?, like in +#' https://mysite.org?params1=1¶m2=2). The query is a character string with +#' each value, named with the parameters names. +#' +#' @return `TRUE` if all required parameters are in the query, `FALSE` otherwise. +#' @export +#' @rdname check_profile +#' +#' @examples +#' is_correct_query(c(iemail = "student@uni.edu", iid = "1234", +#' ifirstname = "John", ilastname = "Doe", institution = "University", +#' icourse = "123", ictitle = "Course title", iurl = "https://moodle.uni.edu", +#' iref = "2FAB3EE8-42B5-4B61-DA23-2167FACD7B569")) +#' is_correct_query(c(iemail = "student@uni.edu")) +is_correct_query <- function(query) { + all(query_params_list() %in% names(query)) +} + +# +#' Reformat the query (eliminate possible other parameters) to get user profile +#' +#' @return A list with the required parameters +#' @export +#' @rdname check_profile +#' +#' @examples +#' profile_from_query(c(iemail = "student@uni.edu", iid = "1234", other = "a")) +profile_from_query <- function(query) { + query[query_params_list()] +} diff --git a/README.md b/README.md index 4b594b4..1eb676b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,58 @@ -# learnitprogress -Shiny app showing the progression in the exercises to students +# learnitprogress - Report Student Progress in LearnIt::R Courses + + + +[![R-CMD-check](https://github.com/learnitr/learnitprogress/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/learnitr/learnitprogress/actions/workflows/R-CMD-check.yaml) +[![Codecov test coverage](https://img.shields.io/codecov/c/github/learnitr/learnitprogress/main.svg)](https://codecov.io/github/learnitr/learnitprogress?branch=main) +[![CRAN Status](https://www.r-pkg.org/badges/version/learnitprogress)](https://cran.r-project.org/package=learnitprogress) +[![r-universe status](https://learnitr.r-universe.dev/badges/learnitprogress)](https://learnitr.r-universe.dev/learnitprogress) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![Lifecycle stable](https://img.shields.io/badge/lifecycle-stable-brightgreen.svg)](https://lifecycle.r-lib.org/articles/stages.html#stable) + + + +A Shiny app that connects to you LRS ("Learning Record Store" database, as created and managed by the 'learnitdown' package) and displays a report showing the completion of exercices and projects. This report is intended for students to track their progress in the course. + +## Installation + +(Not yet! The latest stable version of {learnitprogress} can simply be installed from [CRAN](http://cran.r-project.org):) + +``` r +install.packages("learnitprogress") +``` + +You can also install the latest development version. Make sure you have the {remotes} R package installed: + +``` r +install.packages("remotes") +``` + +Use `install_github()` to install the {learnitprogress} package from GitHub (source from **main** branch will be recompiled on your machine): + +``` r +remotes::install_github("SciViews/learnitprogress") +``` + +R should install all required dependencies automatically, and then it should compile and install {learnitprogress}. + +## Further explore {learnitprogress} + +You can get further help about this package this way. Make the {learnitprogress} package available in your R session: + +``` r +library("learnitprogress") +``` + +Get help about this package: + +``` r +library(help = "learnitprogress") +help("learnitprogress-package") +vignette("learnitprogress") # None is installed with install_github() +``` + +For further instructions, please, refer to these help pages at . + +## Code of Conduct + +Please note that the {learnitprogress} project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By contributing to this project, you agree to abide by its terms. diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..47297e7 --- /dev/null +++ b/TODO.md @@ -0,0 +1,7 @@ +# learnitprogress - To do list + +- A fake LRS as example. + +- Adapt the code to work also outside of SDD + +- English messages + translations (French...) diff --git a/_pkgdown.yml b/_pkgdown.yml new file mode 100644 index 0000000..4b28cb6 --- /dev/null +++ b/_pkgdown.yml @@ -0,0 +1,21 @@ +url: https://learnitr.github.io/learnitprogress + +destination: docs + +development: + mode: auto + +template: + params: + bootswatch: spacelab + mathjax: true + +toc: + depth: 3 + +home: + strip_header: true + +authors: + Philippe Grosjean: + href: https://phgrosjean.sciviews.org diff --git a/inst/CITATION b/inst/CITATION new file mode 100644 index 0000000..8035481 --- /dev/null +++ b/inst/CITATION @@ -0,0 +1,24 @@ +citHeader("To cite learnitr packages in publications use:") + +citEntry( + entry = "Manual", + title = "LearnIt::R", + author = personList(as.person("Philippe Grosjean"), + as.person("Guyliann Engels")), + organization = "UMONS", + address = "MONS, Belgium", + year = version$year, + url = "https://learnitr.r-universe.dev/", + + textVersion = + paste("Grosjean, Ph. & Engels, G. (", version$year, "). ", + "LearnIt::R. ", + "UMONS, Mons, Belgium. ", + "URL https://learnitr.r-universe.dev/.", + sep = "") +) + +citFooter("We have invested a lot of time and effort in creating LearnIt::R,", + "please cite it when using it together with R.", + "See also", sQuote("citation()"), + "for citing R.") diff --git a/inst/WORDLIST b/inst/WORDLIST new file mode 100644 index 0000000..fbd5364 --- /dev/null +++ b/inst/WORDLIST @@ -0,0 +1,28 @@ +CMD +Codecov +JSON +d'utilisateur +exercices +ggplot +https +icourse +ictitle +iemail +ifirstname +ilastname +iref +iurl +LearnIt +learnitdown +Learnr +learnr +learnrs +Lifecycle +LRS +Moodle +mysite +Nom +ORCID +param +params +URL's diff --git a/inst/shiny/progress-report/app.R b/inst/shiny/progress-report/app.R new file mode 100644 index 0000000..55b634a --- /dev/null +++ b/inst/shiny/progress-report/app.R @@ -0,0 +1,462 @@ +# Individual student's progress report for BioDataScience-Course at UMONS +# Version 3.0.0, Copyright (c), 2021-2024, Philippe Grosjean & Guyliann Engels +# +# TODO: +# - Rework to allow using outside of SDD context +# - Specify academic year +# - Check messages in the report +# - Better plots... may be using plotly for interactivity! + use median and max +# for learnr plots +# - Help pages /progress-login and /progress-report to implement + +# Need shinyFeedback v 0.3.0 from GitHub in svbox2020... OK for svbox2021 +#remotes::install_github("merlinoa/shinyFeedback@v0.3.0") + +#date_query_def <- '"date": { "$gt": "2022-09-18 00:00:00.000000" }, ' + +library(shiny) +#library(shinyjs) +library(shinybusy) +library(shinythemes) +#library(shinyFeedback) +library(RCurl) +library(mongolite) +#library(PKI) +library(dplyr) +library(ggplot2) +library(cowplot) +library(fs) +#library(svFlow) +library(learnitprogress) + + +# Connect to the course LRS (MongoDB database) ---------------------------- + +# TODO: make independent from SDD +options(learnitr.lrs_url = getOption("learnitr.lrs_url", + default = Sys.getenv("LEARNITR_LRS_URL", + unset = "mongodb://127.0.0.1/sdd"))) + +mdb_h5p <- mongolite::mongo('h5p', url = getOption("learnitr.lrs_url")) +mdb_h5p$disconnect() +mdb_learnr <- mongolite::mongo('learnr', url = getOption("learnitr.lrs_url")) +mdb_learnr$disconnect() +mdb_shiny <- mongolite::mongo('shiny', url = getOption("learnitr.lrs_url")) +mdb_shiny$disconnect() + + +# The Shiny app ----------------------------------------------------------- + +ui <- fluidPage(theme = shinytheme("lumen"), + titlePanel(textOutput("title"), windowTitle = "Progression"), + + # State #1: message when the app is launched with incorrect/incomplete URL + conditionalPanel('output.wrongURL !== null & output.wrongURL != ""', + span(tags$b(textOutput("wrongURL")), style = "color:red") + ), + +# # State #2: ask for login and user recording if not registered yet +# conditionalPanel('output.loginMessage !== null & output.loginMessage != ""', +# tags$b(textOutput("loginMessage")), +# shinyjs::useShinyjs(), +# shinyFeedback::useShinyFeedback(), +# textInput("login", "Login Github :", ""), +# actionButton("register", "Enregistrement", icon = icon("key")), +# helpText(htmlOutput("registerMessage")), +# tags$a("Page d'aide", +# href = "https://wp.sciviews.org/progress-login", target = "_blank") +# ), + + # State #3: display the progress report for the registered user + conditionalPanel('output.wrongURL == ""', # & output.loginMessage == ""', + + tabsetPanel( + tabPanel("Progression", + h4("Progression des H5Ps, Shiny apps & Learnrs"), + htmlOutput("reportMessage"), # General report message + htmlOutput("learnrMessage"), # Message specific for the learnrs + plotOutput("learnrPlot", height = 600), + + #h4("Progression sur Github"), + #htmlOutput("githubMessage"), # Message specific for Github activity + #plotOutput("githubPlot"), + tags$a("Page d'aide", + href = "https://wp.sciviews.org/progress-report") + ), + tabPanel("Projets GitHub", + h4("Grilles de correction des projets GitHub"), + htmlOutput("gridMessage"), # Message specific for the correction grids + selectInput("gridSelect", " ", c()), + dataTableOutput("gridTable"), + + tags$a("Page d'aide", + href = "https://wp.sciviews.org/progress-report") + ), + tabPanel("Note", + h4("D\u00e9tail de la note"), + htmlOutput("gradeMessage"), # Message specific for the grade plot + plotOutput("gradePlot", height = 600), + + tags$a("Page d'aide", + href = "https://wp.sciviews.org/progress-report") + ) + ) + ) +) + +server <- function(input, output, session) { + # User login, email, ... globally used in the app + user <- reactiveValues(data = NULL, learnr = NULL, github = NULL, + grade = NULL, grid = NULL, course = NULL, module = NULL, login = " ", + email = " ", firstname = " ", lastname = " ", message = "", + learnr_message = "", github_message = "", + grade_message = + "La note finale pour cette AA n'est pas encore attribu\u00e9e.", + grid_message = + "Pas de grille de correction de projet GitHub actuellement disponible.", + done = FALSE) + + # Modal busy box with spinner + shinybusy::show_modal_spinner(spin = "radar") + + # During calculations, login inputbox & register button are visible + # This was the only solution I found for now to avoid it +# shinyjs::hide("register") +# shinyjs::hide("login") + + # Get parameters transmitted by Moodle through the URL + observe({ + # Avoid displaying wrongURL and login conditional panels for now + query <- parseQueryString(session$clientData$url_search) + if (!is_correct_query(query)) {# Wrong URL + # Place (and lock down) the application in state #1 - nothing more to do + user$email <- "" # This way, conditionalPanel 'wrongURL' is activated + showNotification(type = "error", duration = 10, + "Le rapport de progression n'est disponible que depuis le cours (si on est correctement enregistr\u00e9) ou depuis Moodle.") + + } else if (!isTRUE(isolate(user$done))) {# Correct URL, process only once + user$course <- query$course + user$module <- query$module + user$data <- user_data <- profile_from_query(query) + user$firstname <- user_data['ifirstname'] + user$lastname <- user_data['ilastname'] + user_profile <- get_profile(user_data['iemail']) + # If there is an error, print it at the console + if (!is.null(comment(user_profile))) + cat("User profile error:", comment(user_profile), "\n") + + # if (!length(user_profile)) {# If the profile is empty + # # Switch in state #2: ask for user registration + # user$email <- user_data['iemail'] + # user$login <- "" + # shinyjs::show("login") + # shinyjs::show("register") + # + # } else {# Profile exists + # Check provided data match + res <- check_profile(user_profile, user_data) + if (!res) { + # Indicate what was wrong at the console + cat("Profile verification:", comment(res), "\n") + # Switch the app in state #1 (lock down) + user$email <- "" # This way, conditionalPanel 'wrongURL' is activated + showNotification(type = "error", duration = 10, + "Profil utilisateur incorrect, contactez vos enseignants.") + + } else {# Our app will be in state #3, create the progress report now + user$login <- user_profile$login + user$email <- user_profile$email + + # Get data for the learnrs and the Github activity + if (is.null(user$course)) { + course_message <- paste0("de ", user$data["ictitle"], " (", + user$data["icourse"], ")") + } else { + course_message <- paste(user$course) + } + if (is.null(user$module)) { + module_message <- "" + } else { + module_message <- paste(", module", user$module) + } + user$message <- paste0("Votre progression pour le cours ", + course_message, module_message, + ". Gris = progression de la classe, bleu = exercices r\u00e9ussis, rouge = r\u00e9ponses incorrectes. Ceci ne correspond pas \u00e0 la note finale mais seulement \u00e0 l'\u00e9tat d'avancement. Le rapport s'actualise toutes les 5 minutes (donc il se peut que vos derni\u00e8res actions ne soient pas encore visibles).") + + # Get learnr data + #user_learnr <- get_learnr_data(user$email) + if (is.null(user$course)) { + icourse <- user$data["icourse"] + } else { + icourse <- user$course + } + user_learnr <- get_all_data(user$data["iemail"], icourse, user$module) + if (!is.null(comment(user_learnr))) { + cat("Error getting Learnr, Shiny & H5P data:", comment(user_learnr), + "\n") + user$message <- paste0(user$message, + " Pas de donn\u00e9es Learnr, Shiny, H5P", + " ou donn\u00e9es erron\u00e9es.") + } else { + # TODO: get latest data from the database now! + user$learnr <- user_learnr + user$learnr_message <- user_learnr$comment + # Uncomment for debugging purposes + #user$learnr_message <- paste0("Nlines learnr data:", + # nrow(user_learnr$data)) + # Also get general message + if (!is.null(user_learnr$message)) + user$message <- paste0(user$message, "\n
\n", + user_learnr$message) + } + + # Get Github data + user_github <- get_github_data(user$email) + if (!is.null(comment(user_github))) { + cat("Error getting Github data:", comment(user_github), "\n") + user$message <- paste0(user$message, + " Pas de donn\u00e9es Github,", + " ou donn\u00e9es erron\u00e9es.") + } else { + user$github <- user_github + user$github_message <- user_github$comment + # Uncomment for debugging purposes + #user$github_message <- paste0("Nlines Github data:", + # nrow(user_github$data), + # " - Nlines trend: ", nrow(user_github$trend)) + } + + # Get grade data + user_grade <- get_grade_data(user$data["iemail"], icourse) + if (!is.null(user_grade)) { + user$grade <- user_grade + } + + # Get grid data + user_grid <- get_grid_data(user$data["iemail"], icourse, user$module) + if (!is.null(user_grid)) { + user$grid <- user_grid + updateSelectInput(session, "gridSelect", choices = user_grid, + selected = user_grid[1]) + } + } + #} + } + + # Avoid running these long calculations twice and close the busy modal box + user$done <- TRUE + shinybusy::remove_modal_spinner() + }) + + # App title, with user firstname and lastname if possible + output$title <- renderText({ + #paste("Progression", user$firstname, user$lastname) + # Don't put "Progression in the title, cf. already above in Moodle + paste(user$firstname, user$lastname) + }) + + # State #1: wrong URL parameters. Display only this message + output$wrongURL <- renderText({ + if (!is.null(user$email) && user$email == "") { + "Impossible d'afficher votre progression: vous devez lancer cette application depuis le cours et y être enregistré comme \u00e9tudiant ou depuis votre compte Moodle institutionnel. Si c'est le cas, voyez vos enseignants pour qu'ils corrigent le bug !" + } else "" + }) + +# # State #2: ask for Github login to register this user, no report yet +# output$loginMessage <- renderText({ +# if (!is.null(user$login) && user$login == "" && +# !is.null(user$email) && user$email != "") { +# "Votre rapport de progression n'est pas accessible car votre compte n'est pas encore valid\u00e9 ou est incorrect. Entrez votre login GitHub une fois votre compte GitHub cr\u00e9\u00e9, et cliquez sur 'Enregistrement'." +# } else "" +# }) +# +# observeEvent(input$login, +# shinyFeedback::feedbackWarning("login", input$login == "", +# "Complétez votre login GitHub") +# ) +# +# response <- eventReactive(input$register, { +# cat("registering!\n") +# if (input$login == "") { +# +# # The message to display +# "Vous devez entrer votre login GitHub pour pouvoir vous enregistrer !" +# +# } else if (!is_github_user(input$login)) {# Non-existing Github account +# shinyFeedback::feedbackDanger("login", TRUE, +# "Vérifiez votre login GitHub") +# +# # The message to display +# "Login GitHub incorrect (cr\u00e9er d'abord un compte GitHub)." +# +# } else {# Correct Github account +# shinyFeedback::feedbackSuccess("login", TRUE) +# +# # Create a record in the sdd/logins database table +# res <- record_sdd_login(input$login, user$data, +# role = "student", comment = "") +# if (res == "") {# OK, registering request recorded +# # Inactivate the register button + login +# shinyjs::disable("register") +# shinyjs::disable("login") +# #shinyjs::hide("register") +# #shinyjs::hide("login") +# +# # The message to display +# "Demande d'enregistrement effectu\u00e9e. Vous recevrez un mail quand il sera effectif." +# } else {# Error registering the user in the database +# +# # The message to display +# paste0("Login GitHub correct, mais probl\u00e8me lors de l'enregistrement : ", res, +# ". V\u00e9rifiez votre connexion Internet et r\u00e9essayez (\u00e9ventuellement plus tard).") +# } +# } +# }) +# +# output$registerMessage <- renderText({response()}) + + # State #3: display progress report + output$reportMessage <- renderText({ + if (!is.null(user$message)) { + user$message + } else "" + }) + + output$learnrMessage <- renderText({ + if (!is.null(user$learnr_message)) { + user$learnr_message + } else "" + }) + + output$learnrPlot <- renderPlot({ + if (!is.null(user$learnr)) { + #user_data <- user$learnr$data + user_data <- user$learnr + # user_data$`Mediane classe` <- user_data$median / user_data$max * 100 + # user_data$`Meilleur` <- user_data$best / user_data$max * 100 + # user_data$`Votre score` <- user_data$result / user_data$max * 100 + # user_data$tutorial <- factor(user_data$tutorial, + # levels = sort(unique(user_data$tutorial), decreasing = TRUE)) + # # TODO: ajouter les infos relatives à la médiane de la classe et au meilleur + # res <- try(ggplot(user_data, aes(x = tutorial, y = `Votre score`, fill = score)) + + # #geom_col(aes(x = tutorial, y = `Mediane classe`, fill = "#888888")) + + # geom_col() + + # scale_fill_gradient(low = "firebrick", high = "lightblue") + + # coord_flip() + + # ylab("Pourcentage du tutoriel fait") + + # xlab("Tutoriel") + + # cowplot::theme_cowplot(font_size = 14)) + #res <- try(ggplot(user_data, aes(x = date, y = cum_max)) + + # geom_area() + + # xlab("Date") + + # ylab(("Nbre d'exercices")) + + # geom_area(aes(x = date, y = cum_grade), fill = "blue") + + # cowplot::theme_cowplot(font_size = 14) + res <- try(plot_progression(user$login, user_data)) + if (inherits(res, "try-error")) { + user$learnr_message <- as.character(res) + } else { + res + } + } + }) + + output$githubMessage <- renderText({ + if (!is.null(user$github_message)) { + user$github_message + } else "" + }) + + output$githubPlot <- renderPlot({ + if (!is.null(user$github)) { + user_data <- user$github$data + trend <- user$github$trend + res <- try(ggplot(user_data, aes(x = day, y = cusum)) + + geom_smooth(data = trend, aes(x = day, y = trend), + method = "loess", formula = y ~ x, + color = "#2e9ce6", alpha = 0.4, se = FALSE, span = 0.3) + + geom_line() + + geom_point() + + geom_vline(xintercept = as.Date("2020-03-15"), color = "red") + + geom_vline(xintercept = as.Date("2020-03-26"), color = "green") + + labs(y = "Somme des commits", x = "Temps", + #title = paste("Activité de", student) , + caption = "Somme des commits au cours du temps en noir. La ligne bleue repr\u00e9sente l'activit\u00e9 moyenne de la classe. + Le trait vertical rouge repr\u00e9sente le d\u00e9but du confinement et le trait vertical vert marque la fin du dernier cours.") + + cowplot::theme_cowplot(font_size = 14)) + if (inherits(res, "try-error")) { + user$github_message <- as.character(res) + } else { + res + } + } + }) + + output$gradeMessage <- renderText({ + if (!is.null(user$grade_message)) { + user$grade_message + } else "" + }) + + output$gradePlot <- renderPlot({ + if (!is.null(user$grade)) { + user_grade <- user$grade + + res <- try(plot_grade(user_grade, show.name = FALSE)) + if (inherits(res, "try-error")) { + user$grade_message <- as.character(res) + } else { + if (is.null(user$course)) { + course_message <- paste0("de ", user$data["ictitle"], " (", + user$data["icourse"], ")") + } else { + course_message <- paste(user$course) + } + user$grade_message <- paste0("Notes obtenues par type d'exercice pour le cours ", + course_message, + ". Les notes peuvent \u00eatre inf\u00e9rieures \u00e0 la progression car elles tiennent compte des tentatives pour les H5P, des \u00e9l\u00e9ments corrects pour les applications Shiny et de la visualisation des aides pour les learnrs.") + res + } + } + }) + + output$gridMessage <- renderText({ + if (!is.null(user$grid_message)) { + user$grid_message + } else "" + }) + + output$gridTable <- renderDataTable({ + grid <- input$gridSelect + if (!is.null(grid) && !is.na(grid) && grid != "") { + tab <- try(read.csv(grid)) + if (inherits(tab, "try-error")) { + user$grid_message <- as.character(res) + } else { + # Get data for the learnrs and the Github activity + if (is.null(user$course)) { + course_message <- paste0("de ", user$data["ictitle"], " (", + user$data["icourse"], ")") + } else { + course_message <- paste(user$course) + } + if (is.null(user$module)) { + module_message <- "" + } else { + module_message <- paste(", module", user$module) + } + user$grid_message <- paste0("Evaluation de vos projets GitHub pour le cours ", + course_message, module_message, + ". Le crit\u00e8re indique l'\u00e9l\u00e9ment \u00e9valu\u00e9 (# titre =, @chunk =, !user =, YAML =, ...). Les notes sont cumulatives et certains crit\u00e8res peuvent servir \u00e0 diminuer la note globale ou pour un utilisateur en particulier (points n\u00e9gatifs).") + tab$note <- paste(round(tab$score, 2), tab$max, sep = "/") + tab$comment[is.na(tab$comment)] <- "" + tab <- tab[, c("file", "criterion", "note", "comment")] + names(tab) <- c("Fichier", "Crit\u00e8re", "Note", "Commentaire") + tab + } + } + }, options = list(pageLength = 100)) + +} + +shinyApp(ui = ui, server = server) diff --git a/learnitprogress.Rproj b/learnitprogress.Rproj new file mode 100644 index 0000000..fbbe594 --- /dev/null +++ b/learnitprogress.Rproj @@ -0,0 +1,24 @@ +Version: 1.0 + +RestoreWorkspace: Default +SaveWorkspace: Default +AlwaysSaveHistory: Default + +EnableCodeIndexing: Yes +UseSpacesForTab: Yes +NumSpacesForTab: 2 +Encoding: UTF-8 + +RnwWeave: knitr +LaTeX: XeLaTeX + +AutoAppendNewline: Yes +StripTrailingWhitespace: Yes + +BuildType: Package +PackageUseDevtools: Yes +PackageInstallArgs: --no-multiarch --with-keep.source +PackageCheckArgs: --as-cran +PackageRoxygenize: rd,collate,namespace,vignette + +SpellingDictionary: en_US diff --git a/man/check_profile.Rd b/man/check_profile.Rd new file mode 100644 index 0000000..8f5889c --- /dev/null +++ b/man/check_profile.Rd @@ -0,0 +1,61 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/url.R +\name{check_profile} +\alias{check_profile} +\alias{query_params_list} +\alias{is_correct_query} +\alias{profile_from_query} +\title{Check student profile to identify it from URL's parameters} +\usage{ +check_profile(profile, url_profile) + +query_params_list() + +is_correct_query(query) + +profile_from_query(query) +} +\arguments{ +\item{profile}{Profile data gathered from the LRS} + +\item{url_profile}{Profile data gathered from the URL} + +\item{query}{The query part of the URL (text after ?, like in +https://mysite.org?params1=1¶m2=2). The query is a character string with +each value, named with the parameters names.} +} +\value{ +\code{TRUE} if the profile is correct, \code{FALSE} otherwise. + +A character vector with the list of parameters + +\code{TRUE} if all required parameters are in the query, \code{FALSE} otherwise. + +A list with the required parameters +} +\description{ +# Get the list of required parameters to identify a student from the URL. +} +\details{ +In Moodle, the URL variables must be set as follows: +\itemize{ +\item iemail = Student's email +\item iid = Student' ID ("Nom d'utilisateur" in French in Moodle) +\item ifirstname = First name of the student +\item ilastname = Last name of the student +\item institution = Institution +\item icourse = Identifier for the course +\item ictitle = Title of the course +\item iurl = URL of the Moodle server +\item iref = Moodle ID +} +} +\examples{ +query_params_list() +is_correct_query(c(iemail = "student@uni.edu", iid = "1234", + ifirstname = "John", ilastname = "Doe", institution = "University", + icourse = "123", ictitle = "Course title", iurl = "https://moodle.uni.edu", + iref = "2FAB3EE8-42B5-4B61-DA23-2167FACD7B569")) +is_correct_query(c(iemail = "student@uni.edu")) +profile_from_query(c(iemail = "student@uni.edu", iid = "1234", other = "a")) +} diff --git a/man/class_logins.Rd b/man/class_logins.Rd new file mode 100644 index 0000000..008b6b3 --- /dev/null +++ b/man/class_logins.Rd @@ -0,0 +1,38 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/class.R +\name{class_logins} +\alias{class_logins} +\alias{class_aa} +\alias{user_login} +\title{Get a list of all user logins in a given class} +\usage{ +class_logins(class, url = getOption("learnitr.lrs_url"), as.json = FALSE) + +class_aa( + aa, + url = getOption("learnitr.lrs_url"), + as.json = FALSE, + module = NULL +) + +user_login(email, url = getOption("learnitr.lrs_url")) +} +\arguments{ +\item{class}{The short identifier of the class} + +\item{url}{The URL of the MongoDB database (LRS)} + +\item{as.json}{If \code{TRUE}, return the result as a JSON string} + +\item{aa}{The short identifier of the activity} + +\item{module}{The module to restrict too, or \code{NULL} for all modules} + +\item{email}{The email of the student} +} +\value{ +A character vector with the results +} +\description{ +Get a list of all user logins in a given class +} diff --git a/man/get_data.Rd b/man/get_data.Rd new file mode 100644 index 0000000..558567c --- /dev/null +++ b/man/get_data.Rd @@ -0,0 +1,62 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lrs.R +\name{get_data} +\alias{get_data} +\alias{get_profile} +\alias{get_all_data} +\alias{get_github_data} +\alias{get_grid_data} +\alias{get_grade_data} +\alias{get_grade_dir} +\title{Get data from exercises or projects for one student} +\usage{ +get_data( + collection, + query, + fields = "{}", + url = getOption("learnitr.lrs_url"), + count = FALSE +) + +get_profile(email, login = NULL) + +get_all_data(email, icourse, module = NULL) + +get_github_data(email) + +get_grid_data(email, icourse, module = NULL) + +get_grade_data(email, icourse) + +get_grade_dir(root_dir = "/data1/grades", acad_year = NULL) +} +\arguments{ +\item{collection}{The collection in the MongoDB database} + +\item{query}{The JSON query on the MongoDB database} + +\item{fields}{The fields to return} + +\item{url}{The url to access the MongoDB database} + +\item{count}{Do we count items instead of getting them?} + +\item{email}{The email of the student} + +\item{login}{The GitHub login of the student as alternate way to get its +profile (when it is not \code{NULL})} + +\item{icourse}{The course identifier} + +\item{module}{The module, or \code{NULL} for exercises in all modules} + +\item{root_dir}{The root directory where the grades are stored.} + +\item{acad_year}{The academic year to use, or \code{NULL} to use the latest one.} +} +\value{ +A data frame or a list with the data +} +\description{ +Get data from exercises or projects for one student +} diff --git a/man/learnitprogress-package.Rd b/man/learnitprogress-package.Rd new file mode 100644 index 0000000..5ab781a --- /dev/null +++ b/man/learnitprogress-package.Rd @@ -0,0 +1,32 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/learnitprogress-package.R +\docType{package} +\name{learnitprogress-package} +\alias{learnitprogress} +\alias{learnitprogress-package} +\title{learnitprogress: Report Student Progress in 'LearnIt::R' Courses} +\description{ +A Shiny app that connects to you LRS ("Learning Record Store" database, as created and managed by the 'learnitdown' package) and displays a report showing the completion of exercices and projects. This report is intended for students to track their progress in the course. +} +\details{ +The learnitprogress package implements a Shiny application that summarizes +the progression of each of your course's students in their LearnIt::R +exercises and projects. It queries the MongoDB database that is collecting +your LearnIt::R activities (H5P, Shiny, learnr, GitHub projects...). + +See ... +} +\seealso{ +Useful links: +\itemize{ + \item \url{https://github.com/learnitr/learnitprogress} + \item \url{https://learnitr.github.io/learnitprogress/} + \item Report bugs at \url{https://github.com/learnitr/learnitprogress/issues} +} + +} +\author{ +\strong{Maintainer}: Philippe Grosjean \email{phgrosjean@sciviews.org} (\href{https://orcid.org/0000-0002-2694-9471}{ORCID}) + +} +\keyword{internal} diff --git a/man/learnr_prog.Rd b/man/learnr_prog.Rd new file mode 100644 index 0000000..f6e27db --- /dev/null +++ b/man/learnr_prog.Rd @@ -0,0 +1,49 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/progression.R +\name{learnr_prog} +\alias{learnr_prog} +\alias{h5p_prog} +\alias{shiny_prog} +\title{Progression in learnrs, H5P and Shiny apps} +\usage{ +learnr_prog( + user_login, + class_logins, + class_aa, + class_data = NULL, + url = getOption("learnitr.lrs_url") +) + +h5p_prog( + user_login, + class_logins, + class_aa, + class_data = NULL, + url = getOption("learnitr.lrs_url") +) + +shiny_prog( + user_login, + class_logins, + class_aa, + class_data = NULL, + url = getOption("learnitr.lrs_url") +) +} +\arguments{ +\item{user_login}{The GitHub login of one student} + +\item{class_logins}{All the logins for the class} + +\item{class_aa}{The AA identifier for this class} + +\item{class_data}{The data for this class} + +\item{url}{The url of the MongoDB data base} +} +\value{ +A data frame with all the progression data for the user and the class +} +\description{ +Progression in learnrs, H5P and Shiny apps +} diff --git a/man/plot_grade.Rd b/man/plot_grade.Rd new file mode 100644 index 0000000..29c3ef8 --- /dev/null +++ b/man/plot_grade.Rd @@ -0,0 +1,21 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/plot_grade.R +\name{plot_grade} +\alias{plot_grade} +\title{Visualize details about the grade for one student} +\usage{ +plot_grade(data, show.name = TRUE, give.note = TRUE) +} +\arguments{ +\item{data}{Grading data} + +\item{show.name}{Indicate the name of the student in the title} + +\item{give.note}{Indicate the final note in the title} +} +\value{ +A ggplot object +} +\description{ +Visualize details about the grade for one student +} diff --git a/man/plot_progression.Rd b/man/plot_progression.Rd new file mode 100644 index 0000000..3cb6018 --- /dev/null +++ b/man/plot_progression.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/plot_progression.R +\name{plot_progression} +\alias{plot_progression} +\title{Progression plot for one student (exercises H5P, Shiny & Learnr)} +\usage{ +plot_progression(user, data) +} +\arguments{ +\item{user}{The user login} + +\item{data}{The data for this user} +} +\value{ +A ggplot object +} +\description{ +Progression plot for one student (exercises H5P, Shiny & Learnr) +} diff --git a/man/report_progress.Rd b/man/report_progress.Rd new file mode 100644 index 0000000..01a1da5 --- /dev/null +++ b/man/report_progress.Rd @@ -0,0 +1,51 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/report_progress.R +\name{report_progress} +\alias{report_progress} +\alias{run_progress_report} +\title{Show the progress report for one student} +\usage{ +report_progress( + email, + second.course = FALSE, + course = NULL, + module = NULL, + browse = TRUE, + url = getOption("learnitr.lrs_url"), + port = 3260 +) + +run_progress_report(port = 3260) +} +\arguments{ +\item{email}{The email address of the student} + +\item{second.course}{Should we display data for a secondary course that the +student follows?} + +\item{course}{The primary course} + +\item{module}{Should we restrict data for one module, (or all with \code{NULL})?} + +\item{browse}{Should we browse to the provided URL?} + +\item{url}{The url to connect to the MongoDB database} + +\item{port}{The port where the Shiny application is serving. If 0, a version +on a server (for instance, on Posit Connect) is used.} +} +\value{ +The URL to the progress report +} +\description{ +Show the progress report for one student +} +\examples{ +\dontrun{ +# In a different R session, run the following line: +learnitprogress::run_progress_report() +# Then... +options(learnitr.lrs_url = "mongodb://127.0.0.1/sdd") +learnitprogress::report_progress("student.email@school.edu") +} +} diff --git a/pkgdown/favicon/apple-touch-icon-120x120.png b/pkgdown/favicon/apple-touch-icon-120x120.png new file mode 100755 index 0000000000000000000000000000000000000000..a22e98dd9b3f1ad008143374c8a7d44b4f216b7b GIT binary patch literal 13055 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 ct4BuUJBto)UZST57Ce;IsgCw literal 0 HcmV?d00001 diff --git a/pkgdown/favicon/apple-touch-icon-60x60.png b/pkgdown/favicon/apple-touch-icon-60x60.png new file mode 100755 index 0000000000000000000000000000000000000000..c4c9c214cd52e86a3ff76a11a5de13a2b148bacf GIT binary patch literal 5035 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/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 literal 0 HcmV?d00001 diff --git a/pkgdown/favicon/apple-touch-icon.png b/pkgdown/favicon/apple-touch-icon.png new file mode 100755 index 0000000000000000000000000000000000000000..0a80eeb4a040f512bc28f970ce7eaee3b7a17318 GIT binary patch literal 21739 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 ct4BuUJBto)UZST57Ce;IsgCw literal 0 HcmV?d00001 diff --git a/pkgdown/favicon/favicon-16x16.png b/pkgdown/favicon/favicon-16x16.png new file mode 100755 index 0000000000000000000000000000000000000000..015fcb3adc9e89f0effb4f4fabb6c603915933a5 GIT binary patch literal 737 zcmV<70v`Q|P)i1poj58FWQhbW?9;ba!ELWdL_~cP?peYja~^ zaAhuUa%Y?FJQ@H10$xc(K~y-6rISHOlW`cvpZ^RsjdjqP>5z@xyvW!f2_i3p&z;0P z)PfLH=%gW`I9wp^gFKjr9WqcsJdELxc8Y;e-^PLkITh=2l3lT_yBtoIky}`l39h=SeGZ+l!hKGj_rl+U<4Gj&}%FTIQA-J}_zWzEK z4z~{s3@mMLZ(ol@B5x9j#1_e&e-bE);%IJerrB)Hdpw@|01N;$7dcm20|4=O{F}vM zIgZ6*b(u`YnakyVkt|>6TulI=-tBfTb#!#(H5v_NS&r`R?%t|$s@d4s7>P!sog}fh zx7QjB29qRjoT30Qj*pLf>~{Mh08UL!ZKYDF7Lo`A0+VfRZKbufH5WD!%!r^elYPHrC3I!C4#hTUC)t5&{M_gWBoLuSO+Au^(Zp$ z)S;%PrmnoayfxF!z(k7oG#brsg@uK^EXxv)$D^{cvR=2_?TL?%CqAD~rKP2>@7=ri zP#_TO01!Eo0D(H4PPS>&CKmu&v}h4&G@92P9Ubd(FGcf=G&u3kG}6Ke;&`>+S+OaPzzuU z00)3ZB63czqzJ?I9r{ zK8TQz5UQ@O?(FXF&YdK1SVu?4$+osOvtF;iFftQvpHv+-G@@C)a(U)^5jYLIQ_ip>guk{ z%uG*ZW#umbmT|cmhB{K|!Qa zsh*GaXD}Gb*RNmisi>&v-L`F8p5G$$0}%k=v$=EUZdWK2FW0SG=gUNPcDB2(uWvUI zaSR5-k3~gAHm^<~5RgixdOFJ2lDfLOPLW9DxqJ6+J%Ghi0vH97v|z!49dfzc;ti6R zm`Kgd&8JN!)9;549qL=PY8B<=~{Shj2#NhFfLQc_aH0D`@~ zr!*51(Zi&qB-{M?^I7jRhr@yJ@bLXstM$`Ij~<0yxpKvL;J|@?02mq?g4^v5005)W zc;wu0|EjlIy#yh z4hOw@^-3=k3dQyH^<7@QOeSmV@9+Plrl!Ue9v)6Am8vr`GBOJQJ35BHk&J7#T6bA2 zmT%;8`2sGN8@h4hMow{Y@jy#U3r?Ik(Vv={Y9k`on>TN|@87>~*tc)ruSTQMJZH|F z!JwcZxLhs_3=D*+)#}?B85t2*uU_rDb?eq8lgXqaqM`BW^-7>#uRkM~%R9tku`@6* zkO0u(!-t(KR;-|ym>7~urGH+yaN!b$k7ci914tqw$YipHdGqEG0OIj@#Ih{y+O^9i zkx1$QqVn%3_Y!!RrW_hi}f3-B(kcm8+$H;$`|l+khP9smFU07*qoM6N<$ Eg3U25`Tzg` literal 0 HcmV?d00001 diff --git a/pkgdown/favicon/favicon.ico b/pkgdown/favicon/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..3a22db26057848e0991799a17e7ad2701ccf7438 GIT binary patch literal 32038 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 + %\VignetteIndexEntry{learnitprogress - Report Student Progress in 'LearnIt::R' Courses} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>" +) +``` + +```{r setup} +library(learnitprogress) +``` + +TODO...