├── .gitattributes ├── .github ├── .gitignore └── workflows │ ├── pkgdown.yaml │ └── R-CMD-check.yaml ├── tests ├── testthat │ ├── embeddedfiles │ │ ├── tester.txt │ │ ├── img │ │ │ └── testfile2.txt │ │ └── hw00-main.Rmd │ ├── test-assignr.R │ └── solormd │ │ └── hw00-main.Rmd └── testthat.R ├── .gitignore ├── _pkgdown.yml ├── tools └── readme │ ├── assignr-soln-pdf.png │ └── assignr-assign-pdf.png ├── .Rbuildignore ├── NAMESPACE ├── R ├── assignr-package.R └── assignr.R ├── NEWS.md ├── assignr.Rproj ├── man ├── get_example_filepath.Rd ├── assignr-package.Rd └── assignr.Rd ├── inst └── example_rmd │ └── hw00-main.Rmd ├── DESCRIPTION └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /tests/testthat/embeddedfiles/tester.txt: -------------------------------------------------------------------------------- 1 | ba 2 | -------------------------------------------------------------------------------- /tests/testthat/embeddedfiles/img/testfile2.txt: -------------------------------------------------------------------------------- 1 | testing 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .RData 4 | .Ruserdata 5 | docs 6 | -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | library(testthat) 2 | library(assignr) 3 | 4 | test_check("assignr") 5 | -------------------------------------------------------------------------------- /_pkgdown.yml: -------------------------------------------------------------------------------- 1 | url: https://r-pkg.thecoatlessprofessor.com/assignr/ 2 | template: 3 | bootstrap: 5 4 | 5 | -------------------------------------------------------------------------------- /tools/readme/assignr-soln-pdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coatless-rpkg/assignr/HEAD/tools/readme/assignr-soln-pdf.png -------------------------------------------------------------------------------- /tools/readme/assignr-assign-pdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coatless-rpkg/assignr/HEAD/tools/readme/assignr-assign-pdf.png -------------------------------------------------------------------------------- /tests/testthat/test-assignr.R: -------------------------------------------------------------------------------- 1 | test_that("test solo rmd", { 2 | expect_equal(2 * 2, 4) 3 | }) 4 | 5 | test_that("test file embed", { 6 | expect_equal(2 * 2, 4) 7 | }) 8 | -------------------------------------------------------------------------------- /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^.*\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^README\.Rmd$ 4 | ^README-.*\.png$ 5 | ^\.travis\.yml$ 6 | ^docs/ 7 | ^codecov\.yml$ 8 | ^\.github$ 9 | ^_pkgdown\.yml$ 10 | ^docs$ 11 | ^pkgdown$ 12 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | export(assignr) 4 | export(get_example_filepath) 5 | importFrom(purrr,flatten_int) 6 | importFrom(purrr,map2) 7 | importFrom(rmarkdown,render) 8 | importFrom(stringr,str_which) 9 | importFrom(utils,zip) 10 | -------------------------------------------------------------------------------- /R/assignr-package.R: -------------------------------------------------------------------------------- 1 | #' @aliases assignr-package 2 | #' @keywords internal 3 | "_PACKAGE" 4 | 5 | # The following block is used by usethis to automatically manage 6 | # roxygen namespace tags. Modify with care! 7 | ## usethis namespace: start 8 | ## usethis namespace: end 9 | NULL 10 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # assignr 0.0.1 2 | 3 | ## Features 4 | 5 | - Automatically generate a solution key and an assignment from asingle `.Rmd` 6 | file via `assignr("hwXX-main.Rmd")`. 7 | - Compress individual the solution key and assignment files via a zip. 8 | - Automatically deploy to a [GitHub Pages](https://pages.github.com/) 9 | website with the default output directory set to `/docs`. 10 | 11 | -------------------------------------------------------------------------------- /assignr.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: No 4 | SaveWorkspace: No 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: knitr 13 | LaTeX: pdfLaTeX 14 | 15 | AutoAppendNewline: Yes 16 | StripTrailingWhitespace: Yes 17 | 18 | BuildType: Package 19 | PackageUseDevtools: Yes 20 | PackageInstallArgs: --no-multiarch --with-keep.source 21 | PackageRoxygenize: rd,collate,namespace 22 | -------------------------------------------------------------------------------- /man/get_example_filepath.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/assignr.R 3 | \name{get_example_filepath} 4 | \alias{get_example_filepath} 5 | \title{Retrieve example file path} 6 | \usage{ 7 | get_example_filepath(x) 8 | } 9 | \arguments{ 10 | \item{x}{A \code{character} containing the name of the example Rmd.} 11 | } 12 | \value{ 13 | File path to the example file that ships with the package. 14 | } 15 | \description{ 16 | Obtains the file path for the example Rmd in the package. 17 | } 18 | \details{ 19 | The following example files ship with the package: 20 | \itemize{ 21 | \item hw00-main.Rmd 22 | } 23 | } 24 | \examples{ 25 | get_example_filepath("hw00-main.Rmd") 26 | } 27 | -------------------------------------------------------------------------------- /inst/example_rmd/hw00-main.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Homework X' 3 | author: "Prof. Name" 4 | date: 'Due: Friday, Month Day by 1:59 PM CDT' 5 | output: 6 | html_document: 7 | theme: readable 8 | toc: yes 9 | --- 10 | 11 | # Exercise 1 (Introductory `R`) 12 | 13 | ```{asis, directions = TRUE} 14 | For this exercise, we will create a couple different vectors. 15 | ``` 16 | 17 | **(a)** Create two vectors `x0` and `x1`. Each should have a 18 | length of 25 and store the following: 19 | 20 | - `x0`: Each element should be the value `10`. 21 | - `x1`: The first 25 cubed numbers, starting from `1.` (e.g. `1`, `8`, `27`, et cetera) 22 | 23 | ```{asis, solution = TRUE} 24 | **Solution:** 25 | ``` 26 | 27 | ```{r, solution = TRUE} 28 | x0 = rep(10, 25) 29 | x1 = (1:25) ^ 3 30 | ``` 31 | 32 | -------------------------------------------------------------------------------- /tests/testthat/solormd/hw00-main.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Homework X' 3 | author: "Prof. Name" 4 | date: 'Due: Friday, Month Day by 1:59 PM CDT' 5 | output: 6 | html_document: 7 | theme: readable 8 | toc: yes 9 | --- 10 | 11 | # Exercise 1 (Introductory `R`) 12 | 13 | ```{asis, directions = TRUE} 14 | For this exercise, we will create a couple different vectors. 15 | ``` 16 | 17 | **(a)** Create two vectors `x0` and `x1`. Each should have a 18 | length of 25 and store the following: 19 | 20 | - `x0`: Each element should be the value `10`. 21 | - `x1`: The first 25 cubed numbers, starting from `1.` (e.g. `1`, `8`, `27`, et cetera) 22 | 23 | ```{asis, solution = TRUE} 24 | **Solution:** 25 | ``` 26 | 27 | ```{r, solution = TRUE} 28 | x0 = rep(10, 25) 29 | x1 = (1:25) ^ 3 30 | ``` 31 | 32 | -------------------------------------------------------------------------------- /tests/testthat/embeddedfiles/hw00-main.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Homework X' 3 | author: "Prof. Name" 4 | date: 'Due: Friday, Month Day by 1:59 PM CDT' 5 | output: 6 | html_document: 7 | theme: readable 8 | toc: yes 9 | --- 10 | 11 | # Exercise 1 (Introductory `R`) 12 | 13 | ```{asis, directions = TRUE} 14 | For this exercise, we will create a couple different vectors. 15 | ``` 16 | 17 | **(a)** Create two vectors `x0` and `x1`. Each should have a 18 | length of 25 and store the following: 19 | 20 | - `x0`: Each element should be the value `10`. 21 | - `x1`: The first 25 cubed numbers, starting from `1.` (e.g. `1`, `8`, `27`, et cetera) 22 | 23 | ```{asis, solution = TRUE} 24 | **Solution:** 25 | ``` 26 | 27 | ```{r, solution = TRUE} 28 | x0 = rep(10, 25) 29 | x1 = (1:25) ^ 3 30 | ``` 31 | 32 | -------------------------------------------------------------------------------- /man/assignr-package.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/assignr-package.R 3 | \docType{package} 4 | \name{assignr-package} 5 | \alias{assignr-package} 6 | \alias{_PACKAGE} 7 | \title{assignr: Create Homework Assignments and Solutions using 'RMarkdown'} 8 | \description{ 9 | Writing homework assignments for students in the age of 'RMarkdown' necessitates the creation of two separate documents -- assign.Rmd and soln.Rmd. The goal of assignr is to create one document main.Rmd that can be broken apart into the above two documents. Thus, there is no longer a need to copy and paste between the assign and the soln documents as all of the contents are together in one file. 10 | } 11 | \seealso{ 12 | Useful links: 13 | \itemize{ 14 | \item \url{https://github.com/coatless-rpkg/assignr} 15 | \item \url{https://r-pkg.thecoatlessprofessor.com/assignr/} 16 | \item Report bugs at \url{https://github.com/coatless-rpkg/assignr/issues} 17 | } 18 | 19 | } 20 | \author{ 21 | \strong{Maintainer}: James Balamuta \email{balamut2@illinois.edu} [copyright holder] 22 | 23 | Authors: 24 | \itemize{ 25 | \item David Dalpiaz \email{dalpiaz2@illinois.edu} [copyright holder] 26 | } 27 | 28 | } 29 | \keyword{internal} 30 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: assignr 2 | Title: Create Homework Assignments and Solutions using 'RMarkdown' 3 | Version: 0.0.1 4 | Authors@R: c(person("David", "Dalpiaz", email = "dalpiaz2@illinois.edu", 5 | role = c("aut", "cph")), 6 | person("James", "Balamuta", email = "balamut2@illinois.edu", 7 | role = c("aut", "cre", "cph"))) 8 | Description: Writing homework assignments for students in the age of 'RMarkdown' 9 | necessitates the creation of two separate documents -- assign.Rmd and soln.Rmd. 10 | The goal of assignr is to create one document main.Rmd that can be broken 11 | apart into the above two documents. Thus, there is no longer a need to copy 12 | and paste between the assign and the soln documents as all of the contents 13 | are together in one file. 14 | Depends: 15 | R (>= 3.3.0) 16 | URL: https://github.com/coatless-rpkg/assignr, https://r-pkg.thecoatlessprofessor.com/assignr/ 17 | BugReports: https://github.com/coatless-rpkg/assignr/issues 18 | License: GPL (>= 2) 19 | Encoding: UTF-8 20 | LazyData: true 21 | Imports: rmarkdown, 22 | stringr, 23 | purrr 24 | Suggests: 25 | testthat, 26 | knitr, 27 | covr 28 | VignetteBuilder: knitr 29 | RoxygenNote: 7.2.3 30 | Roxygen: list(markdown = TRUE) 31 | -------------------------------------------------------------------------------- /.github/workflows/pkgdown.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | release: 9 | types: [published] 10 | workflow_dispatch: 11 | 12 | name: pkgdown 13 | 14 | jobs: 15 | pkgdown: 16 | runs-on: ubuntu-latest 17 | # Only restrict concurrency for non-PR jobs 18 | concurrency: 19 | group: pkgdown-${{ github.event_name != 'pull_request' || github.run_id }} 20 | env: 21 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 22 | permissions: 23 | contents: write 24 | steps: 25 | - uses: actions/checkout@v3 26 | 27 | - uses: r-lib/actions/setup-pandoc@v2 28 | 29 | - uses: r-lib/actions/setup-r@v2 30 | with: 31 | use-public-rspm: true 32 | 33 | - uses: r-lib/actions/setup-tinytex@v2 34 | 35 | - uses: r-lib/actions/setup-r-dependencies@v2 36 | with: 37 | extra-packages: any::pkgdown, local::. 38 | needs: website 39 | 40 | - name: Build site 41 | run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE) 42 | shell: Rscript {0} 43 | 44 | - name: Deploy to GitHub pages 🚀 45 | if: github.event_name != 'pull_request' 46 | uses: JamesIves/github-pages-deploy-action@v4.4.1 47 | with: 48 | clean: false 49 | branch: gh-pages 50 | folder: docs 51 | -------------------------------------------------------------------------------- /.github/workflows/R-CMD-check.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/master/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | 9 | name: R-CMD-check 10 | 11 | jobs: 12 | R-CMD-check: 13 | runs-on: ${{ matrix.config.os }} 14 | 15 | name: ${{ matrix.config.os }} (${{ matrix.config.r }}) 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | config: 21 | - {os: macOS-latest, r: 'release'} 22 | - {os: windows-latest, r: 'release'} 23 | - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} 24 | - {os: ubuntu-latest, r: 'release'} 25 | - {os: ubuntu-latest, r: 'oldrel-1'} 26 | 27 | env: 28 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 29 | R_KEEP_PKG_SOURCE: yes 30 | 31 | steps: 32 | - uses: actions/checkout@v2 33 | # Required for PDF build 34 | - uses: r-lib/actions/setup-tinytex@v2 35 | 36 | - uses: r-lib/actions/setup-pandoc@v2 37 | 38 | - uses: r-lib/actions/setup-r@v2 39 | with: 40 | r-version: ${{ matrix.config.r }} 41 | http-user-agent: ${{ matrix.config.http-user-agent }} 42 | use-public-rspm: true 43 | 44 | - uses: r-lib/actions/setup-r-dependencies@v2 45 | with: 46 | extra-packages: any::rcmdcheck 47 | needs: check 48 | 49 | - uses: r-lib/actions/check-r-package@v2 50 | -------------------------------------------------------------------------------- /man/assignr.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/assignr.R 3 | \name{assignr} 4 | \alias{assignr} 5 | \title{Create Homework and Assignment Materials} 6 | \usage{ 7 | assignr( 8 | file, 9 | output_dir = NULL, 10 | output_format = c("html_document", "pdf_document"), 11 | assign_file = TRUE, 12 | soln_file = TRUE, 13 | zip_files = TRUE, 14 | render_files = TRUE 15 | ) 16 | } 17 | \arguments{ 18 | \item{file}{Input \code{.Rmd} file with \code{-main.Rmd} in the filename.} 19 | 20 | \item{output_dir}{Output directory. Defaults to name of prefix of filename.} 21 | 22 | \item{output_format}{Output file type. Any \code{\link[rmarkdown:render]{rmarkdown::render()}} output 23 | format should work. 24 | Defaults to generating both an HTML and PDF output with 25 | \code{c("html_document", "pdf_document")}.} 26 | 27 | \item{assign_file}{Generate Student Assignment Material. Default is \code{TRUE}.} 28 | 29 | \item{soln_file}{Generate Solution Material. Default is \code{TRUE}.} 30 | 31 | \item{zip_files}{Create a zip file containing the relevant materials. 32 | Default is \code{TRUE}.} 33 | 34 | \item{render_files}{Create HTML and PDF output for each Rmd file. 35 | Default is \code{TRUE}.} 36 | } 37 | \value{ 38 | The function will generate assignment files for students and 39 | solution keys for instructors. 40 | } 41 | \description{ 42 | Transforms an RMarkdown file into two separate files: \code{filename-assign} 43 | and \code{filename-solutions} 44 | } 45 | \details{ 46 | The \code{file} parameter \emph{must} have the suffix \code{-main.Rmd}. The reason for 47 | requiring this naming scheme is all work should be done in the "main" 48 | document. By enforcing the naming requirement, we are prevent work from 49 | being overridden. 50 | } 51 | \section{Folder structure}{ 52 | 53 | If \code{output_dir} is specified, then it will be used as the parent 54 | for two folders: \verb{*-assign} and \verb{*-sol}, where \code{*} is given by the part 55 | preceeding \code{-main.Rmd}. Inside the folders, there will be \code{html}, \code{pdf}, 56 | and \code{Rmd} documents alongside a \code{zip} a folder containing all of the 57 | documents. 58 | } 59 | 60 | \examples{ 61 | # Obtain an example file 62 | hw00_file = get_example_filepath("hw00-main.Rmd") 63 | 64 | if(interactive()) { 65 | file.show(hw00_file) 66 | } 67 | 68 | # Generate both PDF and HTML outputs for assign and solution. 69 | assignr(hw00_file, "test") 70 | 71 | # Generate only the assignment 72 | assignr(hw00_file, "assignment-set", soln_file = FALSE) 73 | 74 | # Generate only the solution 75 | assignr(hw00_file, "solution-set", assign_file = FALSE) 76 | 77 | # Create only HTML documents for both assignment and solution files. 78 | assignr(hw00_file, "test-html", output_format = "html_document") 79 | 80 | \dontshow{ 81 | # Clean up generated directories 82 | unlink("test", recursive = TRUE) 83 | unlink("assignment-set", recursive = TRUE) 84 | unlink("solution-set", recursive = TRUE) 85 | unlink("test-html", recursive = TRUE) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # assignr 2 | 3 | 4 | [![R build status](https://github.com/coatless-rpkg/assignr/workflows/R-CMD-check/badge.svg)](https://github.com/coatless-rpkg/assignr/actions) 5 | [![Package-License](http://img.shields.io/badge/license-GPL%20(%3E=2)-brightgreen.svg?style=flat)](http://www.gnu.org/licenses/gpl-2.0.html) 6 | 7 | 8 | Tools for creating homework assignments and solutions using [_RMarkdown_](http://rmarkdown.rstudio.com/). 9 | 10 | ## Motivation 11 | 12 | Writing homework assignments for students in the age of [_RMarkdown_](http://rmarkdown.rstudio.com/) necessitates the creation 13 | of _two_ separate documents -- `assign.Rmd` and `soln.Rmd`. 14 | The goal of `assignr` is to create one document `main.Rmd` that can be broken 15 | apart into the above two documents. Thus, there is no longer a need to 16 | _copy and paste_ between the `assign` and the `soln` documents as all of the 17 | contents are together in one file. 18 | 19 | ![Example workflow with `assignr`](https://media.giphy.com/media/l2QEaOm8vqHYG2aNG/giphy.gif) 20 | 21 | ## Installation 22 | 23 | `assignr` is only available via GitHub. 24 | 25 | To install the package from GitHub, you can type: 26 | 27 | ```r 28 | install.packages("devtools") 29 | 30 | devtools::install_github("coatless-rpkg/assignr") 31 | ``` 32 | 33 | ## Usage 34 | 35 | To use `assignr`, create an Rmd file named `WXYZ-main.Rmd`, where `WXYZ` 36 | could be `hw00`. Within the file, add to your code chunks one of the following 37 | chunk options: 38 | 39 | 1. `solution = TRUE` 40 | - to mark a solution 41 | 2. `directions = TRUE` 42 | - to indicate directions 43 | 44 | When specifying text directions that should not appear in the solution file, use 45 | the latter chunk option, and set the code chunk engine to `asis`, e.g. 46 | 47 | ```` 48 | ```{asis name, directions = TRUE} 49 | The goal of the following exercise is... 50 | ``` 51 | ```` 52 | 53 | Then, in _R_, run: 54 | 55 | ```r 56 | # Render output 57 | assignr("hw00-main.Rmd") 58 | ``` 59 | 60 | Example Output: 61 | 62 | ``` 63 | hw00 64 | ├── hw00-assign 65 | │ ├── hw00-assign.Rmd 66 | │ ├── hw00-assign.html 67 | │ ├── hw00-assign.pdf 68 | │ └── hw00-assign.zip 69 | └── hw00-soln 70 | ├── hw00-soln.Rmd 71 | ├── hw00-soln.html 72 | ├── hw00-soln.pdf 73 | └── hw00-soln.zip 74 | ``` 75 | 76 | Previews of what is contained in each file are shown next. 77 | 78 | ### Instructor Main 79 | 80 | In the main file, denoted as `*-main.Rmd`, all content -- including solutions -- 81 | should be placed. As an example of contents, please see the `hw00-main.Rmd` 82 | document that ships with the package. 83 | 84 | ```r 85 | library("assignr") 86 | 87 | file.show(get_example_filepath("hw00-main.Rmd")) 88 | ``` 89 | 90 | ```` 91 | --- 92 | title: 'Homework X' 93 | author: "Prof. Name" 94 | date: 'Due: Friday, Month Day by 1:59 PM CDT' 95 | output: 96 | html_document: 97 | theme: readable 98 | toc: yes 99 | --- 100 | 101 | # Exercise 1 (Introductory `R`) 102 | 103 | ```{asis, directions = TRUE} 104 | For this exercise, we will create a couple different vectors. 105 | ``` 106 | 107 | **(a)** Create two vectors `x0` and `x1`. Each should have a 108 | length of 25 and store the following: 109 | 110 | - `x0`: Each element should be the value `10`. 111 | - `x1`: The first 25 cubed numbers, starting from `1.` (e.g. `1`, `8`, `27`, et cetera) 112 | 113 | ```{asis, solution = TRUE} 114 | **Solution:** 115 | ``` 116 | 117 | ```{r, solution = TRUE} 118 | x0 = rep(10, 25) 119 | x1 = (1:25) ^ 3 120 | ``` 121 | ```` 122 | 123 | ### Student Assignment 124 | 125 | Within this section, the assignment rmarkdown file given to students is displayed. 126 | 127 | ```` 128 | --- 129 | title: 'Homework X' 130 | author: "Prof. Name" 131 | date: 'Due: Friday, Month Day by 1:59 PM CDT' 132 | output: 133 | html_document: 134 | theme: readable 135 | toc: yes 136 | --- 137 | 138 | # Exercise 1 (Introductory `R`) 139 | 140 | For this exercise, we will create a couple different vectors. 141 | 142 | **(a)** Create two vectors `x0` and `x1`. Each should have a 143 | length of 25 and store the following: 144 | 145 | - `x0`: Each element should be the value `10`. 146 | - `x1`: The first 25 cubed numbers, starting from `1.` (e.g. `1`, `8`, `27`, et cetera) 147 | ```` 148 | 149 | ![PDF Rendering of `hw00-assign.Rmd`](tools/readme/assignr-assign-pdf.png) 150 | 151 | ### Solutions 152 | 153 | Lastly, we have the assignment rmarkdown file that contains the solutions 154 | and their respective output. 155 | 156 | ```` 157 | --- 158 | title: 'Homework X' 159 | author: "Prof. Name" 160 | date: 'Due: Friday, Month Day by 1:59 PM CDT' 161 | output: 162 | html_document: 163 | theme: readable 164 | toc: yes 165 | --- 166 | 167 | # Exercise 1 (Introductory `R`) 168 | 169 | 170 | **(a)** Create two vectors `x0` and `x1`. Each should have a 171 | length of 25 and store the following: 172 | 173 | - `x0`: Each element should be the value `10`. 174 | - `x1`: The first 25 cubed numbers, starting from `1.` (e.g. `1`, `8`, `27`, et cetera) 175 | 176 | **Solution:** 177 | 178 | ```{r, solution = TRUE} 179 | x0 = rep(10, 25) 180 | x1 = (1:25) ^ 3 181 | ``` 182 | ```` 183 | 184 | ![PDF Rendering of `hw00-soln.Rmd`](tools/readme/assignr-soln-pdf.png) 185 | 186 | ## Authors 187 | 188 | James Joseph Balamuta and David Dalpiaz 189 | 190 | ## License 191 | 192 | GPL (>= 2) 193 | -------------------------------------------------------------------------------- /R/assignr.R: -------------------------------------------------------------------------------- 1 | 2 | #' @importFrom stringr str_which 3 | detect_positions = function(x, value) { 4 | stringr::str_which(x, value) 5 | } 6 | 7 | find_matching_regions = function(x, value, chunk_lines) { 8 | # find start of chunks that contain value 9 | indices_start = detect_positions(x, value) 10 | 11 | # find the chunk end 12 | indices_end = chunk_lines[which(chunk_lines %in% indices_start) + 1] 13 | 14 | # Kill the process if it is not formatted correctly. 15 | if(length(indices_end) >= 1 && any(x[indices_end] != "```")) { 16 | stop("Code chunks not found at the end of code chunk on lines:", 17 | paste0(which(x[indices_end] != ""), collapse=",")) 18 | } 19 | 20 | # Return as a list 21 | list("indices_start" = indices_start, 22 | "indices_end" = indices_end) 23 | } 24 | 25 | # Flatten regions into a numeric vector 26 | condense_regions = function(regions) { 27 | c(regions$indices_start, regions$indices_end) 28 | } 29 | 30 | #' @importFrom purrr map2 flatten_int 31 | chunk_line_ranges = function(regions) { 32 | # Get line indices from the start to the end (e.g. the range) 33 | flatten_int(map2(regions$indices_start, regions$indices_end, ~ seq(.x, .y))) 34 | } 35 | 36 | 37 | removal_indices = function(x, value, chunk_lines) { 38 | # Obtain the starting and ending indexes for a value 39 | regions = find_matching_regions(x, value, chunk_lines) 40 | 41 | # Generate the range from start to end 42 | chunk_line_ranges(regions) 43 | } 44 | 45 | 46 | #' @importFrom rmarkdown render 47 | #' @importFrom utils zip 48 | generate_hw_pkg = function(x, 49 | remove_indexes, 50 | name, 51 | type, 52 | output_dir = paste0(name, "-", type), 53 | output_format = c("html_document", "pdf_document"), 54 | render_files = TRUE, 55 | zip_files = TRUE, 56 | hw_directory = '', 57 | file_dependencies = character(0)) { 58 | 59 | if (length(remove_indexes) > 0) { 60 | # create assignment output lines 61 | x = x[-remove_indexes] 62 | } 63 | 64 | output_name = paste0(name, "-", type) 65 | output_path = file.path(output_dir, output_name) 66 | 67 | # Remove the output directory 68 | if (dir.exists(output_path)) { 69 | unlink(output_path, recursive = TRUE) 70 | } 71 | 72 | # Make the directory 73 | dir.create(output_path, recursive = TRUE) 74 | 75 | # Fill directory 76 | if(length(file_dependencies) >= 1) { 77 | 78 | # Create any required directories for the copy 79 | for(dir_dependent in unique(dirname(file_dependencies))) { 80 | dir.create(file.path(output_path, dir_dependent), 81 | showWarnings = FALSE, recursive = TRUE) 82 | } 83 | 84 | # Perform a vectorized copy to the appropriate directory 85 | file.copy(file.path(hw_directory, file_dependencies), 86 | file.path(output_path, file_dependencies)) 87 | } 88 | 89 | # Name of Rmd file to build 90 | rmd_material_name = file.path(output_path, 91 | paste0(output_name, ".Rmd")) 92 | 93 | message("Building ", output_name, " files") 94 | 95 | # write to .Rmd, then render as html and pdf 96 | writeLines(x, rmd_material_name) 97 | 98 | if (render_files) { 99 | rmarkdown::render( 100 | rmd_material_name, 101 | encoding = "UTF-8", 102 | envir = new.env(), 103 | output_format = output_format, 104 | quiet = TRUE 105 | ) 106 | } 107 | 108 | if (zip_files) { 109 | message("Creating a zip file for ", output_name) 110 | 111 | # Zip file together in the output directory 112 | zip(file.path(output_path, output_name), 113 | list.files(output_path, full.names = TRUE), 114 | flags = "-r9XqT") # suppress zip information 115 | } 116 | } 117 | 118 | 119 | extract_hw_name = function(x) { 120 | stringr::str_replace(basename(x), 121 | "-.*", "") 122 | 123 | } 124 | 125 | hw_dir_dependencies = function(hw_directory) { 126 | # Move to where the file might be found 127 | old_wd = setwd(file.path(hw_directory)) 128 | 129 | # Determine all files and directories within the homework directory 130 | main_dir_files = list.files(path = ".", full.names = TRUE, recursive = TRUE) 131 | 132 | # Avoid retrieving any file matching our exclusion list 133 | hw_dependencies = grep(main_dir_files, pattern = '-(main|assign|sol)', 134 | invert = TRUE, value = TRUE) 135 | 136 | # Return to original working directory 137 | setwd(old_wd) 138 | 139 | # Release files 140 | hw_dependencies 141 | } 142 | 143 | #' Retrieve example file path 144 | #' 145 | #' Obtains the file path for the example Rmd in the package. 146 | #' @param x A `character` containing the name of the example Rmd. 147 | #' 148 | #' @return File path to the example file that ships with the package. 149 | #' @details 150 | #' The following example files ship with the package: 151 | #' - hw00-main.Rmd 152 | #' @export 153 | #' @examples 154 | #' get_example_filepath("hw00-main.Rmd") 155 | get_example_filepath = function(x) { 156 | fp_example = system.file( "example_rmd" , x , package = "assignr") 157 | 158 | if(!file.exists(fp_example)) { 159 | stop("Not a valid file path for an example Rmd.") 160 | } 161 | 162 | fp_example 163 | } 164 | 165 | 166 | #' Create Homework and Assignment Materials 167 | #' 168 | #' Transforms an RMarkdown file into two separate files: `filename-assign` 169 | #' and `filename-solutions` 170 | #' 171 | #' @param file Input `.Rmd` file with `-main.Rmd` in the filename. 172 | #' @param output_dir Output directory. Defaults to name of prefix of filename. 173 | #' @param output_format Output file type. Any [rmarkdown::render()] output 174 | #' format should work. 175 | #' Defaults to generating both an HTML and PDF output with 176 | #' `c("html_document", "pdf_document")`. 177 | #' @param soln_file Generate Solution Material. Default is `TRUE`. 178 | #' @param assign_file Generate Student Assignment Material. Default is `TRUE`. 179 | #' @param zip_files Create a zip file containing the relevant materials. 180 | #' Default is `TRUE`. 181 | #' @param render_files Create HTML and PDF output for each Rmd file. 182 | #' Default is `TRUE`. 183 | #' @export 184 | #' @return The function will generate assignment files for students and 185 | #' solution keys for instructors. 186 | #' 187 | #' @details 188 | #' The `file` parameter _must_ have the suffix `-main.Rmd`. The reason for 189 | #' requiring this naming scheme is all work should be done in the "main" 190 | #' document. By enforcing the naming requirement, we are prevent work from 191 | #' being overridden. 192 | #' 193 | #' @section Folder structure: 194 | #' If `output_dir` is specified, then it will be used as the parent 195 | #' for two folders: `*-assign` and `*-sol`, where `*` is given by the part 196 | #' preceeding `-main.Rmd`. Inside the folders, there will be `html`, `pdf`, 197 | #' and `Rmd` documents alongside a `zip` a folder containing all of the 198 | #' documents. 199 | #' 200 | #' @examples 201 | #' # Obtain an example file 202 | #' hw00_file = get_example_filepath("hw00-main.Rmd") 203 | #' 204 | #' if(interactive()) { 205 | #' file.show(hw00_file) 206 | #' } 207 | #' 208 | #' # Generate both PDF and HTML outputs for assign and solution. 209 | #' assignr(hw00_file, "test") 210 | #' 211 | #' # Generate only the assignment 212 | #' assignr(hw00_file, "assignment-set", soln_file = FALSE) 213 | #' 214 | #' # Generate only the solution 215 | #' assignr(hw00_file, "solution-set", assign_file = FALSE) 216 | #' 217 | #' # Create only HTML documents for both assignment and solution files. 218 | #' assignr(hw00_file, "test-html", output_format = "html_document") 219 | #' 220 | #' \dontshow{ 221 | #' # Clean up generated directories 222 | #' unlink("test", recursive = TRUE) 223 | #' unlink("assignment-set", recursive = TRUE) 224 | #' unlink("solution-set", recursive = TRUE) 225 | #' unlink("test-html", recursive = TRUE) 226 | #' } 227 | assignr = function(file, 228 | output_dir = NULL, 229 | output_format = c("html_document", "pdf_document"), 230 | assign_file = TRUE, 231 | soln_file = TRUE, 232 | zip_files = TRUE, 233 | render_files = TRUE) { 234 | 235 | # Minimal conditions for processing. 236 | if (length(file) != 1) { 237 | stop("Only one file may be processed at time.") 238 | } else if (!grep( "-main.Rmd$", file)) { 239 | stop("Supplied file must have -main.Rmd") 240 | } 241 | 242 | # Retrieve value before -main.Rmd 243 | hw_name = extract_hw_name(file) 244 | 245 | # Obtain location of the homework directory 246 | hw_directory = dirname(file) 247 | 248 | # Extract a local file structure 249 | hw_dependency_files = hw_dir_dependencies(hw_directory) 250 | 251 | # Begin processing chunks 252 | input_lines = readLines(file) 253 | 254 | chunk_tick_lines = detect_positions(input_lines, "```") 255 | 256 | solution_indexes = removal_indices( 257 | input_lines, 258 | "solution[[:space:]]?=[[:space:]]?[tT]?[rR]?[uU]?[eE]?", 259 | chunk_tick_lines 260 | ) 261 | 262 | direction_regions = find_matching_regions( 263 | input_lines, 264 | "directions[[:space:]]?=[[:space:]]?[Tt]?[rR]?[uU]?[eE]?", 265 | chunk_tick_lines 266 | ) 267 | 268 | # Retains direction text 269 | direction_chunk_indices = condense_regions(direction_regions) 270 | 271 | # Deletes direction text 272 | direction_regions_range = chunk_line_ranges(direction_regions) 273 | 274 | 275 | asis_indexes = condense_regions(find_matching_regions(input_lines, 276 | "asis", 277 | chunk_tick_lines)) 278 | 279 | if (is.null(output_dir)) { 280 | output_dir = hw_name 281 | } 282 | 283 | if (assign_file) { 284 | generate_hw_pkg( 285 | x = input_lines, 286 | remove_indexes = c(solution_indexes, direction_chunk_indices), 287 | name = hw_name, 288 | type = "assign", 289 | output_dir = output_dir, 290 | output_format = output_format, 291 | render_files = render_files, 292 | zip_files = zip_files, 293 | hw_directory = hw_directory, 294 | file_dependencies = hw_dependency_files 295 | ) 296 | } 297 | 298 | if (soln_file) { 299 | generate_hw_pkg( 300 | x = input_lines, 301 | remove_indexes = c(asis_indexes, direction_regions_range), 302 | name = hw_name, 303 | type = "soln", 304 | output_dir = output_dir, 305 | output_format = output_format, 306 | render_files = render_files, 307 | zip_files = zip_files, 308 | hw_directory = hw_directory, 309 | file_dependencies = hw_dependency_files 310 | ) 311 | } 312 | 313 | } 314 | --------------------------------------------------------------------------------