rr-exams

Create a document with an overview of all available R/exams exercises


I have several .qmd files with R/exams exercises. I can select a subset and include them in a PDF file using exams2pdf() without any problem.

However, I would like to produce a PDF file with all the exercises and with the name of the file at the beginning of each, so I can quickly scan them when preparing exams. To do this, I wrote a simple R script that iterates through all the .qmd files, generates the LaTeX sources via exams2pandoc() (specifying template = "mytemplate.md", following the advice given here R/Exams latex output problem using exams2pandoc()), collects them with the .qmd filename as the section title, and then compiles the resulting latex file with pdflatex.

Everything works except for one remaining issue: if a graph is produced in the R code, the following line, for example, is inserted into the LaTeX file:

\pandocbounded{\includegraphics[keepaspectratio]{media/supplements1/exercise1/ex1-unnamed-chunk-3-1.png}}

However, pdflatex fails because it cannot find the PNG file (and I haven’t been able to determine where it gets saved — perhaps in a temporary folder?).

Do you have any suggestions on how to fix this? Or is there perhaps a better way to achieve what I am trying to do?


Solution

  • Overview

    An "exercise browser" is on the wishlist for R/exams. There are some useful bits an pieces available already but unfortunately no readily available and flexible implementation yet. In any case, I would argue that PDF is the wrong technology for this and using HTML would be much better.

    Quick & dirty

    A small extension of exams2html() has been available as an unexported function exams:::browse_exercise() for some years. It lists the exercise file name in addition to other important metadata before listing one random version of the exercise. To try it out on all .Rmd exercises in the R/exams package use:

    setwd(system.file("exercises", package = "exams"))
    exams:::browse_exercise(Sys.glob("*.Rmd"))
    

    This opens an HTML file listing all the .Rmd exercises. A screenshot of one of the exercises in the list is shown below. The file should be self-contained so that you can also copy it around or give it to a colleague etc.

    Screenshot of one of the exercises in browse_exercise() output

    A better idea

    Instead of using one random draw from the exercises in static HTML, it would be better to get an interacitve HTML page with multiple random draws of the exercises. In principle, this can be achieved with the exams2forms package in combination with a Quarto web page. We have played around with this but haven't got a properly working prototype, yet.

    A first approximation

    However, as a first approximation to this better idea, I have less quick & less dirty (but still quite quick & dirty) function exams2webpool(). The R code for this is included below. This expects a named list of exercise files which can, for example, be structured by chaptes or folders/directories etc. Of course, you may also use just a single list element containing all exercises. For illustration I'm setting up the following list - the resulting screenshot is shown below.

    exrc <- list(
      "Knowledge quiz questions" = c("capitals.Rmd", "flags.Rmd", "swisscapital.Rmd", "switzerland.Rmd"),
      "Mathematics exercises" = c("cholesky.Rmd", "deriv.Rmd", "deriv2.Rmd", "hessian.Rmd", "lagrange.Rmd"),
      "Statistics exercises" = c("anova.Rmd", "lm2.Rmd", "tstat.Rmd", "ttest.Rmd"),
      "Interpretation of graphics" = c("boxplots.Rmd", "scatterplot.Rmd"),
      "Misc" = c("vowels.Rmd")
    )
    exams2webpool(exrc)
    

    exams2webpool screenshot

    Again, the HTML is self-contained and can be copied to a different directory or given to colleagues etc. Also you may want to tweak the exams2webpool() function or alter its defaults etc:

    exams2webpool <- function(file, n = 3, nsamp = NULL, dir = NULL,
      name = "webpool", title = "R/exams exercise pool", display = TRUE, edir = NULL, ...,
      clean = TRUE, quiet = TRUE, envir = parent.frame()) {
    
      ## based exams2forms
      stopifnot(require("exams2forms"))
    
      ## directory handling
      if(is.null(dir)) {
        dir <- tempfile()
        if(is.null(edir)) edir <- getwd()
      }
      if(!file.exists(dir) && !dir.create(dir))
        stop(gettextf("Cannot create output directory '%s'.", dir))
    
      ## webpool always expects a named list of vectors
      if(!is.list(file)) file <- as.list(file)
      if(is.null(names(file))) names(file) <- seq_along(file)
    
      ## simple webpool template
      wpool <- c(
        '---',
        'title: "%s"',
        'output:',
        '  exams2forms::webquiz:',
        '    toc: true',
        '    toc_float: true',
        '---',
        '',
        '```{r exams2forms-setup, include = FALSE}',
        'library("exams2forms")',
        '```',
        '',
        '%s'
      )
    
      ## exams2forms arguments
      args <- list(...)
      if(!is.null(edir)) args <- c(list(edir = tools::file_path_as_absolute(edir)), args)
      args <- c(list(n = n, nsamp = nsamp), args)
      args <- lapply(args, deparse)    
      args <- lapply(args, paste, collapse = "\n")
      args <- paste(names(args), "=", unlist(args), collapse = ", ")
    
      ## auxiliary functions
      process_exercise <- function(file, args) sprintf(
          paste(c(
            '### %s',
            '',
            '```{r exams2forms-%s, echo = FALSE, message = FALSE, results="asis"}',
            'exams2forms("%s", %s)',
            '```',
            ''), collapse = "\n"),
          gsub("_", "\\_", tools::file_path_sans_ext(file), fixed = TRUE),
          tools::file_path_sans_ext(file),
          file,
          args)
      
      process_section <- function(id, file, args) sprintf(
          paste(c(
            '## %s',
            '',
            '%s',
            ''), collapse = "\n"),
          gsub("_", "\\_", names(file)[id], fixed = TRUE),
          paste(unlist(lapply(file[[id]], process_exercise, args = args)), collapse = "\n"))
      
      ## insert arguments into webpool
      wpool <- sprintf(
        paste(wpool, collapse = "\n"),
        title,
        paste(unlist(lapply(seq_along(file), process_section, file = file, args = args)), collapse = "\n")
      )
    
      ## write webpool .Rmd
      name <- file.path(dir, paste0(name, c(".Rmd", ".html")))
      writeLines(wpool, name[1L])
      
      ## render quiz
      rmarkdown::render(name[1L], clean = clean, quiet = quiet, envir = envir)
      name <- normalizePath(name)
      if(display) browseURL(name[2L])
      invisible(name)
    }