Miguel A. Castellanos and José C. Chacón

mcastellanos@psi.ucm.es
jchacon@psi.ucm.es

Universidad Complutense de Madrid


moodleQ is an R package for the creation of question banks for moodle from a perspective based on R6 classes and Object Oriented Programming (OOP).


Table of Contents

  1. Objectives
  2. Installation and first steps
  3. Using R6 classes
  4. Types of questions
  5. Using Formatted Text
  6. Insert files in the questionnaire
  7. Create answer banks
  8. Example 1: Reading multiple choice questions from a file
  9. Example 2: Building a test with a common statement
  10. Example 3: Downloading a file with data


Objetives

MoodelQ is an R library that allows you to easily create questions of the main types that moodle has. The questions are saved in an xml file that is imported in moodle into your question bank. This system is much faster than creating questions one by one using moodle’s graphical interface and allows you to design more complex questionnaires. This document explains with examples how to use the library. To learn about moodle, its question bank and its xml format, please refer to the following pages:

This package is in a very preliminary phase (v0.5.3) so it must be used with care and it is recalled that it has no guarantee on its operation.

An example, to create a multiple choice question:

q1 <- question$new(
  type="multichoice",
  'name' = "Q001",
  'question' = "This is the question text",
  'answer' = list("Answer #1", 100),
  'answer' = list("Answer #2"),
  'answer' = list("Answer #3"))

Q <- quiz$new(q1)
Q$save_xml("myfile.xml")

The code above generates a multiple choice question with three alternatives (the first one is the correct one, 100) which is saved in the file “myfile.xml” that can be imported into moodle’s question bank. It would generate the following moodle question


Installation and first steps

The package can be installed via devtools and github

devtools::install_github("mcstllns/moodleQ")

If not already installed, devtools can be installed with

install.packages("devtools")

and once the package is installed it is loaded with

library(moodleQ)

Questions are created by invoking the constructor question$new(). The function arguments are the parameters that define a question. Each question type has defined default parameters or fields that must be filled in for it to be a valid question. Many of these parameters have been defined by default, although they can be modified (see the default settings at the end of this document); for example, the single, shuffleanswer and answernumbering arguments can be modified to change the way the question’s alternatives are marked, randomized and numbered, respectively.

q1 <- question$new(
  type="multichoice",
  'name' = "Q001",
  'question' = "This is the question text",
  'single' = 'false',
  'shuffleanswers' = 0,
  'answernumbering' = 'none',
  'answer' = list("Answer #1", 100),
  'answer' = list("Answer #2"),
  'answer' = list("Answer #3"))

The types of moodle questions that are defined in moodleQ are as follows:

The matching and cloze types of questions have not been implemented yet because they are absolutely useless for working with college students learning data analysis and data science disciplines

The following code creates two questions and stores them in a file: a category question (sets the category in which the questions in the file will be stored within the moodle bank) and a multiple choice question. The questions are packaged in a questionnaire called Q:

q0 <- question$new(
  type="category",
  'name' = 'Q000',
  'category' = "$course$/test_moodleQ")

q1 <- question$new(
  type="multichoice",
  'name' = "Q001",
  'question' = "This is the question text",
  'answer' = list("Answer #1", 100),
  'answer' = list("Answer #2"),
  'answer' = list("Answer #3"))

Q <- quiz$new(q0, q1)

To see the content of a question, use the print() method or simply write the name of the object. To see the xml format of each question, the xml() method is used

q0
##           props                 value
## 1   .attrs.type              category
## 2     name.text                  Q000
## 3 category.text $course$/test_moodleQ
q0$xml()
## <question type="category">
##   <name>
##     <text>Q000</text>
##   </name>
##   <category>
##     <text>$course$/test_moodleQ</text>
##   </category>
## </question>

And to see the contents and the xml code of the whole questionnaire:

Q
##
## nquestions:  2
##
## -----------
##           props                 value
## 1   .attrs.type              category
## 2     name.text                  Q000
## 3 category.text $course$/test_moodleQ
##
## -----------
##                       props                     value
## 1               .attrs.type               multichoice
## 2                 name.text                      Q001
## 3       questiontext.format                      html
## 4         questiontext.text This is the question text
## 5  questiontext.text..cdata                      TRUE
## 6              defaultgrade                         1
## 7                   penalty                         0
## 8                    hidden                         0
## 9               synchronize                         2
## 10                   single                      true
## 11          answernumbering                       abc
## 12           shuffleanswers                         1
## 13          unitgradingtype                         0
## 14              unitpenalty                       0.1
## 15                showunits                         3
## 16                unitsleft                         0
## 17                   format                      html
## 18          answer.fraction                       100
## 19            answer.format                      html
## 20              answer.text                 Answer #1
## 21       answer.text..cdata                      TRUE
## 22          answer.fraction                         0
## 23            answer.format                      html
## 24              answer.text                 Answer #2
## 25       answer.text..cdata                      TRUE
## 26          answer.fraction                         0
## 27            answer.format                      html
## 28              answer.text                 Answer #3
## 29       answer.text..cdata                      TRUE
Q$xml()
## <quiz>
##   <question type="category">
##     <name>
##       <text>Q000</text>
##     </name>
##     <category>
##       <text>$course$/test_moodleQ</text>
##     </category>
##   </question>
##   <question type="multichoice">
##     <name>
##       <text>Q001</text>
##     </name>
##     <questiontext format="html">
##       <text><![CDATA[This is the question text]]></text>
##     </questiontext>
##     <defaultgrade>1</defaultgrade>
##     <penalty>0</penalty>
##     <hidden>0</hidden>
##     <synchronize>2</synchronize>
##     <single>true</single>
##     <answernumbering>abc</answernumbering>
##     <shuffleanswers>1</shuffleanswers>
##     <unitgradingtype>0</unitgradingtype>
##     <unitpenalty>0.1</unitpenalty>
##     <showunits>3</showunits>
##     <unitsleft>0</unitsleft>
##     <format>html</format>
##     <answer fraction="100" format="html">
##       <text><![CDATA[Answer #1]]></text>
##     </answer>
##     <answer fraction="0" format="html">
##       <text><![CDATA[Answer #2]]></text>
##     </answer>
##     <answer fraction="0" format="html">
##       <text><![CDATA[Answer #3]]></text>
##     </answer>
##   </question>
## </quiz>

and save it in a file

Q$save_xml("myfile.xml")

Once the file is created it can be imported into moodle as a question bank to build a questionnaire with them. Follow the instructions on this page (Import Questions). In short, you can access the bank of questions from the course page by selecting More.

And then Question Bank -> import. In that page you select Moodle XML format and in the box Import questions from file you select the file created previously.

Once the questions have been imported, a new quiz will be created and questions will be added from the question bank. To see how to create quizzes you can go to https://docs.moodle.org/27/en/Building_Quiz


Using R6 classes

moodleQ has been built with R6 classes, which gives it greater versatility in handling objects and methods than the libraries built with S3 and S4 classes. Its use is more reminiscent of the code of other object-oriented languages and has a somewhat more modern look. For example, the following syntax creates an object using the new() constructor.

q1 <- question$new(
  type="multichoice",
  'name' = "Q001",
  'question' = "This is the question text",
  'answer' = list("Answer #1", 100),
  'answer' = list("Answer #2"),
  'answer' = list("Answer #3"))

But the following syntax in which the addanswer() method is applied in a chained way is equally valid. This possibility makes the writing of the code more flexible.

q1 <- question$new(
  type="multichoice")

q1$set('name' = "Q001",
       'question' = "This is the question text")

q1$addanswer(list("Answer #1", 100))$
   addanswer(list("Answer #2"))$
   addanswer(list("Answer #3"))

A list of available methods can be seen in the package documentation

help(package = "moodleQ")

And to learn more about the use of R6 classes, the following document is recommended: https://r6.r-lib.org/articles/Introduction.html

Using R6 can sometimes be confusing if you are used to the classic R form but it allows you to easily modify any object the way you want, which will be useful when working with questionnaires. The strategy of defining the whole question in the construction of the object or creating a simple skeleton to modify it will depend on the interests of the code.


Types of questions

All question types accepted by moodle have been implemented (except cloze and matching). This section explains how to generate each of them together with the result obtained in moodle.

category

It is not a question type, it only defines the category in which moodle will add what comes next. Usually it is the first one. The string \(course\) must be defined for moodle to properly place the questions with respect to the course root.

q0 <- question$new(
  type="category",
  'name' = 'Q000',
  'category' = "$course$/test_moodleQ")

multichoice

It is a question with alternatives (multiple choice question). It accepts many parameters but in its simplest format it requires a statement and at least two alternatives. The number of alternatives is variable.

q1 <- question$new(
  type="multichoice",
  'name' = "Q001",
  'question' = "This is the question text",
  'answer' = list("Answer #1", 100),
  'answer' = list("Answer #2"),
  'answer' = list("Answer #3"))

Each alternative is defined as follows: text, punctuation, formatting:

  • Text: required.
  • Punctuation: by default equal to 0, at least one of the alternatives must have a value of 100.
  • Format: by default the format defined in the format attribute is chosen.

It can also be configured with many other parameters, some of which are interesting:

  • single (values: true/false): if it is true then the alternatives are independent (you can choose all or none), if it is false then when you mark one the rest are unmarked.
  • shuffleanswers (values: 1/0): to order or not the alternatives.
  • answernumbering (allowed values: ‘none’, ‘abc’, ‘ABCD’ or ‘123’): changes the way the alternatives are listed.

To see all the options defined by default can be done:

def_conf[['multichoice']]
##
## .attrs.type                multichoice
## name.text                         <NA>
## questiontext..attrs.format        html
## questiontext.text                 <NA>
## questiontext.text..cdata          TRUE
## defaultgrade                         1
## penalty                              0
## hidden                               0
## synchronize                          2
## single                            true
## answernumbering                    abc
## shuffleanswers                       1
## unitgradingtype                      0
## unitpenalty                        0.1
## showunits                            3
## unitsleft                            0
## format                            html

description

It is not a question, just a statement or a text that is uploaded to the students. Of any length you want.

q3 <- question$new(
  type="description",
  'name' = 'Q003',
  'question' = 'This is a very long question text explaining interesting things')

truefalse

Questions in which only the options of true and false appear

q5 <- question$new(
  type='truefalse',
  'name' = "P005",
  'question' = 'This is a question text for true/false questions',
  'answer' = list("true", 100),
  'answer' = list("false", 0),
)

One of the alternatives must be 100 and the other 0. Actually it is equivalent to multichoice but the values are defined as true and false and then moodle adapts it to the language of the installation (true and false in Spanish).

shortanswer

These are questions with short alphanumeric answers that have to be adjusted to a defined answer. You can create as many as you need and each one can have a different score.

q6 <- question$new(
  type="shortanswer",
  'name' = "P006",
  'question' = 'This is a question text for shortanswer questions',
  'answer' = list("This is the right answer", 100),
  'answer' = list("This is the ALMOST right answer", 90)
)

numerical

Question with numerical solution answers. There may be several and with different success rates.

q7 <- question$new(
  type="numerical",
  'name' = "P007",
  'question' = 'This is a question text for numerical questions',
  'answer' = list(17, 100)
)

The above questions, when included in a questionnaire, would generate the following document:

***

Use of formatted text

In moodleQ a default format type (HTML) is defined and when the question is generated it is used to create the format of the statements and alternatives. If you change the format, do so before entering the “answer” attributes so that they use the desired format when creating them.

Both statements and answers can be written in plain text, html or markdown. The questions have a format field that defines by default which format is to be used, although it can be defined separately for each ‘answer’ element.

The plain text format only accepts basic ascii so it is not recommended, while html and markdown accept an enriched format in which font elements such as bold, italics or images can be defined, etc.

By default moodleQ sets the format to html because it is the most functional among the different versions and installations of moodle but can be changed with the argument “format”. The following two questions produce the same result, the first using html and the second using markdown.

q1 <- question$new(
  type="description",
  'name' = 'Q001',
  'question' = '
<h1>Heading #1</h1>
<h2>Heading #2</h2>
<bold>bold text </bold>
<i>italic</i>
<u><li>one</li> <li>two</li></u>
<p> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc id fringilla diam, id lobortis eros. Quisque consequat ultricies enim, nec interdum nisl dignissim sit amet. Pellentesque quis erat non odio cursus porta </p>
  ')

q2 <- question$new(
  type="description",
  'name' = 'Q002',
  'format' = 'markdown',
  'question' = '
#Heading #1

##Heading #2

**bold text**

*italic*

* one
* two

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc id fringilla diam, id lobortis eros. Quisque consequat ultricies enim, nec interdum nisl dignissim sit amet. Pellentesque quis erat non odio cursus porta
  ')

Both in html format and in markdown you can enter formulas defined in Tex, which is the syntax format that uses latex, can be complex at first but there are some online editors that facilitate the task, even the moodle text editor incorporates one:

IMPORTANT NOTICE: Traditionally in Tex you use the $...$ symbols (where … indicates the formula in latex syntax) to create an independent formula, in a new line and centered; and the $$...$$ symbols to create it as an inline element, that is inserted in the middle of the text. In modern versions of moodle these symbols are replaced by `````````` respectively, and they strongly recommend their use as opposed to the previous ones.

When in Moodle is installed the markdown filter both can be used indistinctly with html and markdown, but this filter is not usually installed, generating different behaviors.

In the following code we try both ways of indicating a formula for the two formats and the figure shows the result:

# questiontext= '
# **Inline equation using "\(":**( \alpha = 2*\beta \)
#
#
##Equation using "##":##
#
# alpha = 2*\beta \
#
# **Inline equation using "$":** $ \alpha = 2*\beta $
#
# **Equation using "$$":**
#
# $$ \alpha = 2*\beta $$
# '

When the filter is not installed $...$$ works in both formats, $...$ does not work in either and the new moodle formats only work with html. When writing formulas this must be taken into account.


Insert files in the questionnaire

It is possible to insert documents in the question statements, usually in a first question of description type. The idea is to use the tag to embed a file encoded in base64 in the moodle question that can then be downloaded. To do this you can use the ‘file’ tag with the constructor or directly the addfile() method.

The following code creates a question and inserts three files, A.csv, B.csv and C.csv for the student to download one of them.

# Embed files
q1 <- question$new(
  type="multichoice",
  'name' = "P001",
  'question' = 'Choose: <a href="@@PLUGINFILE@@/A.csv">download A.csv</a> or
      <a href="@@PLUGINFILE@@/B.csv">download B.csv</a>
  <a href="@@PLUGINFILE@@/C.csv">download C.csv</a>',
  answer' = list("Answer 1", 100),
  'answer' = list("Answer 2"))



q1$addfile("./file.csv", "A.csv")
q1$addfile("./file.csv", "B.csv")

df <- data.frame(a = rnorm(10), b=rnorm(10))

tmp <- tempfile()
write.table(df, file=tmp, sep=",", dec=".")
q1$addfile(tmp, "C.csv")
unlink(tmp)
q1$xml()

The first two files are stored on the hard disk and we incorporate them with the addfile method that has three arguments:

  • drivePath: the path to the file in the computer
  • name = NULL: the name of the file, if it is not given we use the one that has the file.
  • quizPath=“/”: the relative path in the questionnaire file structure, by default “/”.

The third file is not stored in the disk, but a data.frame and an R connection is created to be stored and read from there. This allows you to have a code that generates a file for the student to download and analyze and then answer questions about it.

Notice how in the code of the question statement of the previous example the links in html have been introduced so that the files can be downloaded and how it is necessary that the url is preceded by _@@PLUGINFILE@_.

As the files are embedded with the addfile() method they can also be embedded using the constructor. The following code would do the same but from the question constructor. Sometimes one will be useful and sometimes the other.

# the same work but adding to it from the builder

tmp <- tempfile()
write.table(df, file=tmp, sep=",", dec=".")
#q1$addfile(tmp, "C.csv")


q2 <- question$new(
  type="multichoice",
  'name' = "P002",
  'question' = 'Download: <a href="@@PLUGINFILE@@/A.csv">download A.csv</a> o
      <a href="@@PLUGINFILE@@/B.csv">download B.csv</a>
  <a href="@@PLUGINFILE@@/C.csv">download C.csv</a>',
  'file' = list("./fichero.csv", "A.csv"),
  'file' = list("./fichero.csv", "B.csv"),
  'file' = list(tmp, "C.csv"),
  answer' = list("Answer 1", 100),
  'answer' = list("Answer 2"))

unlink(tmp)

q2$xml()

In the same way that we have included .csv files you can include any type of file you want.


Create response banks

The calculated type questions allow you to work with banks of answers. These are questions that accept wildcards or tokens that can be used both in the statement and in the answer alternatives. These wildcards are like variables or identifiers that accept different values. It is easier to understand with an example:

q1 <- question$new(
  type="calculated",
  'name' = "P001",
  'question' = "
  With the numbers: A = {A} and B = {B}


  <p>Calculate: A + B</p>

  ",
  'answer' = list("{C}", 100),
  'dataset' = list(data.frame(A = c(1, 4))),
  'dataset' = list(data.frame(B = c(3, 2))),
  'dataset' = list(data.frame(C = c(4,6))))  

Moodle will randomly present one of the 2 available rows, for example row 1 and will use the values of A, B and C (1, 3 and 4) for the question. When answering with number 4 the answer is marked correct.

In addition, moodle allows you to synchronize the banks of different questions with the synchronize option (default in moodleQ). If the subject is assigned row 1 when the questionnaire is opened, all questions will be assigned row 1, which opens interesting options.

q2 <- question$new(
  type="calculated",
  'name' = "P002",
  'question' = "Calculate A * B",
  'answer' = list("{D}", 100),
  'dataset' = list(data.frame(D = c(3,8))))

When synchronized, if the student is assigned row 1 (values 1 and 3), the correct answers to both questions will be those in row 1: C = 4 and D = 3. This synchronization of the answers opens up the possibility of building more complex database based analyses.

An important clarification__, the calculated questions allow to associate a base entry with a subject, but it is only possible with this type of questions. If we want you to answer multiple choice questions, we have to make a compromise.

q3 <- question$new(
  type="calculated",
  'name' = "P001",
  'question' = "
Calcule A - B.
<hr>
<p>Write in the answer box the number of the right statement:</p>
<u>
<li><b>1</b> = The result is positive</li>
<li><b>2</b> = The result is negative</li>
</u>
  ",
  'answer' = list("{E}", 100),
  'dataset' = list(data.frame(E = c(2, 1))))

In this way the multiple choice is linked to the base assigned to the subject.

Two restrictions apply to this type of question:

  • The variables can only be of a numerical type.
  • You can only use tokens that have been embedded in the same question.

Example 1: reading multiple choice questions from a file

What we do is we store a database of questions in a rich and easily readable format. To keep them organized, we include category questions. For example, we have a file with questions in markdown format in which each question forms a block separated by blank lines and where the first line is the statement, then the correct alternative and then the rest of the alternatives:

category tests_moodleQ

This is statement 1
Answer1.1
Answer1.2
Answer1.3

This is the second statement
Answer2.1
Answer2.2
Answer2.3
Answer2.4

And with a simple code you can read the previous file and pack it in a moodle quiz.

lines <- readLines("./questions.md")

Q <- quiz$new()

nq = 0; i=1
while(i<=length(lines)){
  if(lines[i]=="") i = i + 1
  else if(grepl("category", lines[i])){
    nq = nq + 1
    Q$add(
      question$new(
        'type' = "category",
        'name' = sprintf("P%03d",nq),
        'category' = sprintf("$course$/%s", strsplit(lines[i], " ")[[1]][[2]]))
    i = i + 1
  } else {
    nq = nq + 1
    q <- question$new(type="multichoice",
            'name' = sprintf("P%03d",nq),
            format' = 'Markdown',
            'question' = lines[i],
            'answer' = list(lines[i+1], 100))
    j = i+2
    while(lines[j]!="" & j <= length(lines)){
      q$addanswer(list(lines[j]))
      j = j + 1
    }
    Q$add(q)
    i = j
  }
}

Q$save_xml("miquiz.xml")

Example 2: a test with a common statement

In this example we are going to build a questionnaire that has a description of a problem and they have to download a file embedded in pdf. Then the questions refer to the reading of that downloaded file. All students read the same statement and share the same questions and answers.

# A statement using html
myquestiontext = '
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent egestas urn a dapibus imperdiet. Quisque augue neque, ornare pharetra facilisis ut, ultricies eu magna. Nulla facilisi. That's what I do with my readings. Integrate pharetra sagittis luctus. Sed vel nisi eu arcu sollicitudin venenatis non vel nisi. We are happy, we are comfortable, we are temperate. Proin viverra ultrices laoreet. No request for diam, at faucibus massa. Mauris dapibus lorem ipsum, et condimentum eros mattis sit amet. Phasellus tempor velit odio, non eleifend eros molestie non.

Download this pdf: [reading.pdf](@@PLUGINFILE@@/reading.pdf)
'

q0 <- question$new(
  type="category",
  'name' = 'P000',
  category' = "$course$/Example2")


q1 <- question$new(
  type="description",
  'name' = 'P001',
  'question' = myquestiontext,
  format' = 'markdown',
  'file' = list("./reading.pdf"))

# now a couple of questions

q2 <- question$new(
  type="multichoice",
  'name' = "P002",
  'question' = "Question #1 about reading.pdf",
  answer' = list("Yes", 100),
  answer' = list("No"),
  'answer' = list("Who knows"))

q3 <- question$new(
  type='truefalse',
  'name' = "P003",
  'question' = 'Question #2 about reading.pdf',
  answer' = list("true", 100),
  answer' = list("false"),
)

Q <- quiz$new(q0, q1, q2, q3)

Example 3: downloading a file with data

This is the example that I think may be most useful for data analysis. Data files are generated that are embedded in a question. The student is assigned one of them and has to download it and analyze it. The answers to the questions are linked to the files.

data <- list()
for(i in 1:10) data[[i]] <- data.frame(X = rnorm(10), Y = rnorm(10))

results <- data.frame(
  FILE = sample(1000000:9999999, 10),
  COR = unlist(lapply(data, function(x) cor(x$X,x$Y)),
  TTEST.t = as.vector(unlist(lapply(data, function(x) t.test(x$X,x$Y)$statistic))),
  TTEST.gl = as.vector(unlist(lapply(data, function(x) t.test(x$X,x$Y)$parameter)),
  TTEST.pvalue = as.vector(unlist(lapply(data, function(x) t.test(x$X,x$Y)$p.value)) )

q0 <- question$new(
  type="category",
  'name' = 'P000',
  category' = "$course$/example1")


q1 <- question$new(
  type="calculated",
  'name' = "P001",
  format' = 'markdown',
  'question' = '.
Download this .csv dataset: [ID_{FILE}.csv](@@PLUGINFILE@@/ID_{FILE}.csv)

Calculate the correlation:',
  answer' = list("{COR}", 100),
  dataset' = list(data.frame(FILE = results$FILE )),
  dataset' = list(data.frame(COR = results$COR )) )

# we embed the files
for (i in 1:10){
  tmp <- tempfile()
  write.table(data[[i]], file=tmp, sep=",", dec=".")
  q1$addfile(tmp, sprintf("ID_%d.csv", results$FILE[i])
  unlink(tmp)
}


q2 <- question$new(
  type="calculated",
  'name' = "P002",
  'question' = "
    Calculate a t.test and write the statistic (t) value:",
  answer' = list("{TTEST.t}", 100),
  dataset' = list(data.frame(TTEST.t = results$TTEST.t )) )


q3 <- question$new(
  type="calculated",
  'name' = "P003",
  'question' = "
    Write the degree of freedom:",
  answer' = list("{TTEST.gl}", 100),
  'dataset' = list(data.frame(TTEST.gl = results$TTEST.gl )) )

q4 <- question$new(
  type="calculated",
  'name' = "P004",
  'question' = "
    Write the p-value:",
  answer' = list("{TTEST.pvalue}", 100),
  dataset' = list(data.frame(TTEST.pvalue = results$TTEST.pvalue )) )

Q <- quiz$new(q0, q1, q2, q3, q4)
# Q$xml()

Q$save_xml("mifile11.xml")

Miguel A. Castellanos (2020)