rm(list = ls()) if (!require(devtools)) install.packages("devtools") if (!require(stringi)) install.packages("stringi") if (!require(knitr)) install.packages("knitr") if (!require(slidify)) devtools::install_git(c("ramnathv/slidify", "ramnathv/slidifyLibraries")) #sCourseToolsPackDir <- ifelse(.Platform$OS.type == "windows", # "c:/Daten/pvr/Google Drive/Projects/RStudioProjects/RCourseTools", # "/Users/peter/Google Drive/Projects/RStudioProjects/RCourseTools") devtools::load_all()
Course management involves a standard set of tasks. Once a course is planned, outlined and scheduled, the following tasks must to be executed.
Support for course management can be provided by a set of tools which fascilitate the above listed tasks.
SageMathCloud (SMC) is an open source platform which provides extensive functionality and already solves many of the above tasks. The only thing that is missing so far is convenient offline support. That means as soon as one is disconnected from the internet, it is impossible to use the tools provided by SMC. There is always the possibility to download the files that one is working on to the local machine before going offline and uploading them again when getting re-connected, but that is somewhat tedious and most users would probably wish for a more seamless workflow.
Secondly, when working with the course tools on SMC, all participants of the course such as co-teachers, TAs and students are forced to have an account on SMC. While this might be fine for most course participants, there might be some persons who refuse to create an account on SMC. Furthermore, once course participants have an account, they are required to remember their account credentials which is also not a problem for most participants, but it might be painful for the two or three students who always forget their password and have to get it reset all the time. These problems can be addressed by distributing the course material via a git repository such as Github. Combining the distribution of course material via Github together with R packages such as devtools
, it is very convenient to clone Github repositories. Furthermore R provides a convenient packaging infrastructure which can be used to organize the complete course material.
When trying to come up with a model for a course, one has to distinguish between at least two views.
testable and reusable components
(TRUC).The main focus of this package is to automate and to fascilitate as much as possible the operations with the components of the representation view of a course.
The goal is to have the same functionality as other open source online course tools such as SMC and to have a more seamless workflow and better offline support provided by the set of tools.
This goal is achieved by combining the R packaging mechanism together with the Github support provided by certain R-packages such as devtools
and by R-Studio. More concretely, the complete course material is contained in an R package which can easily be downloaded and installed using the R package devtools
. The main advantage of this approach of putting all course material into an R package is that apart from R, RStudio and devtools
which are required anyway for the course, no additional software needs to be installed or no account must be created by the students.
When starting from scratch, the first thing is to come up with an initial skeleton for a course. This can be done by a call to the function create_course()
. The only mandatory argument that has to be specified when calling create_course()
is the name of the course that we want to create. The course skeleton that is created by create_course()
consists of a number of subdirectories and files all grouped in a directory that has the same name as the course. When not specifying any directory argument to create_course
then the current working directory will be used as parent directory for the course.
The following code section shows a first version of a function that creates a course skeleton using devtools::create()
to come up with an initial course package
#' Create a course with a given name under a given working directory #' #' \code{create_course} uses devtools::create to create a package skeleton #' #' @param psCourseName name of the course #' @param psCourseWorkDir course working directory #' @example create_course("MyCourse") #' @export create_course <- function(psCourseName, psCourseWorkDir = getwd()) { ## ## create_course(psCourseName, psCourseWorkdir): creates the ## skeleton of a course in the form of an R package ## ########################################################### ## ## # check that at least course name argument is specified stopifnot(!is.null(psCourseName)) ## # if the parent directory of the course directory does not exist, create it if (! dir.exists(psCourseWorkDir)) dir.create(psCourseWorkDir) ## # create an R package using devtools::create sCoursePath <- file.path(psCourseWorkDir,psCourseName) ## # in case old versions exist, remove them if (dir.exists(sCoursePath)) unlink(sCoursePath, recursive = TRUE, force = TRUE) ## # use devtools to create the package devtools::create(path = sCoursePath) cat("\n * Created course: ", psCourseName, " in directory: ", psCourseWorkDir, "\n") invisible() }
Now that we have created a first version of a function that creates a course skeleton, we are ready to test it. Before the call to create_course
, we are assigning the course name, the course working directory and the course description to variables.
sCourseName <- "rintrocourse" sCourseWorkDir <- ifelse(.Platform$OS.type == "windows", "c:/Daten/pvr/Projects/RStudioProjects/Courses", "/Users/peter/Data/Projects/RStudioProjects/Courses") sCoursePath <- file.path(sCourseWorkDir, sCourseName) create_course(psCourseName = sCourseName, psCourseWorkDir = sCourseWorkDir)
The initial version of the function create_course
as shown above creates a R package for the course. The DESCRIPTION file contains default values that are determined by the devtools specific options stored in the list obtained by options()
.
In a first refinement we want to give the user the possibility to specify the information that goes into the DESCRIPTION file more precisely. So let us have a look at the content of the DESCRIPTION file. There is a special function to read the content of DESCRIPTION files called read.dcf()
where dcf
stands for Debian Control File.
mDescContent <- read.dcf(file = file.path(sCoursePath, "DESCRIPTION")) print(mDescContent)
The easiest way to specify more information that should go into the description file is to set the devtools specific options, such as shown below.
## save away old values of options opar <- options() ## specify new options for devtools options(devtools.desc.author = person(given = "Peter", family = "von Rohr", email = "peter.vonrohr@gmail.com", role = c("aut", "cre")), devtools.desc.license = "LGPL-3", devtools.name = "Peter von Rohr", devtools.desc = list(Title = "Introduction To R", Description = "Introductory course to the programming language R")) ## run course creation again create_course(psCourseName = sCourseName, psCourseWorkDir = sCourseWorkDir) ## restore old options options(opar)
Again looking at the description file of the created course package, we can see that the specified information is included now.
mDescContent <- read.dcf(file = file.path(sCoursePath, "DESCRIPTION")) print(mDescContent)
In principle the above shown solution is fine. The problem is that the dependency between the RCourseTools
package and
the devtools
package is opened to the user. That means, users of the RCourseTools
package must know about internals
of the devtools
package. This is an additional burden that we do not want to impose on the users of the RCourseTools
package.
One possibility of specifying information to be written to a DESCRIPTION file is to define a Reference Class (RC) which contains the information that we want to be written to the DESCRIPTION file. For more information on reference classes and for examples on how to use them, please read the blog post on Experiments with S4 and Reference Classes.
When looking at a description file, we recognize that the all fields ot the description file, except the author field can be modelled with basic data types. For the author field we are using a separate small reference class which looks as follows.
#' Reference class to represent an author in a description file #' #' @fields givenName authors given name #' @fields familyName authors family name #' @fields emailAddress authors email address #' @fields role authors role DcfAuthorRefClass <- setRefClass(Class = "DcfAuthorRefClass", fields = list( givenName = "character", familyName = "character", emailAddress = "character", role = "character" ), methods = list( setGivenName = function(psGivenName){ "setter for givenName" givenName <<- psGivenName }, getGivenName = function(){ "getter for givenName" return(givenName) }, setFamilyName = function(psFamilyName){ "setter for familyName" familyName <<- psFamilyName }, getFamilyName = function(){ "getter for familyName" return(familyName) }, setEmailAddress = function(psEmailAddress){ "setter for emailAddress" emailAddress <<- psEmailAddress }, getEmailAddress = function(){ "getter for emailAddress" return(emailAddress) }, setRole = function(psRole){ "setter for role" role <<- psRole }, getRole = function(){ "getter for role" return(role) }, addRole = function(psRole){ "adding a role to current list of roles" role <<- c(role, psRole) }, toDcfString = function(){ "writing author information to dcf-formatted string" return(paste("\"", givenName, " ", familyName, " <", emailAddress, "> [", stringi::stri_flatten(role, collapse = ", "), "]\"", sep = "", collapse = "")) } ))
The main reference class for the description file uses the above defined reference class for the author as one component.
#' Reference class representing R package description objects #' #' @fields title dcf field title #' @fields version dcf field version #' @fields author dcf field author #' @fields description dcf field description #' @fields depends dcf field depends #' @fields licence dcf field licence #' @fields LazyData dcf field LazyData #' @fields sDcfFileName output file name #' @fields mExistingDescription container for an existing dcf object RCDesc <- setRefClass(Class = "RCDesc", fields = list( title = "character", version = "character", author = "DcfAuthorRefClass", description = "character", depends = "character", licence = "character", LazyData = "character", sDcfFileName = "character", mExistingDescription = "matrix" ), methods = list( # setters setTitle = function(pstitle){ title <<- pstitle }, setVersion = function(psversion){ version <<- psversion }, setAuthor = function(poauthor){ author <<- poauthor }, setDescription = function(psdescription){ description <<- psdescription }, setDepends = function(psdepends){ depends <<- psdepends }, setLicence = function(pslicence){ licence <<- pslicence }, setLazyData = function(psLazyData){ LazyData <<- psLazyData }, setSDcfFileName = function(pssDcfFileName){ sDcfFileName <<- pssDcfFileName }, # getters getTitle = function() { return(title) }, getVersion = function() { return(version) }, getAuthor = function() { return(author) }, getDescription = function() { return(description) }, getDepends = function() { return(depends) }, getLicence = function() { return(licence) }, getLazyData = function() { return(LazyData) }, getSDcfFileName = function() { return(sDcfFileName) }, # convenience methods for author setGivenName = function(psgivenName){ author$setGivenName(psgivenName) }, setFamilyName = function(psfamilyName){ author$setFamilyName(psfamilyName) }, setEmailAddress = function(psemailAddress){ author$setEmailAddress(psemailAddress) }, setRole = function(psrole){ author$setRole(psrole) }, getGivenName = function() { return(author$getGivenName()) }, getFamilyName = function() { return(author$getFamilyName()) }, getEmailAddress = function() { return(author$getEmailAddress()) }, getRole = function() { return(author$getRole()) }, # show info show = function() { "Showing current conent of DCF object" cat(" * Title: ", title, "\n", " * Version: ", version, "\n", " * Author: ", author$toDcfString(), "\n", " * Description: ", description, "\n", " * Depends: ", depends, "\n", " * Licence: ", licence, "\n", " * LazyData: ", LazyData, "\n", " * DcfFile: ", sDcfFileName, "\n" ) }, # reading an existing description file readDcf = function() { "Reading a description file" # check whether file exists if(file.exists(sDcfFileName)) { mExistingDescription <<- read.dcf(file = sDcfFileName) } else { cat(" * WARNING: Dcf file: ", sDcfFileName, " NOT FOUND\n") } }, # writing the info writeDcf = function() { "Write DCF information to a file" write.dcf(mExistingDescription, file = sDcfFileName) }, # add info to existing dcf addToDcf = function() { "Add DCF information to existing description" if(!is.null(title)) mExistingDescription[1,"Title"] <<- title if(length(version) > 0) mExistingDescription[1,"Version"] <<- version if(length(author) > 0) mExistingDescription[1,"Author"] <<- author$toDcfString() if(length(description) > 0) mExistingDescription[1,"Description"] <<- description if(length(depends) > 0) mExistingDescription[1,"Depends"] <<- depends if(length(licence) > 0) mExistingDescription[1,"License"] <<- licence if(length(LazyData) > 0) mExistingDescription[1,"LazyData"] <<- LazyData } ))
We want to specify the same information with the reference class, as with the devtools
options above.
rCourseDesc <- RCDesc$new() rCourseDesc$setAuthor(DcfAuthorRefClass$new()) rCourseDesc$setGivenName("Peter") rCourseDesc$setFamilyName("von Rohr") rCourseDesc$setEmailAddress("peter.vonrohr@gmail.com") rCourseDesc$setRole(c("aut","cre")) rCourseDesc$setLicence("LGPL-3") rCourseDesc$setTitle("An Introduction To R") rCourseDesc$setDescription("Introductory course to the programming language R") rCourseDesc$setSDcfFileName(pssDcfFileName = file.path(sCoursePath, "DESCRIPTION")) rCourseDesc$show() rCourseDesc$readDcf() rCourseDesc$addToDcf() rCourseDesc$writeDcf() mDescContent <- read.dcf(file = file.path(sCoursePath, "DESCRIPTION")) print(mDescContent)
The improved functionality of specifying information in the description file is now combined with the creation function.
#' Create a course with a given name under a given working directory #' #' \code{create_course} uses devtools::create to create a package skeleton #' #' @param psCourseName name of the course #' @param psCourseWorkDir course working directory #' @example create_course("MyCourse") create_course <- function(psCourseName, psCourseWorkDir = getwd(), poDesc = NULL) { ## ## create_course(psCourseName, psCourseWorkdir): creates the ## skeleton of a course in the form of an R package ## ########################################################### ## ## # check that at least course name argument is specified if (is.null(psCourseName)) stop(" * ERROR in create_course: Argument psCourseName missing\n") ## # if the parent directory of the course directory does not exist, create it if (! dir.exists(psCourseWorkDir)) dir.create(psCourseWorkDir) ## # create an R package using devtools::create sCoursePath <- file.path(psCourseWorkDir,psCourseName) ## # in case old versions exist, remove them if (dir.exists(sCoursePath)) unlink(sCoursePath, recursive = TRUE, force = TRUE) ## # use devtools to create the package devtools::create(path = sCoursePath) cat("\n * Created course: ", psCourseName, " in directory: ", psCourseWorkDir, "\n") if (!is.null(poDesc)) { poDesc$setSDcfFileName(pssDcfFileName = file.path(sCoursePath, "DESCRIPTION")) poDesc$readDcf() poDesc$addToDcf() poDesc$writeDcf() } ## # copy templates directory sTemplateSrcDir <- file.path(system.file(package = "rcoursetools"), "extdata", "templates") sTemplateTrgDir <- file.path(sCoursePath, "inst", "extdata") if (!dir.exists(sTemplateTrgDir)) dir.create(sTemplateTrgDir, recursive = TRUE) cat(" * Copying templates from: ", sTemplateSrcDir, "\n to: ", sTemplateTrgDir, "\n") file.copy(from = sTemplateSrcDir, to = sTemplateTrgDir, recursive = TRUE, copy.mode = TRUE) invisible() }
A course package can now be created with the new function which accepts as additional argument an instance of type RCDesc
.
create_course(psCourseName = sCourseName, psCourseWorkDir = sCourseWorkDir, poDesc = rCourseDesc) mDescContent <- read.dcf(file = file.path(sCoursePath, "DESCRIPTION")) print(mDescContent)
From the content of the description object printed above, we can see that the information specified in the RCDesc
reference object is included in the descirption file.
Once the course skeleton is created, it has to be filled with content. In a first step, we want to provide some tools to create a set of slides to be used in the course.
Tools like knitr
and slidify
help us creating slides that can be used for our course. Package knitr
is used to compile rnoweb files into pdf documents where slidify
converts rmarkdown files to html5 pages.
The function create_slides()
is a wrapper to both tools knitr
and slidify
.
#' Create Slides For A Given Course #' #' \code{create_slides} creates slides for a given course using a specific slide engine #' #' This is a wrapper function that checks whether the required directories exist and #' that calls the slide-engine specific functions #' #' @param psSlidesName name of the slides document #' @param psCourseName name of the course #' @param psCourseWorkDir working directory for course (default getwd()) #' @param psSlideEngine slide engine, either slidify or knitr #' @examples #' create_slides(psCourseName = "rintrocourse", psCourseWorkDir = getwd(), psSlideEngine = "slidify") create_slides <- function(psSlidesName, psCourseName, psCourseWorkDir, psSlideEngine, psSlidesClass = NULL, plReplace = NULL){ ### # check that course work directory exists if (! dir.exists(psCourseWorkDir)) dir.create(psCourseWorkDir) ### # call slide-engine specific functions if (psSlideEngine == "slidify") { create_slides_slidify(psSlidesName = psSlidesName, psCourseName = psCourseName, psCourseWorkDir = psCourseWorkDir) } else if (psSlideEngine == "knitr") { create_slides_knitr(psSlidesName = psSlidesName, psCourseName = psCourseName, psCourseWorkDir = psCourseWorkDir, psSlidesClass = psSlidesClass, plReplace = plReplace) } else { cat(" *** ERROR in create_slides: slide engine: ", psSlideEngine, " not implemented\n") } }
The functions for the specific engines have to be implemented first, before we can create our first set of slides.
#' Create Slides Using Slidify #' #' \code{create_slides_slidify} creates a slide document for a given course using slidify #' #' @param psSlidesName name of the slides document #' @param psCourseName name of the course #' @param psCourseWorkDir working directory for course (default getwd()) create_slides_slidify <- function(psSlidesName, psCourseName, psCourseWorkDir = getwd()){ ### # put together path for slides sSlidesPath <- file.path(psCourseWorkDir, psCourseName, "vignettes", psSlidesName) ### # use slidify::author to come up with a skeleton document slidify::author(deckdir = sSlidesPath, open_rmd = FALSE) }
The same function for slide-engine knitr
looks as follows
#' Create a set of slides using knitr as conversion engine #' #' The target path where the created set of slides is created #' is determined by the course work directory, the name of the course #' and the file name which should be given to the slides. #' Based on the slide class name, a set of template files is #' chosen wich is copied into the slides path. In the last step #' the place-holders in the template are replaced by specific #' information that is specified. create_slides_knitr <- function(psSlidesName, psCourseName, psCourseWorkDir, psSlidesClass = "beamer", psTemplateDir = file.path(psCourseWorkDir, psCourseName, "inst/extdata/templates"), plReplace = NULL){ ### # put together path for slides sSlidesPath <- file.path(psCourseWorkDir, psCourseName, "vignettes") if (!dir.exists(sSlidesPath)) dir.create(sSlidesPath, recursive = TRUE) ### # copy tex files from template directory based on slide class copy_template_files(psSlidesPath = sSlidesPath, psSlidesClass = psSlidesClass, psTemplateDir = psTemplateDir) ### # in case a replacement list != null is specified, we do ### # replacement and renaming in on function, otherwise, ### # we just rename the template if (!is.null(plReplace)) { ### # replace placeholders with sepcific information replace_place_holders(psSlidesName = psSlidesName, psSlidesPath = sSlidesPath, psSlidesClass = psSlidesClass, plReplace = plReplace) } else { ### # no replacement list specified, just ### # rename template source files in copied slide-path rename_template_files(psSlidesName = psSlidesName, psSlidesPath = sSlidesPath, psSlidesClass = psSlidesClass) } cat(" * Set of knitr slides created under: ", sSlidesPath, "\n", " * Slides can be modified by opening project: ", psCourseName, " and running \n", " > file.edit(\"vignettes/", paste(psSlidesName, ".rnw", sep = ""), "\")\n", sep = "") invisible() } #' Copy template source and class files based on psSlidesClass to slides directory #' #' @param psSlidesPath target path of slides source file #' @param psSlidesClass latex class of slides #' @param psTemplateDir template directory where templates of sources are stored copy_template_files <- function(psSlidesPath, psSlidesClass, psTemplateDir) { sTemplatePath <- file.path(psTemplateDir, psSlidesClass) sTemplateFiles <- list.files(path = sTemplatePath) for (f in sTemplateFiles) { file.copy(from = file.path(sTemplatePath, f ), to = psSlidesPath, recursive = TRUE, copy.mode = TRUE) } } #' Renames the tex/rnw source template files to match the specified file name for the slides #' #' @param psSlidesName the final file name of the slides #' @param psSlidesPath path to the slides target directory #' @param psSlidesClass name of the utilsed latex document class rename_template_files <- function(psSlidesName, psSlidesPath, psSlidesClass) { file.rename(from = file.path(psSlidesPath, paste(psSlidesClass, ".rnw", sep = "")), to = file.path(psSlidesPath, paste(psSlidesName, ".rnw", sep = ""))) } #' Replace placeholders with sepcific information given in replacement list #' #' Template source files contain placeholders of the format [REPLACE_WITH_<TAG>] where #' tag is a unique identifier that is used as name in the replacement list. The #' replacement list has the tags as names and as values the string that should #' replace the placeholder. In a loop over the replacement list entries, the #' placeholders are constructed from the tags which are the names in the replacement #' list and the placeholders are replaced by the values in the replacement list. #' #' @param psSlidesName Name of the slides document #' @param psSlidesPath Path to where the slides are stored #' @param psSlidesClass Name of the latex document class used for the slides #' @param plReplace replacement list that maps placeholders to values in the final slides document replace_place_holders <- function(psSlidesName, psSlidesPath, psSlidesClass, plReplace) { ### # read slides source file into character vector sSlideSourceFile <- file.path(psSlidesPath, paste(psSlidesClass, ".rnw", sep = "")) conSlideSource <- file(description = sSlideSourceFile) sSlidesSource <- readLines(con = conSlideSource) close(con = conSlideSource) ### # original template file is not needed anymore file.remove(sSlideSourceFile) ### # loop over entries in replacement list and do the substitution for (sCurReplName in names(plReplace)) { sCurPat <- paste("[REPLACE_WITH_", sCurReplName, "]", sep = "", collapse = "") sSlidesSource <- gsub(sCurPat, plReplace[[sCurReplName]], sSlidesSource, fixed = TRUE) } ### # output file sOutFile <- file.path(psSlidesPath, paste(psSlidesName, ".rnw", sep = "")) cat(sSlidesSource, file = sOutFile, sep = "\n") cat(" * Placeholders replaced\n") invisible() }
Now we are ready to create our first set of slides.
create_slides(psSlidesName = "IntroductionToR", psCourseName = sCourseName, psCourseWorkDir = sCourseWorkDir, psSlideEngine = "slidify")
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.