Search code examples
rshinyr-markdownlearnr

How to score a learnr tutorial in R by the author?


I am trying to create a quiz app using learnr package in R.I want to score the candidate at the or in progress and save the score in RData format.

I think the function ,get_tutorial_state(), may help but I don't know how to use it in my code.

Below is my code. Any help is appreciated.

---
title: "Quiz"
output:
  learnr::tutorial:
    progressive: true
    allow_skip: true
    language:
      en:
        button:
          nexttopic: Next Question
          previoustopic: Previous Question
runtime: shiny_prerendered
 
  
---
library(learnr)
knitr::opts_chunk$set(echo = FALSE)

Question 1

question("Check all that you have NOT done:",
  answer("installed R on my computer", message = "* Install R"),
  answer("installed the RStudio IDE", message = "* Install RStudio IDE"),
  answer("installed the tidyverse R package", message = "* Install Packages"),
  answer("None of the above. I've done them all.", correct = TRUE, message = "You can skip this tutorial!"),
  type = "multiple",
  incorrect = "This tutorial is here to help! To get set up read:"
)

Question 2

question("Where do you download R?",
    answer("www.rstudio.com/download"),
    answer("[cloud.r-project.org](http://cloud.r-project.org)", correct = TRUE, message = "You can also download R from [cran.r-project.org](http://cran.r-project.org)"),
    answer("www.r-project.org", message = "Good try, but not exactly. www.r-project.org doesn't provide a download link, but it does provide a link to one the websites above."),
    answer("www.r.com"),
    allow_retry = TRUE
  )

Questions

Some questions to verify that you understand the purposes of various base and recommended R packages:

quiz(caption = "",
  question("Which package contains functions for installing other R packages?",
    answer("base"),
    answer("tools"),
    answer("utils", correct = TRUE),
    answer("codetools")
  ),
  question("Which of the R packages listed below are used to create plots?",
    answer("lattice", correct = TRUE),
    answer("tools"),
    answer("stats"),
    answer("grid", correct = TRUE)
  )
)

Solution

  • A basic implementation of get_tutorial_state() to display a score looks like this. You can append it to your .Rmd-document.

    ```{r context="server"}
    shiny::observeEvent(
      input$get_score, 
      {
        objs2 = learnr:::get_tutorial_state()
        
        # Number of correct questions
        
        n_correct <- 
          # Access the $correct sublist item in each list item
            lapply(objs2, purrr::pluck, "correct") |>
               # make it a vector containing: TRUE and FALSE and NAs
               # NA is appearing for list items which don't have
               # a $correct subitem
                    unlist() |> 
               # Taking the sum of a logical Vector returns the number of TRUEs
                    sum(na.rm=TRUE)
        
        # Number of total questions
        
        total_questions <- 
          # 1. Access $type in each list item and make it a vector of types
          lapply(objs2, purrr::pluck, "type") |> unlist()
        
        # 2. Count the number of "question" in that vector
        total_questions <- total_questions[total_questions == "question"] |> 
          length()
          
          
        output$score = shiny::renderText(
          paste0(n_correct, " out of ", total_questions,
            " question(s) were correct.")
    )
        invisible()
      }
    )
    ```
    
    ```{r score, echo=FALSE}
    shiny::actionButton("get_score", "Get Score")
    shiny::br()
    shiny::br()
    shiny::textOutput("score")
    ```
    

    The output looks like this:

    get score output

    How I got there

    You can find a demo of how to use get_tutorial_state() in the the learnrhash package at inst/demo_state.Rmd:

    ---
    title: "learnrhash - state"
    output: learnr::tutorial
    runtime: shiny_prerendered
    tutorial:
      id: "demo-state"
      version: 1.1
    ---
    
    ```{r setup, include=FALSE}
    library(learnr)
    ```
    
    ## Question
    
    ```{r planets, echo=FALSE}
    learnr::question(
      "Which planet do we live on?",
      answer("Mars",   correct = FALSE),
      answer("Earth",  correct = TRUE),
      answer("Saturn", correct = FALSE),
      allow_retry = TRUE
    )
    ```
    
    ## Check State
    
    ```{r context="server"}
    shiny::observeEvent(
      input$get_state, 
      {
        objs = learnr:::get_tutorial_state()
      
        output$state = shiny::renderText(
          paste(
            capture.output(str(objs)),
            collapse = "\n"
          )
        )
        invisible()
      }
    )
    ```
    
    ```{r state, echo=FALSE}
    shiny::actionButton("get_state", "Get State")
    shiny::br()
    shiny::br()
    learnrhash:::wrapped_verbatim_text_output("state")
    ```
    
    

    Focus on the Check State section.

    It is basically like writing a tiny shiny app within your tutorial: You need a context = "server" code chunk where the server logic goes. And you need a standard code chunk where the user interface part goes (context = "ui" is the default option for all code chunks, hence you can omit it from code chunk options and just use a standard code chunk).

    If you have not already, I suggest that you acquire some basic knowledge in writing a shiny app before you proceed.

    The output of the above tiny shiny app to call get_tutorial_state() looks like this:

    shiny app output

    So get_tutorial_state returns a list.

    This list contains one list item for each question (and, important: exercises as well, there are just no exercises in this demo). Inside each list item that represents a question, there is:

    • a $correct entry containing TRUE or FALSE.

    In all list items, regardless whether it is for an exercise or a question, there is:

    • a $type entry containing "exercise" or "question".

    You can harvest this information like I did in my solution code at the beginning.

    My solution from above is extracting the necessary data out of the state list, this is done inside the server chunk.

    I also changed the output format in the ui chunk to normal text because verbatim is not what I was looking for.

    getting hands on the tutorial state table

    If you want to better understand the structure of the tutorial state output list, it would be good to access it outside the shiny app.

    In the above demo-state app, change the line

    capture.output(str(objs))
    

    to

    capture.output(dput(objs))
    

    which yields the following if I apply this tiny shiny app in your quiz tutorial. You can copy paste that and hence recreate the list in your "normal" RStudio Environment, to experiment with it.

    list(check = list(type = "question", answer = "installed R on my computer", 
        correct = FALSE, timestamp = "2023-11-03 10:42:14.762 UTC"), 
        q2 = list(type = "question", answer = "www.rstudio.com/download", 
            correct = FALSE, timestamp = "2023-11-03 10:42:19.599 UTC"), 
        `packages-1` = list(type = "question", answer = "utils", 
            correct = TRUE, timestamp = "2023-11-03 10:42:25.129 UTC"), 
        `packages-2` = list(type = "question", answer = "lattice", 
            correct = FALSE, timestamp = "2023-11-03 10:42:31.806 UTC"))