├── .Rbuildignore ├── .gitattributes ├── .gitignore ├── .lintr ├── .travis.yml ├── DESCRIPTION ├── LICENSE ├── LICENSE.md ├── NAMESPACE ├── R ├── assertions.R ├── bucket_problem.R ├── css.R ├── expectations.R ├── incrementor.R ├── methods.R ├── parsons-package.R ├── parsons_problem.R ├── question_bucket.R ├── question_parsons.R └── wrap_labels.R ├── README.Rmd ├── README.md ├── _pkgdown.yml ├── codecov.yml ├── inst ├── examples │ ├── example_parsons.R │ └── example_question_parsons.R ├── htmlwidgets │ └── plugins │ │ └── parsons │ │ ├── _colors.scss │ │ ├── bucket_list.css │ │ ├── bucket_list.scss │ │ ├── parsons.css │ │ ├── parsons.scss │ │ ├── parsons_old.css │ │ ├── rank_list.css │ │ └── rank_list.scss ├── shiny-examples │ ├── problem_type │ │ └── app.R │ └── simple │ │ └── app.R └── tutorials │ ├── bucket │ └── tutorial_bucket.Rmd │ └── parsons │ ├── tutorial_parsons.Rmd │ └── tutorial_parsons.html ├── logo.svg ├── man-roxygen └── question_parsons_description.R ├── man ├── bucket_problem.Rd ├── expectations.Rd ├── figures │ ├── logo.svg │ ├── parsons-logo.png │ ├── parsons_app.gif │ ├── parsons_app_initial.png │ └── parsons_app_submit.png ├── parsons.Rd ├── parsons_problem.Rd ├── question_bucket.Rd ├── question_parsons.Rd ├── question_ui_try_again.bucket_question.Rd └── question_ui_try_again.parsons_question.Rd ├── parsons.Rproj ├── pkgdown └── favicon │ ├── apple-touch-icon-120x120.png │ ├── apple-touch-icon-152x152.png │ ├── apple-touch-icon-180x180.png │ ├── apple-touch-icon-60x60.png │ ├── apple-touch-icon-76x76.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ └── favicon.ico ├── scripts ├── compile_css.R ├── deploy_apps.R ├── deploy_on_travis.R └── load_all_shim.R ├── tests ├── testthat.R └── testthat │ ├── test-expectations.R │ ├── test-learnr-question_parsons.R │ └── test-parsons.R └── vignettes ├── .gitignore └── introduction.Rmd /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^.*\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^LICENSE\.md$ 4 | ^Readme\.md$ 5 | ^README\.Rmd$ 6 | ^\.travis\.yml$ 7 | ^\.lintr$ 8 | ^doc$ 9 | ^Meta$ 10 | ^scripts/ 11 | ^man-roxygen/ 12 | ^docs$ 13 | ^_pkgdown\.yml$ 14 | ^pkgdown$ 15 | ^codecov\.yml$ 16 | ^README\.html$ 17 | ^logo.svg$ 18 | ^inst/tutorials/parsons/rsconnect 19 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.html linguist-documentation=true 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .RData 4 | docs/ 5 | Meta 6 | README.html 7 | doc 8 | inst/tutorials/parsons/rsconnect 9 | inst/doc 10 | inst/tutorials/*/*.html 11 | -------------------------------------------------------------------------------- /.lintr: -------------------------------------------------------------------------------- 1 | linters: with_defaults( 2 | line_length_linter(100), 3 | NULL 4 | ) 5 | exclusions: list( 6 | "tests/testthat/test-htmltools.R", 7 | ) 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # R for travis: see documentation at https://docs.travis-ci.com/user/languages/r 2 | 3 | language: R 4 | cache: packages 5 | 6 | notifications: 7 | email: 8 | on_success: change 9 | on_failure: change 10 | 11 | matrix: 12 | include: 13 | - name: "Release" 14 | r: release 15 | after_success: 16 | - Rscript -e 'covr::codecov()' 17 | - find . -name testthat.Rout | xargs cat 18 | after_failure: 19 | - find . -name testthat.Rout | xargs cat 20 | before_cache: Rscript -e 'remotes::install_cran("pkgdown")' 21 | deploy: 22 | provider: script 23 | script: Rscript -e 'pkgdown::deploy_site_github()' 24 | skip_cleanup: true 25 | 26 | - name: "Old Release" 27 | r: oldrel 28 | 29 | - name: "Devel" 30 | r: devel 31 | 32 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: parsons 2 | Type: Package 3 | Title: Create Parsons Programming Problems in 'learnr` Tutorials 4 | Version: 0.0.3.9000 5 | Date: 2019-08-19 6 | Authors@R: c( 7 | person("Andrie", "de Vries", role = c("cre", "aut"), email = "apdevries@gmail.com"), 8 | person("Barret", "Schloerke", role = "ctb", email = "barret@rstudio.com") 9 | ) 10 | Description: Create Parsons Programming Problems in 'learnr' Tutorials. 11 | URL: https://github.com/rstudio/parsons 12 | BugReports: https://github.com/rstudio/parsons/issues 13 | License: MIT + file LICENSE 14 | LazyData: TRUE 15 | Imports: 16 | sortable (>= 0.3.1), 17 | htmltools, 18 | htmlwidgets, 19 | learnr (>= 0.9.2), 20 | shiny, 21 | assertthat, 22 | utils, 23 | rlang 24 | Suggests: 25 | knitr, 26 | testthat (>= 2.1.0), 27 | withr, 28 | rmarkdown 29 | Remotes: 30 | rstudio/sortable, 31 | rstudio/learnr 32 | VignetteBuilder: knitr 33 | RoxygenNote: 6.1.1 34 | Roxygen: list(markdown = TRUE) 35 | Encoding: UTF-8 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2019 2 | COPYRIGHT HOLDER: Andrie de Vries 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2019 Andrie de Vries 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | S3method(fail_if,"function") 4 | S3method(fail_if,character) 5 | S3method(fail_if,default) 6 | S3method(pass_if,"function") 7 | S3method(pass_if,character) 8 | S3method(pass_if,default) 9 | S3method(print,bucket) 10 | S3method(print,parsons) 11 | S3method(question_is_correct,bucket_question) 12 | S3method(question_is_correct,parsons_question) 13 | S3method(question_ui_completed,bucket_question) 14 | S3method(question_ui_completed,parsons_question) 15 | S3method(question_ui_initialize,bucket_question) 16 | S3method(question_ui_initialize,parsons_question) 17 | S3method(question_ui_try_again,bucket_question) 18 | S3method(question_ui_try_again,parsons_question) 19 | export(bucket_problem) 20 | export(contains_all) 21 | export(contains_any) 22 | export(fail_if) 23 | export(message_if) 24 | export(parsons_problem) 25 | export(pass_if) 26 | export(question_bucket) 27 | export(question_parsons) 28 | importFrom(assertthat,"on_failure<-") 29 | importFrom(assertthat,assert_that) 30 | importFrom(assertthat,is.string) 31 | importFrom(learnr,disable_all_tags) 32 | importFrom(learnr,mark_as) 33 | importFrom(learnr,question_is_correct) 34 | importFrom(learnr,question_is_valid) 35 | importFrom(learnr,question_ui_completed) 36 | importFrom(learnr,question_ui_initialize) 37 | importFrom(learnr,question_ui_try_again) 38 | importFrom(rlang,list2) 39 | importFrom(sortable,add_rank_list) 40 | importFrom(sortable,bucket_list) 41 | importFrom(sortable,is_sortable_options) 42 | importFrom(sortable,sortable_options) 43 | importFrom(utils,modifyList) 44 | -------------------------------------------------------------------------------- /R/assertions.R: -------------------------------------------------------------------------------- 1 | #' @importFrom assertthat assert_that on_failure<- 2 | 3 | is_input_id <- function(x) { 4 | is.null(x) || (is.character(x) && length(x) == 1 && !is.na(x)) 5 | } 6 | 7 | on_failure(is_input_id) <- function(call, env) { 8 | paste0(deparse(call$x), " is not a string (length 1 character)") 9 | } 10 | 11 | # -------------------------------------------------------------------- 12 | 13 | 14 | is_header <- function(x) { 15 | is.null(x) || (is.character(x) && length(x) == 1 && !is.na(x)) 16 | } 17 | 18 | on_failure(is_header) <- function(call, env) { 19 | paste0(deparse(call$x), " is not a string (length 1 character)") 20 | } 21 | 22 | -------------------------------------------------------------------------------- /R/bucket_problem.R: -------------------------------------------------------------------------------- 1 | #' @importFrom sortable sortable_options is_sortable_options 2 | #' @importFrom sortable add_rank_list bucket_list 3 | #' 4 | #' @importFrom learnr question_ui_initialize 5 | #' @importFrom learnr question_ui_completed 6 | #' @importFrom learnr question_is_valid 7 | #' @importFrom learnr question_is_correct 8 | #' 9 | #' @importFrom learnr question_ui_try_again 10 | #' 11 | #' @importFrom learnr mark_as 12 | #' @importFrom learnr disable_all_tags 13 | NULL 14 | 15 | 16 | 17 | #' Create a bucket problem (experimental). 18 | #' 19 | #' This function implements the bucket problem, as exposed by 20 | #' [question_bucket()]. Most users will only use this function inside a 21 | #' `learnr` tutorial, so please see the documentation at [question_bucket()] 22 | #' 23 | #' 24 | #' @inheritParams sortable::rank_list 25 | #' @inheritParams sortable::bucket_list 26 | #' 27 | #' @param initial Vector with initial values for problem (to appear in left 28 | #' column). Note: this must be a super-set of all answers. 29 | #' 30 | #' @param text Vector of headings for each column. 31 | #' 32 | #' @param input_id Character vector of `input_id` to pass (individually) to 33 | #' [rank_list()]. 34 | #' 35 | #' 36 | #' @inheritParams sortable::bucket_list 37 | #' 38 | #' @export 39 | # @example inst/examples/example_bucket.R 40 | #' 41 | #' 42 | # @examples 43 | # ## Example of a shiny app 44 | # if (interactive()) { 45 | # app <- system.file("shiny-examples/bucket_app.R", package = "parsons") 46 | # shiny::runApp(app) 47 | # } 48 | bucket_problem <- function( 49 | initial, 50 | text = c("Drag from here", "Construct your solution here"), 51 | header = NULL, 52 | input_id, 53 | group_name, 54 | class = "default-sortable default-bucket", 55 | options = sortable_options( 56 | # emptyInsertThreshold = 150 57 | ), 58 | orientation = c("horizontal", "vertical") 59 | ) { 60 | if (is.character(initial)) initial <- list(initial, NULL) 61 | assert_that(is_sortable_options(options)) 62 | if (missing(group_name) || is.null(group_name)) { 63 | group_name <- increment_bucket_group() 64 | } 65 | if (missing(input_id) || is.null(input_id)) { 66 | input_id <- paste0(group_name, c("_1", "_2")) 67 | } 68 | 69 | if (length(input_id) == 1) { 70 | input_id <- list(paste0(input_id, "_1"), input_id) 71 | } 72 | orientation <- match.arg(orientation) 73 | 74 | z <- bucket_list( 75 | header = header, 76 | class = class, 77 | add_rank_list(text = text[1], 78 | labels = initial[[1]], 79 | input_id = input_id[[1]], 80 | options = options 81 | ), 82 | add_rank_list(text = text[2], 83 | labels = initial[[2]], 84 | input_id = input_id[[2]], 85 | options = options 86 | ), 87 | group_name = group_name, 88 | orientation = orientation 89 | ) 90 | 91 | min_height <- 50 * (length(initial[[1]]) + 1) 92 | z <- htmltools::tagList( 93 | z, 94 | bucket_dependencies(), 95 | htmltools::tags$style( 96 | sprintf( 97 | htmltools::HTML(".rank-list-container {min-height: %spx;}"), 98 | min_height 99 | ) 100 | ) 101 | ) 102 | as.bucket_problem(z) 103 | } 104 | 105 | -------------------------------------------------------------------------------- /R/css.R: -------------------------------------------------------------------------------- 1 | css_dependency <- function(name, files) { 2 | list( 3 | htmltools::htmlDependency( 4 | name, 5 | version = utils::packageVersion("parsons"), 6 | src = "htmlwidgets/plugins/parsons", 7 | package = "parsons", 8 | stylesheet = files, 9 | all_files = FALSE 10 | ) 11 | ) 12 | } 13 | 14 | 15 | 16 | parsons_dependencies <- function() { 17 | css_dependency("parsons", "parsons.css") 18 | } 19 | 20 | bucket_dependencies <- function() { 21 | css_dependency("parsons", "parsons.css") 22 | } 23 | -------------------------------------------------------------------------------- /R/expectations.R: -------------------------------------------------------------------------------- 1 | is_answer <- function(x){ 2 | inherits(x, "tutorial_question_answer") 3 | } 4 | 5 | is.expectation_pass <- function(x) { 6 | inherits(x, "parsons_expectation_pass") 7 | } 8 | 9 | is.expectation_fail <- function(x) { 10 | inherits(x, "parsons_expectation_fail") 11 | } 12 | 13 | 14 | expectation_pass <- function (fun, message = "failure") { 15 | structure( 16 | class = c("parsons_expectation_pass", "list"), 17 | list(fun = fun, message = message) 18 | ) 19 | } 20 | 21 | expectation_fail <- function (fun, message = "failure") { 22 | structure( 23 | class = c("parsons_expectation_fail", "list"), 24 | list(fun = fun, message = message) 25 | ) 26 | } 27 | 28 | 29 | eval_expectation <- function(exp, answer_list) { 30 | isTRUE(exp$fun(answer_list)) 31 | } 32 | 33 | 34 | 35 | # pass_if ----------------------------------------------------------------- 36 | 37 | #' Add expectations to a parsons problem. 38 | #' 39 | #' @param f One of: 40 | #' * A character vector, indicating an exact match 41 | #' * A function of the function `function(x){...}` that evaluates to TRUE or FALSE 42 | #' * A function of the form `~ .`, as used by the tidy evaluation, e.g. in [purrr::map] 43 | #' @param message Message to display if `fun` evaluates to TRUE 44 | #' 45 | #' @rdname expectations 46 | #' 47 | #' @export 48 | pass_if <- function(f, message = NULL){ 49 | UseMethod("pass_if", f) 50 | } 51 | 52 | #' @export 53 | pass_if.character <- function(f, message = NULL) { 54 | learnr::answer(f, correct = TRUE, message = message) 55 | } 56 | 57 | 58 | 59 | #' @export 60 | pass_if.default <- function(f, message = NULL) { 61 | expectation_pass( 62 | fun = rlang::as_function(f), 63 | message = message 64 | ) 65 | } 66 | 67 | #' @export 68 | pass_if.function <- function(f, message = NULL) { 69 | expectation_pass( 70 | fun = rlang::as_function(f), 71 | message = message 72 | ) 73 | } 74 | 75 | 76 | # fail_if ----------------------------------------------------------------- 77 | 78 | #' @rdname expectations 79 | #' @export 80 | fail_if <- function(f, message = "Incorrect"){ 81 | UseMethod("fail_if", f) 82 | } 83 | 84 | #' @export 85 | fail_if.character <- function(f, message = "Incorrect") { 86 | learnr::answer(f, correct = FALSE, message = message) 87 | } 88 | 89 | #' @export 90 | fail_if.default <- function(f, message = "Incorrect") { 91 | expectation_fail( 92 | fun = rlang::as_function(f), 93 | message = message 94 | ) 95 | } 96 | 97 | #' @export 98 | fail_if.function <- function(f, message = "Incorrect") { 99 | expectation_fail( 100 | fun = rlang::as_function(f), 101 | message = message 102 | ) 103 | } 104 | 105 | 106 | 107 | # message_if -------------------------------------------------------------- 108 | 109 | 110 | #' @export 111 | #' @rdname expectations 112 | message_if <- function(f) { 113 | f 114 | } 115 | 116 | eval_message <- function(f, answer_list) { 117 | UseMethod("eval_message", f) 118 | } 119 | 120 | eval_message.character <- function(f, answer_list){ 121 | f 122 | } 123 | 124 | eval_message.default <- function(f, answer_list) { 125 | # browser() 126 | idx <- rlang::as_function(f)(answer_list) 127 | paste(answer_list[idx], collapse = ", ") 128 | } 129 | 130 | 131 | # ------------------------------------------------------------------------- 132 | 133 | # all_of <- function(.x){ 134 | # force(.x) 135 | # input <- .x 136 | # function(x)length(input) == length(x) && all(sort(input) == sort(x)) 137 | # } 138 | 139 | # pass_if_all_of <- function(zz, message = NULL) { 140 | # f <- function(x)~identical(sort(x), sort(zz)) 141 | # expectation_pass( 142 | # fun = rlang::as_function(f), 143 | # message = message 144 | # ) 145 | # } 146 | 147 | #' @rdname expectations 148 | #' @param ... Combined into an answer list 149 | #' @param x Answer to test 150 | #' @export 151 | contains_all <- function(x, ...){ 152 | y <- unlist(rlang::list2(...)) 153 | identical(sort(x), sort(y)) 154 | } 155 | 156 | #' @rdname expectations 157 | #' @export 158 | contains_any <- function(x, ...){ 159 | y <- unlist(rlang::list2(...)) 160 | any(x %in% y) 161 | } 162 | 163 | -------------------------------------------------------------------------------- /R/incrementor.R: -------------------------------------------------------------------------------- 1 | incrementor <- function(prefix = "increment_"){ 2 | i <- 0 3 | function(){ 4 | i <<- i + 1 5 | paste0(prefix, i) 6 | } 7 | } 8 | 9 | increment_parsons <- incrementor("parsons_list_id_") 10 | increment_parsons_group <- incrementor("parsons_group_") 11 | 12 | 13 | increment_bucket <- incrementor("bucket_list_id_") 14 | increment_bucket_group <- incrementor("bucket_group_") 15 | -------------------------------------------------------------------------------- /R/methods.R: -------------------------------------------------------------------------------- 1 | is_parsons_problem <- function(x) { 2 | inherits(x, "parsons_problem") 3 | } 4 | 5 | as.parsons_problem <- function(x){ 6 | class(x) <- c("parsons_problem", class(x)) 7 | x 8 | } 9 | 10 | #' @export 11 | print.parsons <- function(x, ...){ 12 | htmltools::html_print(x) 13 | } 14 | 15 | 16 | 17 | 18 | is_bucket_problem <- function(x) { 19 | inherits(x, "bucket_problem") 20 | } 21 | 22 | as.bucket_problem <- function(x){ 23 | class(x) <- c("bucket_problem", class(x)) 24 | x 25 | } 26 | 27 | #' @export 28 | print.bucket <- function(x, ...){ 29 | htmltools::html_print(x) 30 | } 31 | -------------------------------------------------------------------------------- /R/parsons-package.R: -------------------------------------------------------------------------------- 1 | #' @importFrom assertthat assert_that is.string 2 | #' @name parsons 3 | #' @aliases parsons-package parsons 4 | "_PACKAGE" 5 | 6 | 7 | # The following block is used by usethis to automatically manage 8 | # roxygen namespace tags. Modify with care! 9 | ## usethis namespace: start 10 | ## usethis namespace: end 11 | NULL 12 | 13 | 14 | -------------------------------------------------------------------------------- /R/parsons_problem.R: -------------------------------------------------------------------------------- 1 | #' @importFrom sortable sortable_options is_sortable_options 2 | #' @importFrom sortable add_rank_list bucket_list 3 | #' 4 | #' @importFrom learnr question_ui_initialize 5 | #' @importFrom learnr question_ui_completed 6 | #' @importFrom learnr question_is_valid 7 | #' @importFrom learnr question_is_correct 8 | #' 9 | #' @importFrom learnr question_ui_try_again 10 | #' 11 | #' @importFrom learnr mark_as 12 | #' @importFrom learnr disable_all_tags 13 | NULL 14 | 15 | 16 | 17 | #' Create a parsons problem (experimental). 18 | #' 19 | #' This function implements the parsons problem, as exposed by 20 | #' [question_parsons()]. Most users will only use this function inside a 21 | #' `learnr` tutorial, so please see the documentation at [question_parsons()] 22 | #' 23 | #' 24 | #' @inheritParams sortable::rank_list 25 | #' @inheritParams sortable::bucket_list 26 | #' 27 | #' @param initial Vector with initial values for problem (to appear in left 28 | #' column). Note: this must be a super-set of all answers. 29 | #' 30 | #' @param text Vector of headings for each column. 31 | #' 32 | #' @param input_id Character vector of `input_id` to pass (individually) to 33 | #' [rank_list()]. 34 | #' 35 | #' 36 | #' @param problem_type One of `base`, `ggplot2` or `tidyverse`, indicating the type of 37 | #' problem statement. For `tidyverse`, the resulting answer will 38 | #' automatically append a ` %>% ` at the end of each answer, and for `ggplot2` 39 | #' every line will be followed by a ` + `. 40 | #' 41 | #' @inheritParams sortable::bucket_list 42 | #' 43 | #' @export 44 | #' @example inst/examples/example_parsons.R 45 | #' 46 | #' @references https://js-parsons.github.io/ 47 | #' 48 | #' 49 | #' @examples 50 | #' ## Example of a shiny app 51 | #' if (interactive()) { 52 | #' app <- system.file("shiny-examples/parsons_app.R", package = "parsons") 53 | #' shiny::runApp(app) 54 | #' } 55 | parsons_problem <- function( 56 | initial, 57 | text = c("Drag from here", "Construct your solution here"), 58 | header = NULL, 59 | input_id, 60 | group_name, 61 | problem_type = c("base", "ggplot2", "tidyverse"), 62 | class = "default-sortable default-parsons", 63 | options = sortable_options( 64 | # emptyInsertThreshold = 150 65 | ), 66 | orientation = c("horizontal", "vertical") 67 | ) { 68 | if (is.character(initial)) initial <- list(initial, NULL) 69 | assert_that(is_sortable_options(options)) 70 | if (missing(group_name) || is.null(group_name)) { 71 | group_name <- increment_parsons_group() 72 | } 73 | if (missing(input_id) || is.null(input_id)) { 74 | input_id <- paste0(group_name, c("_1", "_2")) 75 | } 76 | 77 | if (length(input_id) == 1) { 78 | input_id <- list(paste0(input_id, "_1"), input_id) 79 | } 80 | orientation <- match.arg(orientation) 81 | problem_type <- match.arg(problem_type) 82 | 83 | z <- bucket_list( 84 | header = header, 85 | class = class, 86 | add_rank_list(text = text[1], 87 | labels = wrap_labels(initial[[1]], r_type = problem_type), 88 | input_id = input_id[[1]], 89 | options = options 90 | ), 91 | add_rank_list(text = text[2], 92 | labels = wrap_labels(initial[[2]], r_type = problem_type), 93 | input_id = input_id[[2]], 94 | options = options 95 | ), 96 | group_name = group_name, 97 | orientation = orientation 98 | ) 99 | 100 | min_height <- 50 * (length(initial[[1]]) + 1) 101 | z <- htmltools::tagList( 102 | z, 103 | parsons_dependencies(), 104 | htmltools::tags$style( 105 | sprintf( 106 | htmltools::HTML(".rank-list-container {min-height: %spx;}"), 107 | min_height 108 | ) 109 | ) 110 | ) 111 | as.parsons_problem(z) 112 | } 113 | 114 | -------------------------------------------------------------------------------- /R/question_bucket.R: -------------------------------------------------------------------------------- 1 | #' @importFrom utils modifyList 2 | NULL 3 | 4 | # is element a list of answers 5 | is_answer_list <- function(x){ 6 | all(vapply(x, function(x)inherits(x, "tutorial_question_answer"), FUN.VALUE = logical(1))) 7 | } 8 | 9 | 10 | # promotes embedded list of answers to one level higher 11 | promote_answerlist <- function(x){ 12 | z <- list() 13 | for (i in seq_along(x)){ 14 | if (length(x[[i]]) == 1) { 15 | z <- append(z, x[i]) 16 | } else { 17 | y <- x[[i]] 18 | if (is_answer_list(y)) { 19 | for (j in seq_along(y)) 20 | z <- append(z, y[j]) 21 | } else { 22 | z <- append(z, y) 23 | } 24 | } 25 | } 26 | z 27 | } 28 | 29 | 30 | #' bucket problem question for learnr tutorials (experimental). 31 | #' 32 | #' 33 | #' @param initial Initial value of answer options. This must be a character vector. 34 | #' 35 | #' @param ... One or more answers. Passed to [learnr::question()]. 36 | #' @inheritParams learnr::question 37 | #' @inheritParams bucket_problem 38 | #' 39 | # @param type Must be "bucket_question" 40 | #' @param correct Text to print for a correct answer (defaults to "Correct!") 41 | #' 42 | #' @export 43 | #' @examples 44 | #' ## Example of bucket problem inside a learn tutorial 45 | #' if (interactive()) { 46 | #' learnr::run_tutorial("bucket", package = "parsons") 47 | #' } 48 | question_bucket <- function( 49 | initial, 50 | ..., 51 | text = c("Drag from here", "Construct your solution here"), 52 | orientation = c("horizontal", "vertical"), 53 | correct = "Correct!", 54 | incorrect = "Incorrect", 55 | try_again = incorrect, 56 | message = NULL, 57 | post_message = NULL, 58 | loading = c("Loading: "), 59 | submit_button = "Submit Answer", 60 | try_again_button = "Try Again", 61 | allow_retry = TRUE, 62 | random_answer_order = TRUE, 63 | options = sortable_options() 64 | 65 | 66 | ) { 67 | dots <- list2(...) 68 | 69 | # initialize answers with a dummy that can never appear 70 | answers <- list(learnr::answer(paste(initial, sep = ""), correct = TRUE)) 71 | 72 | # append any other provided answers 73 | answers <- append( 74 | answers, 75 | dots[vapply(dots, is_answer, FUN.VALUE = logical(1))] 76 | ) 77 | pass <- dots[vapply(dots, is.expectation_pass, FUN.VALUE = logical(1))] 78 | fail <- dots[vapply(dots, is.expectation_fail, FUN.VALUE = logical(1))] 79 | 80 | orientation <- match.arg(orientation) 81 | 82 | z <- do.call( 83 | learnr::question, 84 | append( 85 | answers, 86 | list( 87 | text = NULL, 88 | type = "bucket_question", 89 | correct = correct, 90 | incorrect = incorrect, 91 | try_again = try_again, 92 | message = message, 93 | post_message = post_message, 94 | loading = loading, 95 | submit_button = submit_button, 96 | try_again_button = try_again_button, 97 | allow_retry = allow_retry, 98 | random_answer_order = random_answer_order, 99 | options = list( 100 | initial = initial, 101 | text = text, 102 | pass = pass, 103 | fail = fail, 104 | sortable_options = options 105 | ) 106 | ) 107 | ) 108 | ) 109 | z 110 | } 111 | 112 | 113 | #' @export 114 | question_ui_initialize.bucket_question <- function(question, answer_input, ...) { 115 | 116 | labels <- question$options$initial 117 | if (isTRUE(question$random_answer_order)) { # and we should randomize the order 118 | shuffle <- shiny::repeatable(sample, question$seed) 119 | labels <- shuffle(labels) 120 | } 121 | 122 | 123 | # return the bucket htmlwidget 124 | z <- bucket_problem( 125 | input_id = c(question$ids$question, question$ids$answer), 126 | initial = list( 127 | setdiff(labels, answer_input), 128 | answer_input 129 | ), 130 | text = question$options$text, 131 | orientation = question$options$orientation, 132 | options = question$options$sortable_options, 133 | ... 134 | ) 135 | z 136 | } 137 | 138 | 139 | 140 | #' @export 141 | question_ui_completed.bucket_question <- function(question, answer_input, ...) { 142 | # TODO display correct values with X or √ compared to best match 143 | # TODO DON'T display correct values (listen to an option?) 144 | 145 | labels <- question$options$initial 146 | if (isTRUE(question$random_answer_order)) { # and we should randomize the order 147 | shuffle <- shiny::repeatable(sample, question$seed) 148 | labels <- shuffle(labels) 149 | } 150 | 151 | new_options <- modifyList( 152 | question$options$sortable_options, 153 | sortable_options(disabled = TRUE) 154 | ) 155 | 156 | disable_all_tags( 157 | bucket_problem( 158 | input_id = c(question$ids$question, question$ids$answer), 159 | initial = list( 160 | setdiff(labels, answer_input), 161 | answer_input 162 | ), 163 | text = question$options$text, 164 | orientation = question$options$orientation, 165 | options = new_options, 166 | ... 167 | ) 168 | ) 169 | } 170 | 171 | 172 | #' Disable input after student submitted answer. 173 | #' 174 | #' @inheritParams learnr::question_disable_input 175 | #' 176 | #' @param question Question object 177 | #' @param answer_input user input value 178 | #' @param ... not used 179 | #' 180 | #' @export 181 | question_ui_try_again.bucket_question <- function(question, answer_input, ...) { 182 | # TODO display correct values with X or √ compared to best match 183 | # TODO DON'T display correct values (listen to an option?) 184 | labels <- question$options$initial 185 | if (isTRUE(question$random_answer_order)) { # and we should randomize the order 186 | shuffle <- shiny::repeatable(sample, question$seed) 187 | labels <- shuffle(labels) 188 | } 189 | new_options <- modifyList( 190 | question$options$sortable_options, 191 | sortable_options(disabled = TRUE) 192 | ) 193 | 194 | disable_all_tags( 195 | bucket_problem( 196 | input_id = c(question$ids$question, question$ids$answer), 197 | initial = list( 198 | setdiff(labels, answer_input), 199 | answer_input 200 | ), 201 | text = question$options$text, 202 | orientation = question$options$orientation, 203 | options = new_options, 204 | ... 205 | ) 206 | ) 207 | } 208 | 209 | 210 | #' @inheritParams learnr::question_disable_input 211 | #' @export 212 | question_is_correct.bucket_question <- function(question, answer_input, ...) { 213 | # for each possible answer, check if it matches 214 | for (answer in question$answers) { 215 | if (identical(answer$option, answer_input)) { 216 | # if it matches, return the correct-ness and its message 217 | return(mark_as(answer$correct, answer$message)) 218 | } 219 | } 220 | 221 | # for each possible expectation, check if it matches 222 | pass_expectations <- question$options$pass 223 | 224 | for (exp in pass_expectations) { 225 | if (eval_expectation(exp, answer_input)) { 226 | # if it matches, return the correct-ness and its message 227 | return(mark_as(TRUE, messages = exp$message)) 228 | } 229 | } 230 | 231 | # for each possible expectation, check if it matches 232 | fail_expectations <- question$options$fail 233 | 234 | # browser() 235 | 236 | for (exp in fail_expectations) { 237 | if (eval_expectation(exp, answer_input)) { 238 | # if it matches, return the correct-ness and its message 239 | return(mark_as(FALSE, eval_message(exp$message, answer_input))) 240 | } 241 | } 242 | 243 | 244 | # no match found. not correct 245 | mark_as(FALSE, NULL) 246 | } 247 | -------------------------------------------------------------------------------- /R/question_parsons.R: -------------------------------------------------------------------------------- 1 | #' @importFrom utils modifyList 2 | #' @importFrom rlang list2 3 | NULL 4 | 5 | #' Parsons problem question for learnr tutorials (experimental). 6 | #' 7 | #' @template question_parsons_description 8 | #' 9 | #' @param initial Initial value of answer options. This must be a character vector. 10 | #' 11 | #' @param ... One or more answers. Passed to [learnr::question()]. 12 | #' @inheritParams learnr::question 13 | #' @inheritParams parsons_problem 14 | #' 15 | # @param type Must be "parsons_question" 16 | #' @param correct Text to print for a correct answer (defaults to "Correct!") 17 | #' 18 | #' @export 19 | #' @examples 20 | #' ## Example of parsons problem inside a learn tutorial 21 | #' if (interactive()) { 22 | #' learnr::run_tutorial("parsons", package = "parsons") 23 | #' } 24 | question_parsons <- function( 25 | initial, 26 | ..., 27 | problem_type = c("base", "ggplot2", "tidyverse"), 28 | orientation = c("horizontal", "vertical"), 29 | correct = "Correct!", 30 | incorrect = "Incorrect", 31 | try_again = incorrect, 32 | message = NULL, 33 | post_message = NULL, 34 | loading = c("Loading: "), 35 | submit_button = "Give feedback", 36 | try_again_button = "Try Again", 37 | allow_retry = TRUE, 38 | random_answer_order = TRUE, 39 | options = sortable_options() 40 | 41 | 42 | ) { 43 | dots <- list2(...) 44 | answers <- dots[vapply(dots, is_answer, FUN.VALUE = logical(1))] 45 | pass <- dots[vapply(dots, is.expectation_pass, FUN.VALUE = logical(1))] 46 | fail <- dots[vapply(dots, is.expectation_fail, FUN.VALUE = logical(1))] 47 | 48 | problem_type <- match.arg(problem_type) 49 | orientation <- match.arg(orientation) 50 | 51 | z <- do.call( 52 | learnr::question, 53 | append( 54 | answers, 55 | list( 56 | text = NULL, 57 | type = "parsons_question", 58 | correct = correct, 59 | incorrect = incorrect, 60 | try_again = try_again, 61 | message = message, 62 | post_message = post_message, 63 | loading = loading, 64 | submit_button = submit_button, 65 | try_again_button = try_again_button, 66 | allow_retry = allow_retry, 67 | random_answer_order = random_answer_order, 68 | options = list( 69 | initial = initial, 70 | pass = pass, 71 | fail = fail, 72 | problem_type = problem_type, 73 | sortable_options = options 74 | ) 75 | ) 76 | ) 77 | ) 78 | z 79 | } 80 | 81 | 82 | #' @export 83 | question_ui_initialize.parsons_question <- function(question, answer_input, ...) { 84 | 85 | labels <- question$options$initial 86 | if (isTRUE(question$random_answer_order)) { # and we should randomize the order 87 | shuffle <- shiny::repeatable(sample, question$seed) 88 | labels <- shuffle(labels) 89 | } 90 | 91 | 92 | # return the parsons htmlwidget 93 | z <- parsons_problem( 94 | input_id = c(question$ids$question, question$ids$answer), 95 | initial = list( 96 | setdiff(labels, answer_input), 97 | answer_input 98 | ), 99 | problem_type = question$options$problem_type, 100 | orientation = question$options$orientation, 101 | options = question$options$sortable_options, 102 | ... 103 | ) 104 | z 105 | } 106 | 107 | 108 | 109 | #' @export 110 | question_ui_completed.parsons_question <- function(question, answer_input, ...) { 111 | # TODO display correct values with X or √ compared to best match 112 | # TODO DON'T display correct values (listen to an option?) 113 | 114 | labels <- question$options$initial 115 | if (isTRUE(question$random_answer_order)) { # and we should randomize the order 116 | shuffle <- shiny::repeatable(sample, question$seed) 117 | labels <- shuffle(labels) 118 | } 119 | 120 | new_options <- modifyList( 121 | question$options$sortable_options, 122 | sortable_options(disabled = TRUE) 123 | ) 124 | 125 | disable_all_tags( 126 | parsons_problem( 127 | input_id = c(question$ids$question, question$ids$answer), 128 | initial = list( 129 | setdiff(labels, answer_input), 130 | answer_input 131 | ), 132 | problem_type = question$options$problem_type, 133 | orientation = question$options$orientation, 134 | options = new_options, 135 | ... 136 | ) 137 | ) 138 | } 139 | 140 | 141 | #' Disable input after student submitted answer. 142 | #' 143 | #' @inheritParams learnr::question_disable_input 144 | #' 145 | #' @param question Question object 146 | #' @param answer_input user input value 147 | #' @param ... not used 148 | #' 149 | #' @export 150 | question_ui_try_again.parsons_question <- function(question, answer_input, ...) { 151 | # TODO display correct values with X or √ compared to best match 152 | # TODO DON'T display correct values (listen to an option?) 153 | labels <- question$options$initial 154 | if (isTRUE(question$random_answer_order)) { # and we should randomize the order 155 | shuffle <- shiny::repeatable(sample, question$seed) 156 | labels <- shuffle(labels) 157 | } 158 | new_options <- modifyList( 159 | question$options$sortable_options, 160 | sortable_options(disabled = TRUE) 161 | ) 162 | 163 | disable_all_tags( 164 | parsons_problem( 165 | input_id = c(question$ids$question, question$ids$answer), 166 | initial = list( 167 | setdiff(labels, answer_input), 168 | answer_input 169 | ), 170 | problem_type = question$options$problem_type, 171 | orientation = question$options$orientation, 172 | options = new_options, 173 | ... 174 | ) 175 | ) 176 | } 177 | 178 | 179 | #' @inheritParams learnr::question_disable_input 180 | #' @export 181 | question_is_correct.parsons_question <- function(question, answer_input, ...) { 182 | # for each possible answer, check if it matches 183 | for (answer in question$answers) { 184 | if (identical(answer$option, answer_input)) { 185 | # if it matches, return the correct-ness and its message 186 | return(mark_as(answer$correct, answer$message)) 187 | } 188 | } 189 | 190 | # for each possible expectation, check if it matches 191 | pass_expectations <- question$options$pass 192 | 193 | for (exp in pass_expectations) { 194 | if (eval_expectation(exp, answer_input)) { 195 | # if it matches, return the correct-ness and its message 196 | return(mark_as(TRUE, messages = exp$message)) 197 | } 198 | } 199 | 200 | # for each possible expectation, check if it matches 201 | fail_expectations <- question$options$fail 202 | 203 | for (exp in fail_expectations) { 204 | if (eval_expectation(exp, answer_input)) { 205 | # if it matches, return the correct-ness and its message 206 | return(mark_as(FALSE, messages = exp$message)) 207 | } 208 | } 209 | 210 | 211 | # no match found. not correct 212 | mark_as(FALSE, NULL) 213 | } 214 | -------------------------------------------------------------------------------- /R/wrap_labels.R: -------------------------------------------------------------------------------- 1 | 2 | wrap_labels <- function(labels, 3 | class = "rank-list-item-internal", 4 | r_type = c("base", "ggplot2", "tidyverse")){ 5 | r_type <- match.arg(r_type) 6 | lapply(labels, function(x) { 7 | htmltools::tags$div( 8 | class = paste(class, r_type), 9 | x 10 | ) 11 | }) 12 | } 13 | 14 | # initial = c( 15 | # "iris", 16 | # "mutate(...)", 17 | # "summarize(...)", 18 | # "print()" 19 | # ) 20 | # wrap_labels(initial) 21 | -------------------------------------------------------------------------------- /README.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: github_document 3 | --- 4 | 5 | 6 | 7 | ```{r, include = FALSE} 8 | knitr::opts_chunk$set( 9 | collapse = TRUE, 10 | comment = "#>", 11 | fig.path = "man/figures/README-", 12 | out.width = "100%" 13 | ) 14 | ``` 15 | 16 | # parsons 17 | 18 | 19 | 20 | [![Travis build status](https://travis-ci.org/rstudio/parsons.svg?branch=master)](https://travis-ci.org/rstudio/parsons) 21 | [![CRAN version](http://www.r-pkg.org/badges/version/parsons)](https://cran.r-project.org/package=parsons) 22 | [![parsons downloads per month](http://cranlogs.r-pkg.org/badges/parsons)](http://www.rpackages.io/package/parsons) 23 | [![Codecov test coverage](https://codecov.io/gh/rstudio/parsons/branch/master/graph/badge.svg)](https://codecov.io/gh/rstudio/parsons?branch=master) 24 | [![Lifecycle: experimental](https://img.shields.io/badge/lifecycle-experimental-orange.svg)](https://www.tidyverse.org/lifecycle/#experimental) 25 | 26 | 27 | 28 | Use the `parsons` package to create Parsons problems for teaching progamming. You can create custom questions in your `learnr` tutorials. 29 | 30 | 31 | ## Installation 32 | 33 | ~~You can install the released version of parsons from [CRAN](https://CRAN.R-project.org) with:~~ 34 | 35 | ```r 36 | ~~install.packages("parsons")~~ 37 | ``` 38 | 39 | And the development version from [GitHub](https://github.com/rstudio/parsons) with: 40 | 41 | ```r 42 | # install.packages("remotes") 43 | remotes::install_github("rstudio/parsons") 44 | ``` 45 | 46 | 47 | ## Examples 48 | 49 | 50 | ### Parsons problems 51 | 52 | A Parsons problem is a specific type of question, useful for teaching programming, where all the lines of code are given, but the student must provide the correct order. 53 | 54 | The `parsons()` function has experimental support for parsons problems. 55 | 56 |
57 | 58 |
59 | 60 | You can add a parsons problem to a `learnr` tutorial with the `question_parsons()` function: 61 | 62 | ```R 63 | question_parsons( 64 | initial = c( 65 | "iris", 66 | "mutate(...)", 67 | "summarize(...)", 68 | "print()" 69 | ), 70 | pass_if( 71 | c( 72 | "iris", 73 | "mutate(...)", 74 | "summarize(...)" 75 | ) 76 | ), 77 | fail_if( 78 | ~length(.) < 2, 79 | message = "Include at least two answers" 80 | ), 81 | fail_if( 82 | function(x){"print()" %in% x}, 83 | message = "You should not include print() in your answer" 84 | ), 85 | fail_if( 86 | ~{.[1] != "iris"}, 87 | message = "Your solution should start with 'iris'" 88 | ) 89 | ) 90 | ``` 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # parsons 5 | 6 | 7 | 8 | [![Travis build 9 | status](https://travis-ci.org/rstudio/parsons.svg?branch=master)](https://travis-ci.org/rstudio/parsons) 10 | [![CRAN 11 | version](http://www.r-pkg.org/badges/version/parsons)](https://cran.r-project.org/package=parsons) 12 | [![parsons downloads per 13 | month](http://cranlogs.r-pkg.org/badges/parsons)](http://www.rpackages.io/package/parsons) 14 | [![Codecov test 15 | coverage](https://codecov.io/gh/rstudio/parsons/branch/master/graph/badge.svg)](https://codecov.io/gh/rstudio/parsons?branch=master) 16 | [![Lifecycle: 17 | experimental](https://img.shields.io/badge/lifecycle-experimental-orange.svg)](https://www.tidyverse.org/lifecycle/#experimental) 18 | 19 | 20 | Use the `parsons` package to create Parsons problems for teaching 21 | progamming. You can create custom questions in your `learnr` tutorials. 22 | 23 | ## Installation 24 | 25 | ~~You can install the released version of parsons from 26 | [CRAN](https://CRAN.R-project.org) with:~~ 27 | 28 | ``` r 29 | ~~install.packages("parsons")~~ 30 | ``` 31 | 32 | And the development version from 33 | [GitHub](https://github.com/rstudio/parsons) with: 34 | 35 | ``` r 36 | # install.packages("remotes") 37 | remotes::install_github("rstudio/parsons") 38 | ``` 39 | 40 | ## Examples 41 | 42 | ### Parsons problems 43 | 44 | A Parsons problem is a specific type of question, useful for teaching 45 | programming, where all the lines of code are given, but the student must 46 | provide the correct order. 47 | 48 | The `parsons()` function has experimental support for parsons problems. 49 | 50 |
51 | 52 | 53 | 54 |
55 | 56 | You can add a parsons problem to a `learnr` tutorial with the 57 | `question_parsons()` function: 58 | 59 | ``` r 60 | question_parsons( 61 | initial = c( 62 | "iris", 63 | "mutate(...)", 64 | "summarize(...)", 65 | "print()" 66 | ), 67 | pass_if( 68 | c( 69 | "iris", 70 | "mutate(...)", 71 | "summarize(...)" 72 | ) 73 | ), 74 | fail_if( 75 | ~length(.) < 2, 76 | message = "Include at least two answers" 77 | ), 78 | fail_if( 79 | function(x){"print()" %in% x}, 80 | message = "You should not include print() in your answer" 81 | ), 82 | fail_if( 83 | ~{.[1] != "iris"}, 84 | message = "Your solution should start with 'iris'" 85 | ) 86 | ) 87 | ``` 88 | -------------------------------------------------------------------------------- /_pkgdown.yml: -------------------------------------------------------------------------------- 1 | destination: docs 2 | url: https://rstudio.github.io/parsons 3 | reference: 4 | - title: Parsons problems 5 | desc: Create parsons problems in your learnr tutorials 6 | contents: 7 | - starts_with("question") 8 | - Parsons_problem_hello_world 9 | - title: Specifying conditions 10 | desc: Add multiple conditions to evaluate questions and provide feedback 11 | contents: 12 | - matches("pass") 13 | - title: Package documentation 14 | desc: ~ 15 | contents: 16 | - parsons 17 | 18 | tutorials: 19 | - name: tutorial_parsons 20 | title: "Parson's problems Hello World!" 21 | url: https://andrie-de-vries.shinyapps.io/Parsons_problem_hello_world/ 22 | 23 | articles: 24 | - title: Creating a parsons problem 25 | desc: ~ 26 | contents: 27 | - introduction 28 | 29 | 30 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: true 2 | 3 | coverage: 4 | status: 5 | project: 6 | default: 7 | target: auto 8 | threshold: 1% 9 | patch: 10 | default: 11 | target: auto 12 | threshold: 1% 13 | -------------------------------------------------------------------------------- /inst/examples/example_parsons.R: -------------------------------------------------------------------------------- 1 | ## -- example-parsons ----------------------------------------------------- 2 | 3 | ## Parson's problem 4 | 5 | parsons_problem( 6 | header = "This is an example of a Parsons problem", 7 | text = c("Drag from here", "Construct your solution here"), 8 | initial = c( 9 | "iris", 10 | "mutate(...)", 11 | "summarize(...)", 12 | "print()" 13 | ), 14 | input_id = "input_parsons" 15 | ) 16 | 17 | 18 | parsons_problem( 19 | initial = c( 20 | "iris", 21 | "mutate(...)", 22 | "summarize(...)", 23 | "print()" 24 | ), 25 | input_id = "input_parsons" 26 | ) 27 | 28 | -------------------------------------------------------------------------------- /inst/examples/example_question_parsons.R: -------------------------------------------------------------------------------- 1 | if (require(learnr, quietly = TRUE)) { 2 | # to be used within a learnr tutorial... 3 | question_parsons( 4 | initial = c( 5 | "iris", 6 | "mutate(...)", 7 | "summarize(...)", 8 | "print()" 9 | ), 10 | answer(c( 11 | "iris", 12 | "mutate(...)", 13 | "summarize(...)" 14 | ), correct = TRUE), 15 | answer(c( 16 | "iris", 17 | "mutate(...)", 18 | "summarize(...)", 19 | "print()" 20 | )) 21 | ) 22 | } 23 | 24 | 25 | if (require(learnr, quietly = TRUE)) { 26 | # to be used within a learnr tutorial... 27 | question_parsons( 28 | initial = c( 29 | "iris", 30 | "mutate(...)", 31 | "summarize(...)", 32 | "print()" 33 | ), 34 | answer(c( 35 | "iris", 36 | "mutate(...)", 37 | "summarize(...)" 38 | ), correct = TRUE), 39 | fail_if( 40 | function(x){!"print()" %in% x}, 41 | "You should not include print()" 42 | ) 43 | ) 44 | } 45 | 46 | if (require(learnr, quietly = TRUE)) { 47 | # to be used within a learnr tutorial... 48 | question_parsons( 49 | initial = c( 50 | "iris", 51 | "mutate(...)", 52 | "summarize(...)", 53 | "print()" 54 | ), 55 | pass_if(c( 56 | "iris", 57 | "mutate(...)", 58 | "summarize(...)" 59 | )), 60 | fail_if( 61 | function(x){!"print()" %in% x}, 62 | "You should not include print()" 63 | ) 64 | ) 65 | } 66 | -------------------------------------------------------------------------------- /inst/htmlwidgets/plugins/parsons/_colors.scss: -------------------------------------------------------------------------------- 1 | $item-color: white; 2 | $rank-background-color: transparent; 3 | $item-background-color: #f8f8f8; 4 | $border-color: #ddd; 5 | $item-dragging-color: #75aadb; 6 | $item-hover-color: scale-color($item-dragging-color, $lightness: 0%); 7 | $item-ghost-color: scale-color($item-dragging-color, $lightness: 70%); 8 | 9 | $min-width: 150px; 10 | $min-container-height: 100px; 11 | $min-item-height: 50px; 12 | 13 | $rank-list-min-flex-size: 200px; 14 | $rank-list-min-height: 45px; 15 | 16 | $border-radius: 3px; 17 | -------------------------------------------------------------------------------- /inst/htmlwidgets/plugins/parsons/bucket_list.css: -------------------------------------------------------------------------------- 1 | .default-sortable.bucket-list-container { 2 | background-color: transparent; 3 | padding: 10px; 4 | margin: 5px; 5 | flex: 1 0 auto; 6 | min-height: 100px; 7 | } 8 | 9 | .default-sortable.bucket-list { 10 | display: flex; 11 | } 12 | 13 | .default-sortable.bucket-list.bucket-list-horizontal { 14 | flex-direction: row; 15 | flex-wrap: wrap; 16 | } 17 | 18 | .default-sortable.bucket-list.bucket-list-vertical { 19 | flex-direction: column; 20 | flex-wrap: nowrap; 21 | } 22 | 23 | .default-sortable.bucket-list.bucket-list-vertical .rank-list-container { 24 | flex: 1 0 auto; 25 | } 26 | 27 | -------------------------------------------------------------------------------- /inst/htmlwidgets/plugins/parsons/bucket_list.scss: -------------------------------------------------------------------------------- 1 | @import "colors"; 2 | 3 | .default-sortable { 4 | &.bucket-list-container { 5 | background-color: $rank-background-color; 6 | padding: 10px; 7 | margin: 5px; 8 | 9 | flex: 1 0 auto; 10 | min-height: 100px; 11 | 12 | 13 | } 14 | 15 | &.bucket-list { 16 | display: flex; 17 | // flex-direction: column; 18 | // flex-flow: column nowrap; 19 | &.bucket-list-horizontal { 20 | flex-direction: row; 21 | flex-wrap: wrap; 22 | } 23 | 24 | &.bucket-list-vertical { 25 | flex-direction: column; 26 | flex-wrap: nowrap; 27 | 28 | .rank-list-container { 29 | flex: 1 0 auto; 30 | } 31 | } 32 | } 33 | 34 | } 35 | 36 | -------------------------------------------------------------------------------- /inst/htmlwidgets/plugins/parsons/parsons.css: -------------------------------------------------------------------------------- 1 | .default-parsons .rank-list-item { 2 | background-color: transparent; 3 | padding: 0; 4 | border: none; 5 | } 6 | 7 | .default-parsons .rank-list-item.sortable-chosen { 8 | background-color: white; 9 | border: none; 10 | } 11 | 12 | .default-parsons .rank-list-item.sortable-ghost { 13 | background-color: #d6e6f4; 14 | } 15 | 16 | .default-parsons .rank-list-item.sortable-drag { 17 | background-color: #75aadb; 18 | border: 1px solid #4d91d0; 19 | } 20 | 21 | .default-parsons .rank-list-item:hover:not(.disabled) { 22 | background-color: white; 23 | } 24 | 25 | .default-parsons .rank-list-item .rank-list-item-internal { 26 | border-radius: 3px; 27 | display: inherit; 28 | padding: 10px 15px; 29 | background-color: inherit; 30 | border: 1px solid #ddd; 31 | overflow: inherit; 32 | width: inherit; 33 | background-color: transparent; 34 | font-family: monospace, sant-serif; 35 | width: fit-content; 36 | } 37 | 38 | .default-parsons .rank-list-item .rank-list-item-internal:hover:not(.disabled) { 39 | border-color: #75aadb; 40 | } 41 | 42 | .default-parsons .column_2 .rank-list-item:not(:last-child) .rank-list-item-internal.ggplot2:after { 43 | content: " +"; 44 | } 45 | 46 | .default-parsons .column_2 .rank-list-item:not(:last-child) .rank-list-item-internal.tidyverse:after { 47 | content: " %>%"; 48 | } 49 | 50 | .default-parsons .column_2 .rank-list-item:not(:first-child) .rank-list-item-internal.ggplot2 { 51 | margin-left: 30px; 52 | width: auto; 53 | } 54 | 55 | .default-parsons .column_2 .rank-list-item:not(:first-child) .rank-list-item-internal.tidyverse { 56 | margin-left: 30px; 57 | width: auto; 58 | } 59 | 60 | -------------------------------------------------------------------------------- /inst/htmlwidgets/plugins/parsons/parsons.scss: -------------------------------------------------------------------------------- 1 | @import "colors"; 2 | 3 | .default-parsons { 4 | .rank-list-item { 5 | background-color: transparent; 6 | padding: 0; 7 | border: none; 8 | 9 | &:last-child { 10 | // padding-bottom: 50px; 11 | // background-color: red; 12 | } 13 | &:last-child.sortable-chosen { 14 | // padding-bottom: 0px; 15 | // background-color: red; 16 | } 17 | &:hover:not(.disabled) { 18 | // padding-bottom: 0px; 19 | } 20 | &.sortable-chosen { 21 | background-color: $item-color; 22 | border: none; 23 | } 24 | &.sortable-ghost { 25 | background-color: $item-ghost-color; 26 | } 27 | // &.sortable-ghost.sortable-chosen { 28 | // background-color: $item-ghost-color; 29 | // } 30 | &.sortable-drag { 31 | // padding-bottom: -50px; 32 | background-color: $item-dragging-color; 33 | border: 1px solid darken($item-dragging-color, 10%); 34 | } 35 | &:hover:not(.disabled) { 36 | background-color: $item-color; 37 | } 38 | 39 | } 40 | 41 | .rank-list-item .rank-list-item-internal { 42 | 43 | border-radius: $border-radius; 44 | // position: inherit; 45 | display: inherit; 46 | padding: 10px 15px; 47 | background-color: inherit; 48 | border: 1px solid $border-color; 49 | overflow: inherit; 50 | width: inherit; 51 | 52 | background-color: transparent; 53 | font-family: monospace, sant-serif; 54 | width: fit-content; 55 | &:hover:not(.disabled) { 56 | // background-color: $item-hover-color; 57 | border-color: $item-hover-color; 58 | } 59 | 60 | } 61 | 62 | .column_2 { 63 | .rank-list-item { 64 | &:not(:last-child) .rank-list-item-internal.ggplot2:after { 65 | content: " +"; 66 | } 67 | &:not(:last-child) .rank-list-item-internal.tidyverse:after { 68 | content: " %>%"; 69 | } 70 | } 71 | 72 | .rank-list-item { 73 | 74 | &:not(:first-child) .rank-list-item-internal.ggplot2 { 75 | margin-left: 30px; 76 | width: auto; 77 | } 78 | &:not(:first-child) .rank-list-item-internal.tidyverse { 79 | margin-left: 30px; 80 | width: auto; 81 | } 82 | } 83 | 84 | } 85 | } 86 | 87 | 88 | -------------------------------------------------------------------------------- /inst/htmlwidgets/plugins/parsons/parsons_old.css: -------------------------------------------------------------------------------- 1 | .rank-list-container .column_2 { 2 | padding-bottom: 30px; 3 | } 4 | 5 | .rank-list-item { 6 | font-family: monospace, sant-serif; 7 | } 8 | 9 | .column_2 .rank-list-item-container:not(:last-child) .rank-list-item:after { 10 | content: " %>%"; 11 | } 12 | 13 | .column_2 .rank-list-item-container:not(:first-child) { 14 | margin-left: 30px; 15 | width: auto; 16 | } 17 | -------------------------------------------------------------------------------- /inst/htmlwidgets/plugins/parsons/rank_list.css: -------------------------------------------------------------------------------- 1 | .default-sortable.rank-list-container { 2 | flex: 1 0 auto; 3 | background-color: transparent; 4 | border: 1px solid #ddd; 5 | padding: 10px; 6 | margin: 5px; 7 | display: flex; 8 | flex-flow: column nowrap; 9 | } 10 | 11 | .default-sortable .rank-list-title { 12 | flex: 0 0 auto; 13 | } 14 | 15 | .default-sortable .rank-list { 16 | flex: 1 0 auto; 17 | -webkit-border-radius: 3px; 18 | border-radius: 5px; 19 | background-color: white; 20 | margin: 5px; 21 | min-height: 45px; 22 | } 23 | 24 | .default-sortable .rank-list.rank-list-empty { 25 | border-style: dashed; 26 | border-color: #ddd; 27 | } 28 | 29 | .default-sortable .rank-list-item { 30 | border-radius: 3px; 31 | display: block; 32 | padding: 10px 15px; 33 | background-color: #f8f8f8; 34 | border: 1px solid #ddd; 35 | overflow: hidden; 36 | width: 100%; 37 | } 38 | 39 | .default-sortable .rank-list-item:hover:not(.disabled) { 40 | background-color: #75aadb; 41 | cursor: grab; 42 | } 43 | 44 | .default-sortable .rank-list-item.sortable-ghost { 45 | color: transparent; 46 | } 47 | 48 | .default-sortable .rank-list-item.sortable-ghost:hover:not(.disabled) { 49 | cursor: grabbing; 50 | } 51 | 52 | .default-sortable .rank-list-item.sortable-chosen, .default-sortable .rank-list-item.sortable-ghost.sortable-chosen, .default-sortable .rank-list-item.sortable-drag { 53 | background-color: #75aadb; 54 | border: 1px solid #4d91d0; 55 | } 56 | 57 | .default-sortable .rank-list-item.sortable-chosen:hover:not(.disabled), .default-sortable .rank-list-item.sortable-ghost.sortable-chosen:hover:not(.disabled), .default-sortable .rank-list-item.sortable-drag:hover:not(.disabled) { 58 | cursor: grabbing; 59 | } 60 | 61 | .default-sortable .rank-list-item.disabled { 62 | cursor: not-allowed; 63 | } 64 | 65 | -------------------------------------------------------------------------------- /inst/htmlwidgets/plugins/parsons/rank_list.scss: -------------------------------------------------------------------------------- 1 | @import "colors"; 2 | 3 | .default-sortable { 4 | 5 | &.rank-list-container { 6 | flex: 1 0 auto; 7 | background-color: $rank-background-color; 8 | border: 1px solid $border-color; 9 | padding: 10px; 10 | margin: 5px; 11 | display: flex; 12 | flex-flow: column nowrap; 13 | } 14 | 15 | .rank-list-title { 16 | flex: 0 0 auto; 17 | } 18 | 19 | .rank-list { 20 | flex: 1 0 auto; 21 | -webkit-border-radius: 3px; 22 | border-radius: 5px; 23 | background-color: $item-color; 24 | margin: 5px; 25 | min-height: $rank-list-min-height; 26 | 27 | &.rank-list-empty { 28 | border-style: dashed; 29 | border-color: $border-color; 30 | } 31 | } 32 | 33 | 34 | 35 | 36 | 37 | .rank-list-item { 38 | border-radius: $border-radius; 39 | // position: relative; 40 | display: block; 41 | padding: 10px 15px; 42 | background-color: $item-background-color; 43 | border: 1px solid $border-color; 44 | overflow: hidden; 45 | width: 100%; 46 | 47 | &:hover:not(.disabled) { 48 | background-color: $item-hover-color; 49 | cursor: grab; 50 | } 51 | 52 | // Class name for the drop placeholder 53 | &.sortable-ghost { 54 | color: transparent; 55 | &:hover:not(.disabled) { 56 | cursor: grabbing; 57 | } 58 | } 59 | // Class name for the chosen item 60 | &.sortable-chosen, 61 | // Class name for chosen and original item 62 | &.sortable-ghost.sortable-chosen, 63 | // Class name for the dragging item 64 | &.sortable-drag { 65 | // color: white; 66 | background-color: $item-dragging-color; 67 | border: 1px solid darken($item-dragging-color, 10%); 68 | &:hover:not(.disabled) { 69 | cursor: grabbing; 70 | } 71 | } 72 | 73 | &.disabled { 74 | cursor: not-allowed; 75 | } 76 | 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /inst/shiny-examples/problem_type/app.R: -------------------------------------------------------------------------------- 1 | ## ---- parsons-app ------------------------------------------------------- 2 | ## Example shiny app with parsons problem 3 | 4 | library(shiny) 5 | library(htmltools) 6 | library(parsons) 7 | 8 | parsons_generator <- function(initial, problem_type, orientation = "horizontal"){ 9 | fluidRow( 10 | column( 11 | width = 12, 12 | 13 | ## This is the parsons problem 14 | parsons_problem( 15 | header = paste("This is an example of a Parsons problem using", problem_type), 16 | orientation = "horizontal", 17 | initial = initial, 18 | group_name = paste0("parsons_", problem_type), 19 | problem_type = problem_type 20 | ) 21 | 22 | ), 23 | fluidRow( 24 | column( 25 | width = 12, 26 | h2("Result"), 27 | verbatimTextOutput(paste0("answer_", problem_type)) 28 | ) 29 | ) 30 | ) 31 | } 32 | 33 | 34 | ui <- fluidPage( 35 | tags$head( 36 | tags$style(HTML(".bucket-list-container {min-height: 300px;}")) 37 | ), 38 | mainPanel( 39 | fluidRow( 40 | column( 41 | width = 12, 42 | h2("Question") 43 | )), 44 | 45 | tabsetPanel( 46 | type = "tabs", 47 | 48 | # base R --- 49 | tabPanel( 50 | "base", 51 | parsons_generator( 52 | initial = c( 53 | "x <- 1", 54 | "y <- 2", 55 | "sum(x, y)" 56 | ), 57 | problem_type = "base" 58 | ) 59 | ), 60 | 61 | # tidyverse --- 62 | tabPanel( 63 | "tidyverse", 64 | parsons_generator( 65 | initial = c( 66 | "iris", 67 | "mutate(...)", 68 | "summarize(...)", 69 | "print()" 70 | ), 71 | problem_type = "tidyverse" 72 | ) 73 | ), 74 | 75 | # ggplot2 --- 76 | tabPanel( 77 | "ggplot2", 78 | parsons_generator( 79 | initial = c( 80 | "ggplot(...)", 81 | "geom_point(...)", 82 | "geom_smooth(...)", 83 | "coord_fixed(...)" 84 | ), 85 | problem_type = "ggplot2" 86 | ) 87 | ) 88 | 89 | 90 | 91 | ) 92 | ) 93 | 94 | 95 | ) 96 | 97 | server <- function(input,output) { 98 | output$answer_base <- 99 | renderPrint( 100 | input$parsons_base 101 | ) 102 | 103 | output$answer_ggplot2 <- 104 | renderPrint( 105 | input$parsons_ggplot2 106 | ) 107 | 108 | output$answer_tidyverse <- 109 | renderPrint( 110 | input$parsons_tidyverse 111 | ) 112 | } 113 | 114 | shinyApp(ui, server) 115 | -------------------------------------------------------------------------------- /inst/shiny-examples/simple/app.R: -------------------------------------------------------------------------------- 1 | ## ---- parsons-app ------------------------------------------------------- 2 | ## Example shiny app with parsons problem 3 | 4 | library(shiny) 5 | library(parsons) 6 | 7 | ui <- fluidPage( 8 | fluidRow( 9 | column( 10 | width = 12, 11 | tags$h2("Question"), 12 | 13 | ## This is the parsons problem 14 | parsons_problem( 15 | header = "This is an example of a Parsons problem", 16 | orientation = "horizontal", 17 | problem_type = "tidyverse", 18 | initial = c( 19 | "iris", 20 | "mutate(...)", 21 | "summarize(...)", 22 | "print()" 23 | ), 24 | group_name = "parsons_unique_id" 25 | ) 26 | 27 | ) 28 | ), 29 | fluidRow( 30 | column( 31 | width = 12, 32 | tags$h2("Result"), 33 | verbatimTextOutput("answer") 34 | ) 35 | ) 36 | ) 37 | 38 | server <- function(input,output) { 39 | output$answer <- 40 | renderPrint( 41 | input$parsons_unique_id # This matches the input_id of the parsons problem 42 | ) 43 | } 44 | 45 | shinyApp(ui, server) 46 | -------------------------------------------------------------------------------- /inst/tutorials/bucket/tutorial_bucket.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: Bucket questions 3 | author: Andrie de Vries 4 | date: "`r Sys.Date()`" 5 | output: learnr::tutorial 6 | runtime: shiny_prerendered 7 | --- 8 | 9 | 10 | 11 | 12 | 13 | 14 | ## Example 15 | 16 | Here is an example of `question_bucket()`, an experimental function in the `parsons` package. 17 | 18 | ```{r setup} 19 | library(learnr) 20 | library(parsons) 21 | library(magrittr) 22 | ``` 23 | 24 | 25 | Drag the operating systems into the correct bucket: 26 | 27 | 28 | 29 | ```{r Hello-world} 30 | 31 | supported <- c("Red Hat", "Ubuntu", "Suse Linux", "CentOS") 32 | unsupported <- c( "Windows Server 2018", "Fedora", "Debian") 33 | 34 | question_bucket( 35 | initial = c(supported, unsupported), 36 | pass_if(~ contains_all(., supported)), 37 | fail_if( 38 | ~ length(.) < 4, 39 | "Provide at least 4 answers" 40 | ), 41 | fail_if( 42 | ~ length(.) > 4, 43 | "Provide only 4 answers" 44 | ), 45 | fail_if(~ contains_any(., unsupported), 46 | message = ~ message_if(which(. %in% unsupported)) 47 | ), 48 | text = c("Operating systems", "Supported by RStudio Connect") 49 | ) 50 | ``` 51 | 52 | 53 | -------------------------------------------------------------------------------- /inst/tutorials/parsons/tutorial_parsons.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: Parsons problems 3 | author: Andrie de Vries 4 | date: "`r Sys.Date()`" 5 | output: learnr::tutorial 6 | runtime: shiny_prerendered 7 | --- 8 | 9 | 10 | 11 | ```{r setup, include=FALSE} 12 | library(learnr) 13 | library(parsons) 14 | library(magrittr) 15 | ``` 16 | 17 | 18 | ## Parson's problems Hello World! 19 | 20 | A Parsons problem gives the student some programming statements in random order. The student then constructs an answer by dragging the statements into the correct order. 21 | 22 | Here is an example. Drag the statements from the top panel to the bottom panel to produce "Hello World!". 23 | 24 | ```{r Hello-world, echo=FALSE} 25 | question_parsons( 26 | initial = c( 27 | "Hello", 28 | "World", 29 | "!" 30 | ), 31 | pass_if( 32 | c( 33 | "Hello", 34 | "World", 35 | "!" 36 | ) 37 | ), 38 | problem_type = "base" 39 | ) 40 | ``` 41 | 42 | 43 | ## Constructing a parsons's problem 44 | 45 | To create a parsons problem in a `learnr` tutorial, use the `question_parson()` function. 46 | 47 | You must provide at minimum: 48 | 49 | * The `initial` set of values, as a character vector 50 | 51 | * A "correct" answer 52 | 53 | Note that, when using only this minimal specification, the only feedback the student will ever get is "Incorrect, try again". 54 | 55 | ```{r explain-1} 56 | question_parsons( 57 | initial = c( 58 | "Hello", 59 | "World", 60 | "!" 61 | ), 62 | answer( 63 | c( 64 | "Hello", 65 | "World", 66 | "!" 67 | ), 68 | correct = TRUE 69 | ), 70 | problem_type = "base" 71 | ) 72 | ``` 73 | 74 | 75 | ## Using pass and fail conditions 76 | 77 | An alternative way to specify the correct answer(s) is to supply a `pass_if()` statement. Using `pass_if()` and `fail_if()` is a powerful way to provide feedback to your students. 78 | 79 | These `pass_if()` and `fail_if()` statements are evaluated in turn, until the first expectation evaluates to `TRUE`, and the `leanr` will provide the feedback in the `message`. 80 | 81 | You can specify `pass_if()` as well as `fail_if()` in any of the following ways: 82 | 83 | * A character vector, that must be matched exactly, or 84 | 85 | * A function in the tradition form `function(x) ...`, e.g. `function(x) length(x) < 3` 86 | 87 | * A function specified using the `rlang` tidy evaluation formula notation, e.g. `~length(.) < 3` 88 | 89 | 90 | This next example will give feedback if the length of the answer is 2 or fewer items. 91 | 92 | ```{r explain-2} 93 | question_parsons( 94 | initial = c( 95 | "Hello", 96 | "World", 97 | "!" 98 | ), 99 | pass_if( 100 | c( 101 | "Hello", 102 | "World", 103 | "!" 104 | ) 105 | ), 106 | fail_if( 107 | ~length(.) <= 2, 108 | "Provide more answers" 109 | ), 110 | problem_type = "base" 111 | ) 112 | ``` 113 | 114 | 115 | 116 | 117 | ## Using multiple conditions 118 | 119 | Create a statement that uses `iris` data, then does some mutation and finally creates a summary. 120 | 121 | ```{r iris} 122 | question_parsons( 123 | initial = c( 124 | "iris", 125 | "mutate(...)", 126 | "summarize(...)", 127 | "print()" 128 | ), 129 | pass_if( 130 | c( 131 | "iris", 132 | "mutate(...)", 133 | "summarize(...)" 134 | ) 135 | ), 136 | fail_if( 137 | ~length(.) < 2, 138 | message = "Include at least two answers" 139 | ), 140 | fail_if( 141 | function(x){"print()" %in% x}, 142 | message = "You should not include print() in your answer" 143 | ), 144 | fail_if( 145 | ~{.[1] != "iris"}, 146 | message = "Your solution should start with 'iris'" 147 | ), 148 | fail_if( 149 | ~length(.) < 3, 150 | message = "Use at least 3 elements to form your answer" 151 | ), 152 | 153 | problem_type = "tidyverse" 154 | ) 155 | ``` 156 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /inst/tutorials/parsons/tutorial_parsons.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | Parsons problems 19 | 20 | 21 | 22 | 23 | 28 | 37 | 38 | 39 | 40 | 41 | 94 | 95 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 |
117 |
118 | 119 |
120 | 121 |
122 |

Parson’s problems Hello World!

123 |

A Parsons problem gives the student some programming statements in random order. The student then constructs an answer by dragging the statements into the correct order.

124 |

Here is an example. Drag the statements from the top panel to the bottom panel to produce “Hello World!”.

125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |

Constructing a parsons’s problem

133 |

To create a parsons problem in a learnr tutorial, use the question_parson() function.

134 |

You must provide at minimum:

135 |
    136 |
  • The initial set of values, as a character vector

  • 137 |
  • A “correct” answer

  • 138 |
139 |

Note that, when using only this minimal specification, the only feedback the student will ever get is “Incorrect, try again”.

140 |
question_parsons(
141 |   initial = c(
142 |     "Hello",
143 |     "World",
144 |     "!"
145 |   ),
146 |   answer(
147 |     c(
148 |     "Hello",
149 |     "World",
150 |     "!"
151 |     ),
152 |     correct = TRUE
153 |   ),
154 |   problem_type = "base"
155 | )
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |

Using pass and fail conditions

164 |

An alternative way to specify the correct answer(s) is to supply a pass_if() statement. Using pass_if() and fail_if() is a powerful way to provide feedback to your students.

165 |

These pass_if() and fail_if() statements are evaluated in turn, until the first expectation evaluates to TRUE, and the leanr will provide the feedback in the message.

166 |

You can specify pass_if() as well as fail_if() in any of the following ways:

167 |
    168 |
  • A character vector, that must be matched exactly, or

  • 169 |
  • A function in the tradition form function(x) ..., e.g. function(x) length(x) < 3

  • 170 |
  • A function specified using the rlang tidy evaluation formula notation, e.g. ~length(.) < 3

  • 171 |
172 |

This next example will give feedback if the length of the answer is 2 or fewer items.

173 |
question_parsons(
174 |   initial = c(
175 |     "Hello",
176 |     "World",
177 |     "!"
178 |   ),
179 |   pass_if(
180 |     c(
181 |     "Hello",
182 |     "World",
183 |     "!"
184 |     )
185 |   ),
186 |   fail_if(
187 |     ~length(.) <= 2,
188 |     "Provide more answers"
189 |   ),
190 |   problem_type = "base"
191 | )
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |

Using multiple conditions

200 |

Create a statement that uses iris data, then does some mutation and finally creates a summary.

201 |
question_parsons(
202 |   initial = c(
203 |     "iris",
204 |     "mutate(...)",
205 |     "summarize(...)",
206 |     "print()"
207 |   ),
208 |   pass_if(
209 |     c(
210 |       "iris",
211 |       "mutate(...)",
212 |       "summarize(...)"
213 |     )
214 |   ),
215 |   fail_if(
216 |     ~length(.) < 2,
217 |     message = "Include at least two answers"
218 |   ),
219 |   fail_if(
220 |     function(x){"print()" %in% x},
221 |     message = "You should not include print() in your answer"
222 |   ),
223 |   fail_if(
224 |     ~{.[1] != "iris"},
225 |     message = "Your solution should start with 'iris'"
226 |   ),
227 |   fail_if(
228 |     ~length(.) < 3,
229 |     message = "Use at least 3 elements to form your answer"
230 |   ),
231 | 
232 |   problem_type = "tidyverse"
233 | )
234 |
235 |
236 |
237 |
238 |
239 | 240 | 245 | 246 | 249 | 250 | 253 | 254 | 257 | 258 | 261 | 262 | 265 | 266 | 269 | 270 | 271 | 274 | 275 |
276 | 277 |
278 | 279 |
280 |
281 |
282 |
283 | 284 | 285 |
286 | 287 |

Andrie de Vries

288 |

2019-08-24

289 |
290 | 291 | 292 |
293 |
294 |
295 |
296 | 297 | 298 |
299 |
300 | 301 | 302 | 303 | 304 | 313 | 314 | 315 | 316 | 324 | 325 | 326 | 327 | 328 | 329 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 27 | 44 | 45 | 48 | 65 | 66 | 69 | 86 | 87 | 90 | 107 | 108 | 111 | 113 | 120 | 127 | 128 | 129 | 132 | 137 | 138 | 141 | 146 | 147 | 150 | 155 | 156 | 159 | 164 | 165 | 168 | 173 | 174 | 177 | 182 | 183 | 186 | 191 | 192 | 195 | 200 | 201 | 204 | 209 | 210 | 213 | 220 | 221 | 224 | 232 | 233 | 236 | 244 | 245 | 248 | 256 | 257 | 260 | 268 | 269 | 272 | 280 | 281 | 284 | 292 | 293 | 296 | 304 | 305 | 308 | 317 | 318 | 321 | 329 | 330 | 333 | 341 | 342 | 345 | 353 | 354 | 357 | 366 | 367 | 370 | 379 | 380 | 383 | 392 | 393 | 394 | 418 | 420 | 421 | 423 | image/svg+xml 424 | 426 | 427 | 428 | 429 | 430 | 435 | 452 | 456 | 465 | 474 | 483 | 492 | 493 | 516 | 519 | 528 | 537 | 546 | 555 | 556 | 558 | s n o s 619 | 622 | 631 | 640 | 649 | 650 | 652 | p r a 698 | 702 | 711 | 720 | 729 | 730 | 731 | 732 | -------------------------------------------------------------------------------- /man-roxygen/question_parsons_description.R: -------------------------------------------------------------------------------- 1 | #' @description 2 | #' 3 | #' Add Parsons Problem tasks to your `learnr` tutorials. The 4 | #' student can drag-and-drop the answer options into the desired order. 5 | #' 6 | #' This is a highly experimental, initial attempt at making Parsons problems in 7 | #' `learnr` tutorials. Parsons problems is a type of programming assignment 8 | #' where the student must order statements in the correct order. 9 | #' 10 | #' If the task also includes indentation of the statements, it's called a 11 | #' second-order Parsons problem. Note that second order problems have not yet 12 | #' been implemented. 13 | #' 14 | #' \if{html}{\figure{parsons_app_submit.png}{options: width="60\%" max-width="500px" alt="Figure: Parsons submit state"}} 15 | #' 16 | #' Features (design choices): 17 | #' 18 | #' * Items (except the last) in the right hand column will have a ` %>% ` 19 | #' appended. 20 | #' 21 | #' * Items (except the first) in the right hand column will automatically be 22 | #' indented. 23 | #' 24 | #' * The initial values are shuffled into random answer order. 25 | #' 26 | #' 27 | #' Limitations: 28 | #' 29 | #' * It does not do any code evaluation 30 | #' 31 | #' * It does not support indentation 32 | #' 33 | #' * It assumes code is from the `tidyverse` and only supports the `magrittr` 34 | #' pipe ` %>% ` operator 35 | #' 36 | #' 37 | #' @section Creating a parsons question: 38 | #' 39 | #' Use `question_parsons` inside a `learnr` tutorial chunk 40 | #' 41 | #' For example: 42 | #' 43 | #' ```` 44 | #' ```{r iris} 45 | #' question_parsons( 46 | #' initial = c( 47 | #' "iris", 48 | #' "mutate(...)", 49 | #' "summarize(...)", 50 | #' "print()" 51 | #' ), 52 | #' answer(c( 53 | #' "iris", 54 | #' "mutate(...)", 55 | #' "summarize(...)" 56 | #' ), correct = TRUE) 57 | #' ) 58 | #' ``` 59 | #' ```` 60 | #' 61 | #' On initialization, the initial values are randomized: 62 | #' 63 | #' \if{html}{\figure{parsons_app_initial.png}{options: width="60\%" max-width="500px" alt="Figure: Parsons initial state"}} 64 | #' 65 | #' As the student drags values to the right column, the `magrittr` gets 66 | #' appended, and items are automatically indented: 67 | #' 68 | #' \if{html}{\figure{parsons_app_submit.png}{options: width="60\%" max-width="500px" alt="Figure: Parsons submit state"}} 69 | #' 70 | #' 71 | -------------------------------------------------------------------------------- /man/bucket_problem.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/bucket_problem.R 3 | \name{bucket_problem} 4 | \alias{bucket_problem} 5 | \title{Create a bucket problem (experimental).} 6 | \usage{ 7 | bucket_problem(initial, text = c("Drag from here", 8 | "Construct your solution here"), header = NULL, input_id, group_name, 9 | class = "default-sortable default-bucket", 10 | options = sortable_options(), orientation = c("horizontal", 11 | "vertical")) 12 | } 13 | \arguments{ 14 | \item{initial}{Vector with initial values for problem (to appear in left 15 | column). Note: this must be a super-set of all answers.} 16 | 17 | \item{text}{Vector of headings for each column.} 18 | 19 | \item{header}{Text that appears at the top of the bucket list. (This is 20 | encoded as an HTML \code{

} tag, so not strictly speaking a header.)} 21 | 22 | \item{input_id}{Character vector of \code{input_id} to pass (individually) to 23 | \code{\link[=rank_list]{rank_list()}}.} 24 | 25 | \item{group_name}{Passed to \code{SortableJS} as the group name. Also the input 26 | value set in Shiny. (\code{input[[group_name]]})} 27 | 28 | \item{class}{A css class applied to the rank list. This can be used to 29 | define custom styling.} 30 | 31 | \item{options}{Options to be supplied to \link{sortable_js} object. See \link{sortable_options} for more details} 32 | 33 | \item{orientation}{Either \code{horizontal} or \code{vertical}, and specifies the 34 | layout of the components on the page.} 35 | } 36 | \description{ 37 | This function implements the bucket problem, as exposed by 38 | \code{\link[=question_bucket]{question_bucket()}}. Most users will only use this function inside a 39 | \code{learnr} tutorial, so please see the documentation at \code{\link[=question_bucket]{question_bucket()}} 40 | } 41 | -------------------------------------------------------------------------------- /man/expectations.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/expectations.R 3 | \name{pass_if} 4 | \alias{pass_if} 5 | \alias{fail_if} 6 | \alias{message_if} 7 | \alias{contains_all} 8 | \alias{contains_any} 9 | \title{Add expectations to a parsons problem.} 10 | \usage{ 11 | pass_if(f, message = NULL) 12 | 13 | fail_if(f, message = "Incorrect") 14 | 15 | message_if(f) 16 | 17 | contains_all(x, ...) 18 | 19 | contains_any(x, ...) 20 | } 21 | \arguments{ 22 | \item{f}{One of: 23 | \itemize{ 24 | \item A character vector, indicating an exact match 25 | \item A function of the function \code{function(x){...}} that evaluates to TRUE or FALSE 26 | \item A function of the form \code{~ .}, as used by the tidy evaluation, e.g. in \link[purrr:map]{purrr::map} 27 | }} 28 | 29 | \item{message}{Message to display if \code{fun} evaluates to TRUE} 30 | 31 | \item{x}{Answer to test} 32 | 33 | \item{...}{Combined into an answer list} 34 | } 35 | \description{ 36 | Add expectations to a parsons problem. 37 | } 38 | -------------------------------------------------------------------------------- /man/figures/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 27 | 44 | 45 | 48 | 65 | 66 | 69 | 86 | 87 | 90 | 107 | 108 | 111 | 113 | 120 | 127 | 128 | 129 | 132 | 137 | 138 | 141 | 146 | 147 | 150 | 155 | 156 | 159 | 164 | 165 | 168 | 173 | 174 | 177 | 182 | 183 | 186 | 191 | 192 | 195 | 200 | 201 | 204 | 209 | 210 | 213 | 220 | 221 | 224 | 232 | 233 | 236 | 244 | 245 | 248 | 256 | 257 | 260 | 268 | 269 | 272 | 280 | 281 | 284 | 292 | 293 | 296 | 304 | 305 | 308 | 317 | 318 | 321 | 329 | 330 | 333 | 341 | 342 | 345 | 353 | 354 | 357 | 366 | 367 | 370 | 379 | 380 | 383 | 392 | 393 | 394 | 418 | 420 | 421 | 423 | image/svg+xml 424 | 426 | 427 | 428 | 429 | 430 | 435 | 452 | 456 | 465 | 474 | 483 | 492 | 493 | 516 | 519 | 528 | 537 | 546 | 555 | 556 | 558 | s n o s 619 | 622 | 631 | 640 | 649 | 650 | 652 | p r a 698 | 702 | 711 | 720 | 729 | 730 | 731 | 732 | -------------------------------------------------------------------------------- /man/figures/parsons-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rstudio/parsons/42d0bfb8f23a3c8048cc4c1fbca95a0ef1e0bca3/man/figures/parsons-logo.png -------------------------------------------------------------------------------- /man/figures/parsons_app.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rstudio/parsons/42d0bfb8f23a3c8048cc4c1fbca95a0ef1e0bca3/man/figures/parsons_app.gif -------------------------------------------------------------------------------- /man/figures/parsons_app_initial.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rstudio/parsons/42d0bfb8f23a3c8048cc4c1fbca95a0ef1e0bca3/man/figures/parsons_app_initial.png -------------------------------------------------------------------------------- /man/figures/parsons_app_submit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rstudio/parsons/42d0bfb8f23a3c8048cc4c1fbca95a0ef1e0bca3/man/figures/parsons_app_submit.png -------------------------------------------------------------------------------- /man/parsons.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/parsons-package.R 3 | \docType{package} 4 | \name{parsons} 5 | \alias{parsons} 6 | \alias{_PACKAGE} 7 | \alias{parsons-package} 8 | \title{parsons: Create Parsons Programming Problems in 'learnr` Tutorials} 9 | \description{ 10 | Create Parsons Programming Problems in 'learnr' Tutorials. 11 | } 12 | \seealso{ 13 | Useful links: 14 | \itemize{ 15 | \item \url{https://github.com/rstudio/parsons} 16 | \item Report bugs at \url{https://github.com/rstudio/parsons/issues} 17 | } 18 | 19 | } 20 | \author{ 21 | \strong{Maintainer}: Andrie de Vries \email{apdevries@gmail.com} 22 | 23 | Other contributors: 24 | \itemize{ 25 | \item Barret Schloerke \email{barret@rstudio.com} [contributor] 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /man/parsons_problem.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/parsons_problem.R 3 | \name{parsons_problem} 4 | \alias{parsons_problem} 5 | \title{Create a parsons problem (experimental).} 6 | \usage{ 7 | parsons_problem(initial, text = c("Drag from here", 8 | "Construct your solution here"), header = NULL, input_id, group_name, 9 | problem_type = c("base", "ggplot2", "tidyverse"), 10 | class = "default-sortable default-parsons", 11 | options = sortable_options(), orientation = c("horizontal", 12 | "vertical")) 13 | } 14 | \arguments{ 15 | \item{initial}{Vector with initial values for problem (to appear in left 16 | column). Note: this must be a super-set of all answers.} 17 | 18 | \item{text}{Vector of headings for each column.} 19 | 20 | \item{header}{Text that appears at the top of the bucket list. (This is 21 | encoded as an HTML \code{

} tag, so not strictly speaking a header.)} 22 | 23 | \item{input_id}{Character vector of \code{input_id} to pass (individually) to 24 | \code{\link[=rank_list]{rank_list()}}.} 25 | 26 | \item{group_name}{Passed to \code{SortableJS} as the group name. Also the input 27 | value set in Shiny. (\code{input[[group_name]]})} 28 | 29 | \item{problem_type}{One of \code{base}, \code{ggplot2} or \code{tidyverse}, indicating the type of 30 | problem statement. For \code{tidyverse}, the resulting answer will 31 | automatically append a \code{\%>\%} at the end of each answer, and for \code{ggplot2} 32 | every line will be followed by a \code{+}.} 33 | 34 | \item{class}{A css class applied to the rank list. This can be used to 35 | define custom styling.} 36 | 37 | \item{options}{Options to be supplied to \link{sortable_js} object. See \link{sortable_options} for more details} 38 | 39 | \item{orientation}{Either \code{horizontal} or \code{vertical}, and specifies the 40 | layout of the components on the page.} 41 | } 42 | \description{ 43 | This function implements the parsons problem, as exposed by 44 | \code{\link[=question_parsons]{question_parsons()}}. Most users will only use this function inside a 45 | \code{learnr} tutorial, so please see the documentation at \code{\link[=question_parsons]{question_parsons()}} 46 | } 47 | \examples{ 48 | ## Example of a shiny app 49 | if (interactive()) { 50 | app <- system.file("shiny-examples/parsons_app.R", package = "parsons") 51 | shiny::runApp(app) 52 | } 53 | ## -- example-parsons ----------------------------------------------------- 54 | 55 | ## Parson's problem 56 | 57 | parsons_problem( 58 | header = "This is an example of a Parsons problem", 59 | text = c("Drag from here", "Construct your solution here"), 60 | initial = c( 61 | "iris", 62 | "mutate(...)", 63 | "summarize(...)", 64 | "print()" 65 | ), 66 | input_id = "input_parsons" 67 | ) 68 | 69 | 70 | parsons_problem( 71 | initial = c( 72 | "iris", 73 | "mutate(...)", 74 | "summarize(...)", 75 | "print()" 76 | ), 77 | input_id = "input_parsons" 78 | ) 79 | 80 | } 81 | \references{ 82 | https://js-parsons.github.io/ 83 | } 84 | -------------------------------------------------------------------------------- /man/question_bucket.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/question_bucket.R 3 | \name{question_bucket} 4 | \alias{question_bucket} 5 | \title{bucket problem question for learnr tutorials (experimental).} 6 | \usage{ 7 | question_bucket(initial, ..., text = c("Drag from here", 8 | "Construct your solution here"), orientation = c("horizontal", 9 | "vertical"), correct = "Correct!", incorrect = "Incorrect", 10 | try_again = incorrect, message = NULL, post_message = NULL, 11 | loading = c("Loading: "), submit_button = "Give feedback", 12 | try_again_button = "Try Again", allow_retry = TRUE, 13 | random_answer_order = TRUE, options = sortable_options()) 14 | } 15 | \arguments{ 16 | \item{initial}{Initial value of answer options. This must be a character vector.} 17 | 18 | \item{...}{One or more answers. Passed to \code{\link[learnr:question]{learnr::question()}}.} 19 | 20 | \item{text}{Question or option text} 21 | 22 | \item{orientation}{Either \code{horizontal} or \code{vertical}, and specifies the 23 | layout of the components on the page.} 24 | 25 | \item{correct}{Text to print for a correct answer (defaults to "Correct!")} 26 | 27 | \item{incorrect}{Text to print for an incorrect answer (defaults to "Incorrect") 28 | when \code{allow_retry} is \code{FALSE}.} 29 | 30 | \item{try_again}{Text to print for an incorrect answer (defaults to "Incorrect") 31 | when \code{allow_retry} is \code{TRUE}.} 32 | 33 | \item{message}{Additional message to display along with correct/incorrect feedback. 34 | This message is always displayed after a question submission.} 35 | 36 | \item{post_message}{Additional message to display along with correct/incorrect feedback. 37 | If \code{allow_retry} is \code{TRUE}, this message will only be displayed after the 38 | correct submission. If \code{allow_retry} is \code{FALSE}, it will produce a second 39 | message alongside the \code{message} message value.} 40 | 41 | \item{loading}{Loading text to display as a placeholder while the question is loaded} 42 | 43 | \item{submit_button}{Label for the submit button. Defaults to \code{"Submit Answer"}} 44 | 45 | \item{try_again_button}{Label for the try again button. Defaults to \code{"Submit Answer"}} 46 | 47 | \item{allow_retry}{Allow retry for incorrect answers. Defaults to \code{FALSE}.} 48 | 49 | \item{random_answer_order}{Display answers in a random order.} 50 | 51 | \item{options}{Extra options to be stored in the question object.} 52 | } 53 | \description{ 54 | bucket problem question for learnr tutorials (experimental). 55 | } 56 | \examples{ 57 | ## Example of bucket problem inside a learn tutorial 58 | if (interactive()) { 59 | learnr::run_tutorial("bucket", package = "parsons") 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /man/question_parsons.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/question_parsons.R 3 | \name{question_parsons} 4 | \alias{question_parsons} 5 | \title{Parsons problem question for learnr tutorials (experimental).} 6 | \usage{ 7 | question_parsons(initial, ..., problem_type = c("base", "ggplot2", 8 | "tidyverse"), orientation = c("horizontal", "vertical"), 9 | correct = "Correct!", incorrect = "Incorrect", 10 | try_again = incorrect, message = NULL, post_message = NULL, 11 | loading = c("Loading: "), submit_button = "Give feedback", 12 | try_again_button = "Try Again", allow_retry = TRUE, 13 | random_answer_order = TRUE, options = sortable_options()) 14 | } 15 | \arguments{ 16 | \item{initial}{Initial value of answer options. This must be a character vector.} 17 | 18 | \item{...}{One or more answers. Passed to \code{\link[learnr:question]{learnr::question()}}.} 19 | 20 | \item{problem_type}{One of \code{base}, \code{ggplot2} or \code{tidyverse}, indicating the type of 21 | problem statement. For \code{tidyverse}, the resulting answer will 22 | automatically append a \code{\%>\%} at the end of each answer, and for \code{ggplot2} 23 | every line will be followed by a \code{+}.} 24 | 25 | \item{orientation}{Either \code{horizontal} or \code{vertical}, and specifies the 26 | layout of the components on the page.} 27 | 28 | \item{correct}{Text to print for a correct answer (defaults to "Correct!")} 29 | 30 | \item{incorrect}{Text to print for an incorrect answer (defaults to "Incorrect") 31 | when \code{allow_retry} is \code{FALSE}.} 32 | 33 | \item{try_again}{Text to print for an incorrect answer (defaults to "Incorrect") 34 | when \code{allow_retry} is \code{TRUE}.} 35 | 36 | \item{message}{Additional message to display along with correct/incorrect feedback. 37 | This message is always displayed after a question submission.} 38 | 39 | \item{post_message}{Additional message to display along with correct/incorrect feedback. 40 | If \code{allow_retry} is \code{TRUE}, this message will only be displayed after the 41 | correct submission. If \code{allow_retry} is \code{FALSE}, it will produce a second 42 | message alongside the \code{message} message value.} 43 | 44 | \item{loading}{Loading text to display as a placeholder while the question is loaded} 45 | 46 | \item{submit_button}{Label for the submit button. Defaults to \code{"Submit Answer"}} 47 | 48 | \item{try_again_button}{Label for the try again button. Defaults to \code{"Submit Answer"}} 49 | 50 | \item{allow_retry}{Allow retry for incorrect answers. Defaults to \code{FALSE}.} 51 | 52 | \item{random_answer_order}{Display answers in a random order.} 53 | 54 | \item{options}{Extra options to be stored in the question object.} 55 | } 56 | \description{ 57 | Add Parsons Problem tasks to your \code{learnr} tutorials. The 58 | student can drag-and-drop the answer options into the desired order. 59 | 60 | This is a highly experimental, initial attempt at making Parsons problems in 61 | \code{learnr} tutorials. Parsons problems is a type of programming assignment 62 | where the student must order statements in the correct order. 63 | 64 | If the task also includes indentation of the statements, it's called a 65 | second-order Parsons problem. Note that second order problems have not yet 66 | been implemented. 67 | 68 | \if{html}{\figure{parsons_app_submit.png}{options: width="60\%" max-width="500px" alt="Figure: Parsons submit state"}} 69 | 70 | Features (design choices): 71 | \itemize{ 72 | \item Items (except the last) in the right hand column will have a \code{\%>\%} 73 | appended. 74 | \item Items (except the first) in the right hand column will automatically be 75 | indented. 76 | \item The initial values are shuffled into random answer order. 77 | } 78 | 79 | Limitations: 80 | \itemize{ 81 | \item It does not do any code evaluation 82 | \item It does not support indentation 83 | \item It assumes code is from the \code{tidyverse} and only supports the \code{magrittr} 84 | pipe \code{\%>\%} operator 85 | } 86 | } 87 | \section{Creating a parsons question}{ 88 | 89 | 90 | Use \code{question_parsons} inside a \code{learnr} tutorial chunk 91 | 92 | For example:\preformatted{```{r iris} 93 | question_parsons( 94 | initial = c( 95 | "iris", 96 | "mutate(...)", 97 | "summarize(...)", 98 | "print()" 99 | ), 100 | answer(c( 101 | "iris", 102 | "mutate(...)", 103 | "summarize(...)" 104 | ), correct = TRUE) 105 | ) 106 | ``` 107 | } 108 | 109 | On initialization, the initial values are randomized: 110 | 111 | \if{html}{\figure{parsons_app_initial.png}{options: width="60\%" max-width="500px" alt="Figure: Parsons initial state"}} 112 | 113 | As the student drags values to the right column, the \code{magrittr} gets 114 | appended, and items are automatically indented: 115 | 116 | \if{html}{\figure{parsons_app_submit.png}{options: width="60\%" max-width="500px" alt="Figure: Parsons submit state"}} 117 | } 118 | 119 | \examples{ 120 | ## Example of parsons problem inside a learn tutorial 121 | if (interactive()) { 122 | learnr::run_tutorial("parsons", package = "parsons") 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /man/question_ui_try_again.bucket_question.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/question_bucket.R 3 | \name{question_ui_try_again.bucket_question} 4 | \alias{question_ui_try_again.bucket_question} 5 | \title{Disable input after student submitted answer.} 6 | \usage{ 7 | \method{question_ui_try_again}{bucket_question}(question, answer_input, 8 | ...) 9 | } 10 | \arguments{ 11 | \item{question}{Question object} 12 | 13 | \item{answer_input}{user input value} 14 | 15 | \item{...}{not used} 16 | } 17 | \description{ 18 | Disable input after student submitted answer. 19 | } 20 | -------------------------------------------------------------------------------- /man/question_ui_try_again.parsons_question.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/question_parsons.R 3 | \name{question_ui_try_again.parsons_question} 4 | \alias{question_ui_try_again.parsons_question} 5 | \title{Disable input after student submitted answer.} 6 | \usage{ 7 | \method{question_ui_try_again}{parsons_question}(question, answer_input, 8 | ...) 9 | } 10 | \arguments{ 11 | \item{question}{Question object} 12 | 13 | \item{answer_input}{user input value} 14 | 15 | \item{...}{not used} 16 | } 17 | \description{ 18 | Disable input after student submitted answer. 19 | } 20 | -------------------------------------------------------------------------------- /parsons.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: Default 4 | SaveWorkspace: Default 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: XeLaTeX 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,vignette 22 | -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rstudio/parsons/42d0bfb8f23a3c8048cc4c1fbca95a0ef1e0bca3/pkgdown/favicon/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rstudio/parsons/42d0bfb8f23a3c8048cc4c1fbca95a0ef1e0bca3/pkgdown/favicon/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rstudio/parsons/42d0bfb8f23a3c8048cc4c1fbca95a0ef1e0bca3/pkgdown/favicon/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rstudio/parsons/42d0bfb8f23a3c8048cc4c1fbca95a0ef1e0bca3/pkgdown/favicon/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rstudio/parsons/42d0bfb8f23a3c8048cc4c1fbca95a0ef1e0bca3/pkgdown/favicon/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rstudio/parsons/42d0bfb8f23a3c8048cc4c1fbca95a0ef1e0bca3/pkgdown/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /pkgdown/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rstudio/parsons/42d0bfb8f23a3c8048cc4c1fbca95a0ef1e0bca3/pkgdown/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /pkgdown/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rstudio/parsons/42d0bfb8f23a3c8048cc4c1fbca95a0ef1e0bca3/pkgdown/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /pkgdown/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rstudio/parsons/42d0bfb8f23a3c8048cc4c1fbca95a0ef1e0bca3/pkgdown/favicon/favicon.ico -------------------------------------------------------------------------------- /scripts/compile_css.R: -------------------------------------------------------------------------------- 1 | 2 | if (!require(sass)) { 3 | install.packages("sass") 4 | } 5 | library(sass) 6 | 7 | scss_files <- dir( 8 | file.path("inst", "htmlwidgets", "plugins", "parsons"), 9 | pattern = "^[^_].*?\\.scss", 10 | full.names = TRUE 11 | ) 12 | 13 | for (scss_file in scss_files) { 14 | message("Translating: ", basename(scss_file)) 15 | sass::sass( 16 | input = sass::sass_file(scss_file), 17 | output = sub("\\.scss", ".css", scss_file) 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /scripts/deploy_apps.R: -------------------------------------------------------------------------------- 1 | 2 | if (!requireNamespace("remotes")) install.packages("remotes") 3 | 4 | # install rsconnect and glue 5 | remotes::install_cran("rsconnect") 6 | remotes::install_cran("glue") 7 | 8 | # install the latest 9 | remotes::install_github("rstudio/sortable", upgrade = "always") 10 | 11 | 12 | 13 | deploy_app <- function( 14 | app_dir, 15 | name = glue::glue("sortable_{basename(app_dir)}_app"), 16 | ... 17 | ) { 18 | cat("\n\n\n") 19 | message("Deploying: ", name) 20 | cat("\n") 21 | rsconnect::deployApp( 22 | appDir = app_dir, 23 | appName = name, 24 | server = "shinyapps.io", 25 | account = "andrie-de-vries", 26 | forceUpdate = TRUE, 27 | ... 28 | ) 29 | } 30 | 31 | deploy_tutorial <- function( 32 | app_dir, 33 | doc = dir(app_dir, pattern = "\\.Rmd$")[1], 34 | name = glue::glue("sortable_tutorial_{basename(app_dir)}") 35 | ) { 36 | deploy_app( 37 | app_dir = app_dir, 38 | name = name, 39 | appPrimaryDoc = dir(app_dir, pattern = "\\.Rmd$")[1] 40 | ) 41 | } 42 | 43 | 44 | deploy_folder <- function(path, fn) { 45 | lapply( 46 | dir(path, full.names = TRUE), 47 | function(path) { 48 | if (dir.exists(path)) { 49 | fn(path) 50 | } 51 | } 52 | ) 53 | } 54 | 55 | deploy_folder("inst/shiny-examples", deploy_app) 56 | deploy_folder("inst/tutorials", deploy_tutorial) 57 | 58 | message("done") 59 | -------------------------------------------------------------------------------- /scripts/deploy_on_travis.R: -------------------------------------------------------------------------------- 1 | 2 | 3 | if (!requireNamespace("remotes")) install.packages("remotes") 4 | remotes::install_cran("rsconnect") 5 | 6 | # Set the account info for deployment. 7 | rsconnect::setAccountInfo( 8 | name = Sys.getenv("shinyapps_name"), 9 | token = Sys.getenv("shinyapps_token"), 10 | secret = Sys.getenv("shinyapps_secret") 11 | ) 12 | 13 | # deploy all apps 14 | source("scripts/deploy_apps.R") 15 | -------------------------------------------------------------------------------- /scripts/load_all_shim.R: -------------------------------------------------------------------------------- 1 | # Makes it easy to test the package in development by shimming `htmlwidgets` 2 | # and `htmltools`, before `load_all()`. 3 | # 4 | # Solution by Winston Chang (https://gist.github.com/wch/c942335660dc6c96322f) 5 | 6 | local({ 7 | shim_system_file <- function(package) { 8 | imports <- parent.env(asNamespace(package)) 9 | pkgload:::unlock_environment(imports) 10 | imports$system.file <- pkgload:::shim_system.file 11 | } 12 | 13 | shim_system_file("htmlwidgets") 14 | shim_system_file("htmltools") 15 | }) 16 | 17 | devtools::load_all() 18 | -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | library(testthat) 2 | library(parsons) 3 | 4 | test_check("parsons") 5 | -------------------------------------------------------------------------------- /tests/testthat/test-expectations.R: -------------------------------------------------------------------------------- 1 | context("expectations") 2 | 3 | test_that("pass_if works", { 4 | 5 | expect_is( 6 | pass_if("x"), 7 | "tutorial_question_answer" 8 | ) 9 | 10 | expect_is( 11 | pass_if(~x == 1, "1"), 12 | "parsons_expectation_pass" 13 | ) 14 | 15 | expect_is( 16 | pass_if(function(x)x == 1, "1"), 17 | "parsons_expectation_pass" 18 | ) 19 | 20 | }) 21 | 22 | test_that("fail_if works", { 23 | 24 | expect_is( 25 | fail_if("x"), 26 | "tutorial_question_answer" 27 | ) 28 | 29 | expect_is( 30 | fail_if(~x == 1, "1"), 31 | "parsons_expectation_fail" 32 | ) 33 | 34 | expect_is( 35 | fail_if(function(x)x == 1, "1"), 36 | "parsons_expectation_fail" 37 | ) 38 | 39 | expect_is( 40 | fail_if(function(x){!"print()" %in% x}, "1"), 41 | "parsons_expectation_fail" 42 | ) 43 | 44 | }) 45 | -------------------------------------------------------------------------------- /tests/testthat/test-learnr-question_parsons.R: -------------------------------------------------------------------------------- 1 | context("learnr question_parsons") 2 | 3 | test_that( "init display validates", { 4 | 5 | question <- question_parsons( 6 | # "Sort the first 5 letters", 7 | initial = LETTERS[1:5], 8 | learnr::answer(LETTERS[1:5], correct = TRUE), 9 | learnr::answer(rev(LETTERS[1:5]), correct = FALSE, "Other direction!") 10 | ) 11 | expect_is(question, "parsons_question") 12 | 13 | expect_silent({ 14 | question_ui_initialize(question, "ignored") 15 | }) 16 | 17 | expect_silent({ 18 | question_ui_completed(question, LETTERS[5:1]) 19 | }) 20 | expect_silent( 21 | question_ui_try_again(question, rev(LETTERS[1:5])) 22 | ) 23 | 24 | expect_true( 25 | question_is_valid(question, letters[1:5]) 26 | ) 27 | expect_false( 28 | question_is_valid(question, NULL) 29 | ) 30 | 31 | # expect_identical( 32 | # question_is_correct(question, LETTERS[1:5]), 33 | # learnr::question_is_correct(TRUE, NULL) 34 | # ) 35 | tmp_answer <- learnr::answer("ignored", FALSE, "Other direction!") 36 | # expect_identical( 37 | # question_is_correct(question, LETTERS[5:1]), 38 | # learnr::question_is_correct_value(FALSE, tmp_answer$message) 39 | # ) 40 | # expect_identical( 41 | # question_is_correct(question, letters[1:5]), 42 | # learnr::question_is_correct_value(FALSE, NULL) 43 | # ) 44 | 45 | }) 46 | -------------------------------------------------------------------------------- /tests/testthat/test-parsons.R: -------------------------------------------------------------------------------- 1 | context("parsons") 2 | 3 | test_that("Can create parsons", { 4 | library(learnr) 5 | z <- parsons_problem( 6 | header = "This is an example of a Parsons problem", 7 | initial = c( 8 | "iris", 9 | "mutate(...)", 10 | "summarize(...)", 11 | "print()" 12 | ), 13 | answer(c( 14 | "iris", 15 | "mutate(...)", 16 | "summarize(...)", 17 | "print()" 18 | )), 19 | input_id = "test-parsons" 20 | ) 21 | 22 | expect_is(z, "parsons_problem") 23 | }) 24 | -------------------------------------------------------------------------------- /vignettes/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | *.R 3 | -------------------------------------------------------------------------------- /vignettes/introduction.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "introduction" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{introduction} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | ```{r, include = FALSE} 11 | knitr::opts_chunk$set( 12 | collapse = TRUE, 13 | comment = "#>" 14 | ) 15 | ``` 16 | 17 | ```{r setup} 18 | library(parsons) 19 | ``` 20 | --------------------------------------------------------------------------------