├── .gitignore
├── .github
├── .gitignore
└── workflows
│ └── R-CMD-check.yaml
├── LICENSE
├── tests
├── testthat.R
└── testthat
│ ├── test-name-doesnt-exist.txt
│ ├── test-permission-granted.txt
│ ├── test-checking-permissions.txt
│ ├── test-find-existing-cabinets.txt
│ ├── test-print-available.txt
│ ├── test-none-available.txt
│ ├── test-project-doesnt-exist.txt
│ ├── test-paths.R
│ ├── test-options.R
│ ├── test-file-cabinet.txt
│ ├── test-FileCabinet.R
│ ├── test-check_cabinet.R
│ ├── test-projects.R
│ ├── test-name.R
│ ├── test-blocks.R
│ ├── test-feedback.txt
│ ├── test-permissions.R
│ └── test-feedback.R
├── .Rbuildignore
├── codecov.yml
├── NAMESPACE
├── man
├── delete_cabinet.Rd
├── edit_r_profile.Rd
├── get_cabinets.Rd
├── cabinets_options_set.Rd
├── cabinets.Rd
├── create_cabinet.Rd
├── new_cabinet_proj.Rd
└── FileCabinet.Rd
├── cabinets.Rproj
├── R
├── edit.R
├── git.R
├── cabinets-package.R
├── block.R
├── feedback.R
├── checks.R
├── utils.R
└── cabinets.R
├── cran-comments.md
├── LICENSE.md
├── NEWS.md
├── DESCRIPTION
├── CODE_OF_CONDUCT.md
├── README.md
└── README.Rmd
/.gitignore:
--------------------------------------------------------------------------------
1 | .Rproj.user
2 |
--------------------------------------------------------------------------------
/.github/.gitignore:
--------------------------------------------------------------------------------
1 | *.html
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | YEAR: 2019
2 | COPYRIGHT HOLDER: Nicholas Williams
3 |
--------------------------------------------------------------------------------
/tests/testthat.R:
--------------------------------------------------------------------------------
1 | library(testthat)
2 | library(cabinets)
3 |
4 | test_check("cabinets")
5 |
--------------------------------------------------------------------------------
/tests/testthat/test-name-doesnt-exist.txt:
--------------------------------------------------------------------------------
1 | > check_name("test_cab")
2 | Message: PASSING Checking cabinet name
3 |
4 |
--------------------------------------------------------------------------------
/tests/testthat/test-permission-granted.txt:
--------------------------------------------------------------------------------
1 | > ask_permission()
2 | Message: PASSING Checking for permissions
3 |
4 |
--------------------------------------------------------------------------------
/tests/testthat/test-checking-permissions.txt:
--------------------------------------------------------------------------------
1 | > check_permissions()
2 | Message: PASSING Checking for permissions
3 |
4 |
--------------------------------------------------------------------------------
/tests/testthat/test-find-existing-cabinets.txt:
--------------------------------------------------------------------------------
1 | > check_cabinet(".test_cab")
2 | Message: PASSING Checking cabinet existence
3 |
4 |
--------------------------------------------------------------------------------
/tests/testthat/test-print-available.txt:
--------------------------------------------------------------------------------
1 | > get_cabinets(env)
2 | Message: These are the available cabinets:
3 |
4 | Message: * x
5 |
6 |
--------------------------------------------------------------------------------
/tests/testthat/test-none-available.txt:
--------------------------------------------------------------------------------
1 | > get_cabinets(env)
2 | Message: [31mx[39m No cabinets found. Cabinets can be created using `create_cabinets()`
3 |
4 |
--------------------------------------------------------------------------------
/tests/testthat/test-project-doesnt-exist.txt:
--------------------------------------------------------------------------------
1 | > check_project(file.path("a", "random", "path"))
2 | Message: PASSING Checking if project already exists
3 |
4 |
--------------------------------------------------------------------------------
/tests/testthat/test-paths.R:
--------------------------------------------------------------------------------
1 |
2 | context("Path specific")
3 |
4 | test_that("Parse paths correctly", {
5 | expect_equal(get_paths("hello/world/test"), c("hello/world/test", "hello/world", "hello"))
6 | })
7 |
--------------------------------------------------------------------------------
/tests/testthat/test-options.R:
--------------------------------------------------------------------------------
1 | context("setting cabinet options")
2 |
3 | test_that("options can be set", {
4 | cabinets_options_set("cabinets.permission" = FALSE)
5 | expect_false(getOption("cabinets.permission"))
6 | })
7 |
--------------------------------------------------------------------------------
/.Rbuildignore:
--------------------------------------------------------------------------------
1 | ^cabinets\.Rproj$
2 | ^\.Rproj\.user$
3 | ^\.travis\.yml$
4 | ^codecov\.yml$
5 | ^LICENSE\.md$
6 | ^cran-comments\.md$
7 | ^CRAN-RELEASE$
8 | ^CODE_OF_CONDUCT\.md$
9 | ^README\.md$
10 | ^README\.Rmd$
11 | ^\.github$
12 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | comment: false
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 |
--------------------------------------------------------------------------------
/tests/testthat/test-file-cabinet.txt:
--------------------------------------------------------------------------------
1 | > FileCabinet$new(name = "test", directory = "hello/world", structure = list(
2 | + `hello/world` = NULL))
3 | Cabinet name: test
4 | Cabinet path: hello/world
5 | Cabinet structure:
6 | └── hello
7 | └── world
8 |
9 |
--------------------------------------------------------------------------------
/NAMESPACE:
--------------------------------------------------------------------------------
1 | # Generated by roxygen2: do not edit by hand
2 |
3 | export(FileCabinet)
4 | export(cabinets_options_set)
5 | export(create_cabinet)
6 | export(delete_cabinet)
7 | export(edit_r_profile)
8 | export(get_cabinets)
9 | export(new_cabinet_proj)
10 | importFrom(R6,R6Class)
11 | importFrom(utils,capture.output)
12 |
--------------------------------------------------------------------------------
/tests/testthat/test-FileCabinet.R:
--------------------------------------------------------------------------------
1 | context("create FileCabinet")
2 |
3 | test_that("creating FileCabinet", {
4 | x <- FileCabinet$new(name = "test_cab",
5 | directory = "a/random/path",
6 | structure = list('test' = NULL))
7 |
8 | expect_output(x$print())
9 | })
10 |
--------------------------------------------------------------------------------
/man/delete_cabinet.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/edit.R
3 | \name{delete_cabinet}
4 | \alias{delete_cabinet}
5 | \title{Delete Cabinets}
6 | \usage{
7 | delete_cabinet(cabinet)
8 | }
9 | \arguments{
10 | \item{cabinet}{An R6 object of class Cabinet written to the .Rprofile.}
11 | }
12 | \description{
13 | Delete Cabinets
14 | }
15 |
--------------------------------------------------------------------------------
/tests/testthat/test-check_cabinet.R:
--------------------------------------------------------------------------------
1 | context("check_cabinet function")
2 |
3 | test_that("finding existing cabinets", {
4 | withr::with_environment(.GlobalEnv, {
5 | .test_cab <<- NULL
6 | cli::cli_div(theme = list(".alert-success" = list(before = "PASSING ")))
7 | verify_output(test_path("test-find-existing-cabinets.txt"), {
8 | check_cabinet(".test_cab")
9 | })
10 | rm(.test_cab, envir = .GlobalEnv)
11 | })
12 | })
13 |
--------------------------------------------------------------------------------
/cabinets.Rproj:
--------------------------------------------------------------------------------
1 | Version: 1.0
2 |
3 | RestoreWorkspace: No
4 | SaveWorkspace: No
5 | AlwaysSaveHistory: Default
6 |
7 | EnableCodeIndexing: Yes
8 | UseSpacesForTab: Yes
9 | NumSpacesForTab: 4
10 | Encoding: UTF-8
11 |
12 | RnwWeave: Sweave
13 | LaTeX: pdfLaTeX
14 |
15 | AutoAppendNewline: Yes
16 | StripTrailingWhitespace: Yes
17 |
18 | BuildType: Package
19 | PackageUseDevtools: Yes
20 | PackageInstallArgs: --no-multiarch --with-keep.source
21 | PackageRoxygenize: rd,collate,namespace
22 |
--------------------------------------------------------------------------------
/R/edit.R:
--------------------------------------------------------------------------------
1 |
2 | cab_start <- "## {name} cabinet start"
3 | cab_end <- "## {name} cabinet end"
4 |
5 | #' Delete Cabinets
6 | #'
7 | #' @param cabinet An R6 object of class Cabinet written to the .Rprofile.
8 | #'
9 | #' @export
10 | delete_cabinet <- function(cabinet) {
11 | name <- cabinet$name
12 | cab_start <- glue::glue(cab_start)
13 | cab_end <- glue::glue(cab_end)
14 | block_delete(cab_start, cab_end)
15 | on.exit(rm(list = paste0(".", name), envir = .GlobalEnv))
16 | }
17 |
--------------------------------------------------------------------------------
/cran-comments.md:
--------------------------------------------------------------------------------
1 | ## Version 0.6.0
2 |
3 | ## Test environments
4 |
5 | * local OS X install, R 4.0.3
6 | * Windows (GitHub Actions), R 4.0.3
7 | * Windows (GitHub Actions), R 3.6
8 | * Windows (Win Builder), R devel
9 | * Ubuntu 16.04 (GitHub Actions), R devel
10 | * Ubuntu 16.04 (GitHub Actions), R 4.0.3
11 | * Ubuntu 16.04 (GitHub Actions), R 3.5
12 |
13 | ## R CMD check results
14 |
15 | 0 errors | 0 warnings | 0 notes
16 |
17 | ## Downstream dependencies
18 |
19 | There are no downstream dependencies
20 |
21 |
22 |
--------------------------------------------------------------------------------
/tests/testthat/test-projects.R:
--------------------------------------------------------------------------------
1 | context("pre-existing projects")
2 |
3 | test_that("project directory already exists", {
4 | temp_dir <- tempdir()
5 | expect_error(check_project(temp_dir))
6 | unlink(temp_dir)
7 | })
8 |
9 | skip_if(!cli::is_utf8_output())
10 |
11 | test_that("project doesn't exist", {
12 | cli::cli_div(theme = list(".alert-success" = list(before = "PASSING ")))
13 | verify_output(test_path("test-project-doesnt-exist.txt"), {
14 | check_project(file.path("a", "random", "path"))
15 | })
16 | })
17 |
--------------------------------------------------------------------------------
/man/edit_r_profile.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/cabinets.R
3 | \name{edit_r_profile}
4 | \alias{edit_r_profile}
5 | \title{Open .Rprofile for editing}
6 | \usage{
7 | edit_r_profile()
8 | }
9 | \value{
10 | A message that .Rprofile is being opened or that it doesn't exist.
11 | }
12 | \description{
13 | \code{edit_r_profile} opens the .Rprofile file for editing.
14 | If the .Rprofile file doesn't exist an error message will be returned.
15 | This is essentially a wrapper function for \code{file.edit}.
16 | }
17 |
--------------------------------------------------------------------------------
/man/get_cabinets.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/feedback.R
3 | \name{get_cabinets}
4 | \alias{get_cabinets}
5 | \title{Print available cabinets}
6 | \usage{
7 | get_cabinets(envir = parent.frame())
8 | }
9 | \arguments{
10 | \item{envir}{The environment to check in. The default is the global environment.}
11 | }
12 | \value{
13 | Objects of class FileCabinet found in the global environment.
14 | }
15 | \description{
16 | \code{get_cabinets} returns objects of class FileCabinet.
17 | }
18 | \examples{
19 | get_cabinets()
20 | }
21 |
--------------------------------------------------------------------------------
/tests/testthat/test-name.R:
--------------------------------------------------------------------------------
1 | context("check name")
2 |
3 | test_that("name already exists", {
4 | withr::with_environment(.GlobalEnv, {
5 | .test_cab <<- NULL
6 | expect_error(check_name("test_cab"))
7 | rm(.test_cab, envir = .GlobalEnv)
8 | })
9 | })
10 |
11 | test_that("name doesn't exist", {
12 | withr::with_environment(.GlobalEnv, {
13 | cli::cli_div(theme = list(".alert-success" = list(before = "PASSING ")))
14 | verify_output(test_path("test-name-doesnt-exist.txt"), {
15 | check_name("test_cab")
16 | })
17 | })
18 | })
19 |
--------------------------------------------------------------------------------
/R/git.R:
--------------------------------------------------------------------------------
1 |
2 | use_git <- function(git_root, git_ignore = NULL) {
3 | status <- tryCatch(
4 | if (check_git()) {
5 | init_git(git_root, git_ignore)
6 | } else {
7 | warning()
8 | }, warning = function(w) {
9 | no_git()
10 | }
11 | )
12 | invisible(status)
13 | }
14 |
15 | init_git <- function(git_root, git_ignore = NULL) {
16 | ignores <- c(".Rproj.user", ".Rhistory", ".Rdata", ".Ruserdata")
17 | if (!is.null(git_ignore)) ignores <- c(ignores, git_ignore)
18 | ignores <- paste0(ignores, "\n", collapse = "")
19 | git2r::init(git_root)
20 | gi <- file.path(git_root, ".gitignore")
21 | writeLines(ignores, gi)
22 | initiated_git(git_root)
23 | }
24 |
--------------------------------------------------------------------------------
/man/cabinets_options_set.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/checks.R
3 | \name{cabinets_options_set}
4 | \alias{cabinets_options_set}
5 | \title{Set cabinets options}
6 | \usage{
7 | cabinets_options_set(..., .envir = NULL)
8 | }
9 | \arguments{
10 | \item{...}{Options to be set}
11 |
12 | \item{.envir}{Environment to set options in; if NULL, will use environment at \code{parent.frame()}}
13 | }
14 | \value{
15 | If no options are set, returns the options specified in \code{options}.
16 | }
17 | \description{
18 | Set cabinets options
19 | }
20 | \details{
21 | Mainly used for specifying if cabinets has permission to write to .Rprofile. Permission can be revoked at any time by opening the .Rprofile file and setting \code{"cabinets.permission" = FALSE}.
22 | }
23 |
--------------------------------------------------------------------------------
/tests/testthat/test-blocks.R:
--------------------------------------------------------------------------------
1 | context("Blocks")
2 |
3 | test_that("deleting blocks", {
4 | withr::with_tempfile("block", {
5 | con <- file(block, open = "a")
6 | writeLines("blank space", con = con)
7 | writeLines(glue::glue("## test cabinet start"), con = con)
8 | writeLines("Text to delete", con = con)
9 | writeLines(glue::glue("## test cabinet end"), con = con)
10 | writeLines("blank space", con = con)
11 | close(con)
12 |
13 | expect_equal(read_utf8(block),
14 | c("blank space", "## test cabinet start",
15 | "Text to delete", "## test cabinet end",
16 | "blank space"))
17 | block_delete("## test cabinet start", "## test cabinet end", block)
18 | expect_equal(read_utf8(block), c("blank space", "blank space"))
19 | }, fileext = "txt")
20 | })
21 |
22 |
--------------------------------------------------------------------------------
/R/cabinets-package.R:
--------------------------------------------------------------------------------
1 | #' Streamlined organization with project specific custom templates
2 | #'
3 | #' Cabinets makes it easy to create project specific file structure templates
4 | #' that can be referenced at the start of any R session. Cabinets works by writing
5 | #' project specific file templates to the .Rprofile file of the default working directory.
6 | #' Doing so allows the templates to be accessed in new R sessions without having to
7 | #' redefine them. On first use, users will be prompted for explicit permission to
8 | #' write to .Rprofile. Permission to write can be revoked at any time by setting the
9 | #' permission option in the .Rprofile file to FALSE. Due to these explicit permission
10 | #' requirements, cabinets will only work in interactive R sessions.
11 | #'
12 | #' @docType package
13 | #' @name cabinets
14 | #' @importFrom utils capture.output
15 | #' @importFrom R6 R6Class
16 | NULL
17 |
--------------------------------------------------------------------------------
/man/cabinets.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/cabinets-package.R
3 | \docType{package}
4 | \name{cabinets}
5 | \alias{cabinets}
6 | \title{Streamlined organization with project specific custom templates}
7 | \description{
8 | Cabinets makes it easy to create project specific file structure templates
9 | that can be referenced at the start of any R session. Cabinets works by writing
10 | project specific file templates to the .Rprofile file of the default working directory.
11 | Doing so allows the templates to be accessed in new R sessions without having to
12 | redefine them. On first use, users will be prompted for explicit permission to
13 | write to .Rprofile. Permission to write can be revoked at any time by setting the
14 | permission option in the .Rprofile file to FALSE. Due to these explicit permission
15 | requirements, cabinets will only work in interactive R sessions.
16 | }
17 |
--------------------------------------------------------------------------------
/R/block.R:
--------------------------------------------------------------------------------
1 |
2 | block_delete <- function(block_start, block_end,
3 | path = file.path(normalizePath("~"), ".Rprofile")) {
4 | block_lines <- read_utf8(path)
5 | to_delete <- block_find(block_lines,
6 | block_start,
7 | block_end)
8 | to_delete <- seq(to_delete[1] - 1L, to_delete[2] + 1L)
9 | new_lines <-
10 | enc2utf8(gsub("\r?\n", platform_line_ending(), block_lines[-to_delete]))
11 |
12 | con <- file(path, open = "wb", encoding = "utf-8")
13 | writeLines(new_lines, con = con, sep = platform_line_ending())
14 | on.exit(close(con))
15 | }
16 |
17 | # based on code from the usethis package
18 | block_find <- function(lines, block_start, block_end) {
19 | if (is.null(lines)) {
20 | return(NULL)
21 | }
22 |
23 | start <- which(lines == block_start)
24 | end <- which(lines == block_end)
25 |
26 | if (length(start) == 0 && length(end) == 0) {
27 | return(NULL)
28 | }
29 |
30 | c(start + 1L, end - 1L)
31 | }
32 |
--------------------------------------------------------------------------------
/tests/testthat/test-feedback.txt:
--------------------------------------------------------------------------------
1 | > creating_project("hello", "world")
2 | Message: PASSING Creating hello using cabinet template: .world
3 |
4 | > opening_project("test")
5 | Message: * Opening R project, test, in a new session
6 |
7 | > checking_existence()
8 | Message: PASSING Checking cabinet existence
9 |
10 | > checking_git()
11 | Message: PASSING Checking for git configuration
12 |
13 | > no_cabinets()
14 | Message: PASSING No cabinets found. Cabinets can be created using `create_cabinets()`
15 |
16 | > no_r_profile()
17 | Message: PASSING '.Rprofile' doesn't exist
18 |
19 | > initiated_git("test")
20 | Message: PASSING Git repository initiated in test
21 |
22 | > created_cabinet("test")
23 | Message: PASSING Cabinet .test created
24 |
25 | Message: * Restart R for changes to take effect
26 |
27 | Message: * Cabinet can be called with .test
28 |
29 | > no_git()
30 | Message: PASSING Git not found or git not fully configured
31 |
32 | Message: * Check out
5 |
6 |
7 |
8 | [](https://CRAN.R-project.org/package=cabinets)
10 |  [](https://www.tidyverse.org/lifecycle/#stable)
12 | [](https://codecov.io/gh/nt-williams/cabinets?branch=master)
14 | [](https://github.com/nt-williams/cabinets/actions)
16 |
17 |
18 | Reproducibility can be tedious, **cabinets** makes it easier\!
19 | **cabinets** creates project specific file structure templates that can
20 | be referenced at the start of any R session. These templates can then be
21 | used to initiate future projects with the option for them to be
22 | initiatied with a git repository. **cabinets** works by writing project
23 | specific file templates to the .Rprofile file of the default working
24 | directory. Doing so allows the templates to be accessed in new R
25 | sessions without having to redefine them.
26 |
27 | **cabinets** has two main functions: `create_cabinet()` and
28 | `new_cabinet_proj()`. `create_cabinet()` constructs an R6 object of
29 | class `FileCabinet` which is then written to a .Rprofile file. At the
30 | start of fresh R sessions, the .Rprofile file loads the previously
31 | created cabinets for further use. `new_cabinet_proj()` can then be used
32 | to create future projects based on a cabinet template. Projects have the
33 | option of being created with an RStudio project and a git repository.
34 |
35 | Users will be prompted for explicit permission to write to .Rprofile on
36 | first use. Permission to write can be revoked at any time by setting the
37 | permission option in the .Rprofile file to `FALSE`. Due to these
38 | explicit permission requirements, **cabinets** will only work in
39 | interactive R sessions.
40 |
41 | ## Installation
42 |
43 | You can install the released version of **cabinets** from
44 | [CRAN](https://CRAN.R-project.org) with:
45 |
46 | ``` r
47 | install.packages("cabinets")
48 | ```
49 |
50 | And the development version from [GitHub](https://github.com/) with:
51 |
52 | ``` r
53 | # install.packages("devtools")
54 | devtools::install_github("nt-williams/cabinets@dev")
55 | ```
56 |
57 | ## Motivation
58 |
59 | Different organizations, research groups, projects, etc. have different
60 | standard directories. One organization might have the standard file
61 | structure:
62 |
63 | ├── data
64 | │ ├── derived
65 | │ └── source
66 | ├── code
67 | ├── reports
68 | ├── documents
69 | └── log
70 |
71 | While another uses a structure like this:
72 |
73 | ├── Code
74 | │ ├── ReportsCode
75 | │ └── AnalysisCode
76 | ├── Notes
77 | └── Log
78 |
79 | **cabinets** makes it easy to define these templates once and then call
80 | them for use whenever starting new projects in R.
81 |
82 | ## Example
83 |
84 | A cabinet has three components: a name, a location, and a file
85 | structure. The location defines where new directories will be created
86 | from a cabinet template. The file structure defines, well, the structure
87 | of this directory. We use a list to define the structure, where the name
88 | of each list index is a unique relative path within the new directory.
89 | Using the first project file structure described above, lets create a
90 | new cabinet.
91 |
92 | ``` r
93 | loc <- "~/Desktop"
94 |
95 | file_str <- list(
96 | 'data' = NULL,
97 | 'code' = NULL,
98 | 'data/derived' = NULL,
99 | 'data/source' = NULL,
100 | 'reports' = NULL,
101 | 'documents' = NULL,
102 | 'log' = NULL
103 | )
104 |
105 | create_cabinet(name = "contract",
106 | directory = loc,
107 | structure = file_str)
108 |
109 | #> ✓ Checking for permissions
110 | #> ✓ Checking for .Rprofile
111 | #> ✓ Checking cabinet name
112 | #> ✓ Cabinet .contract created
113 | #> ● Restart R for changes to take effect
114 | #> ● Cabinet can be called with .contract
115 | ```
116 |
117 | The cabinet is now created and doesn’t have to be redefined in future R
118 | sessions. To examine the cabinet we just call it.
119 |
120 | ``` r
121 | .contract
122 |
123 | #> Cabinet name: contract
124 | #> Cabinet path: ~/Desktop
125 | #> Cabinet structure:
126 | #> ├── data
127 | #> │ ├── derived
128 | #> │ └── source
129 | #> ├── code
130 | #> ├── reports
131 | #> ├── documents
132 | #> └── log
133 | ```
134 |
135 | `new_cabinet_proj()` is then used to create a new directory from a
136 | cabinet template.
137 |
138 | ``` r
139 | new_cabinet_proj(cabinet = .contract,
140 | project_name = "project",
141 | git = TRUE,
142 | git_ignore = "data",
143 | renv = TRUE)
144 | ```
145 |
146 | ## Similar implementations
147 |
148 | Similar implementations exist elsewhere. **cabinets** is unique for
149 | giving the user the ability to design their own project templates. These
150 | are all great packages, use what’s best for you\!
151 |
152 | - [workflowr](https://github.com/jdblischak/workflowr)
153 | - [projects](https://github.com/NikKrieger/projects)
154 | - [starters](https://github.com/lockedata/starters)
155 | - [ProjectTemplate](https://github.com/KentonWhite/ProjectTemplate)
156 |
157 | ## Contributing
158 |
159 | Please note that the **cabinets** project is released with a
160 | [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By contributing to
161 | this project, you agree to abide by its terms.
162 |
--------------------------------------------------------------------------------
/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 | # cabinets
17 |
18 |
19 | [](https://CRAN.R-project.org/package=cabinets)
20 | 
21 | [](https://www.tidyverse.org/lifecycle/#stable)
22 | [](https://codecov.io/gh/nt-williams/cabinets?branch=master)
23 | [](https://github.com/nt-williams/cabinets/actions)
24 |
25 |
26 | Reproducibility can be tedious, **cabinets** makes it easier! **cabinets** creates project specific file structure templates that can be referenced at the start of any R session. These templates can then be used to initiate future projects with the option for them to be initiatied with a git repository. **cabinets** works by writing project specific file templates to the .Rprofile file of the default working directory. Doing so allows the templates to be accessed in new R sessions without having to redefine them.
27 |
28 | **cabinets** has two main functions: `create_cabinet()` and `new_cabinet_proj()`. `create_cabinet()` constructs an R6 object of class `FileCabinet` which is then written to a .Rprofile file. At the start of fresh R sessions, the .Rprofile file loads the previously created cabinets for further use. `new_cabinet_proj()` can then be used to create future projects based on a cabinet template. Projects have the option of being created with an RStudio project and a git repository.
29 |
30 | Users will be prompted for explicit permission to write to .Rprofile on first use. Permission to write can be revoked at any time by setting the permission option in the .Rprofile file to `FALSE`. Due to these explicit permission requirements, **cabinets** will only work in interactive R sessions.
31 |
32 | ## Installation
33 |
34 | You can install the released version of **cabinets** from [CRAN](https://CRAN.R-project.org) with:
35 |
36 | ``` r
37 | install.packages("cabinets")
38 | ```
39 |
40 | And the development version from [GitHub](https://github.com/) with:
41 |
42 | ``` r
43 | # install.packages("devtools")
44 | devtools::install_github("nt-williams/cabinets@dev")
45 | ```
46 |
47 | ## Motivation
48 |
49 | Different organizations, research groups, projects, etc. have different standard directories. One organization might have the standard file structure:
50 |
51 | ```
52 | ├── data
53 | │ ├── derived
54 | │ └── source
55 | ├── code
56 | ├── reports
57 | ├── documents
58 | └── log
59 | ```
60 |
61 | While another uses a structure like this:
62 |
63 | ```
64 | ├── Code
65 | │ ├── ReportsCode
66 | │ └── AnalysisCode
67 | ├── Notes
68 | └── Log
69 | ```
70 |
71 | **cabinets** makes it easy to define these templates once and then call them for use whenever starting new projects in R.
72 |
73 | ## Example
74 |
75 | A cabinet has three components: a name, a location, and a file structure. The location defines where new directories will be created from a cabinet template. The file structure defines, well, the structure of this directory. We use a list to define the structure, where the name of each list index is a unique relative path within the new directory. Using the first project file structure described above, lets create a new cabinet.
76 |
77 | ``` r
78 | loc <- "~/Desktop"
79 |
80 | file_str <- list(
81 | 'data' = NULL,
82 | 'code' = NULL,
83 | 'data/derived' = NULL,
84 | 'data/source' = NULL,
85 | 'reports' = NULL,
86 | 'documents' = NULL,
87 | 'log' = NULL
88 | )
89 |
90 | create_cabinet(name = "contract",
91 | directory = loc,
92 | structure = file_str)
93 |
94 | #> ✓ Checking for permissions
95 | #> ✓ Checking for .Rprofile
96 | #> ✓ Checking cabinet name
97 | #> ✓ Cabinet .contract created
98 | #> ● Restart R for changes to take effect
99 | #> ● Cabinet can be called with .contract
100 | ```
101 |
102 | The cabinet is now created and doesn't have to be redefined in future R sessions. To examine the cabinet we just call it.
103 |
104 | ``` r
105 | .contract
106 |
107 | #> Cabinet name: contract
108 | #> Cabinet path: ~/Desktop
109 | #> Cabinet structure:
110 | #> ├── data
111 | #> │ ├── derived
112 | #> │ └── source
113 | #> ├── code
114 | #> ├── reports
115 | #> ├── documents
116 | #> └── log
117 | ```
118 |
119 | `new_cabinet_proj()` is then used to create a new directory from a cabinet template.
120 |
121 | ``` r
122 | new_cabinet_proj(cabinet = .contract,
123 | project_name = "project",
124 | git = TRUE,
125 | git_ignore = "data",
126 | renv = TRUE)
127 | ```
128 |
129 | ## Similar implementations
130 |
131 | Similar implementations exist elsewhere. **cabinets** is unique for giving the user the ability to design their own project templates. These are all great packages, use what's best for you!
132 |
133 | * [workflowr](https://github.com/jdblischak/workflowr)
134 | * [projects](https://github.com/NikKrieger/projects)
135 | * [starters](https://github.com/lockedata/starters)
136 | * [ProjectTemplate](https://github.com/KentonWhite/ProjectTemplate)
137 |
138 | ## Contributing
139 |
140 | Please note that the **cabinets** project is released with a
141 | [Contributor Code of Conduct](CODE_OF_CONDUCT.md).
142 | By contributing to this project, you agree to abide by its terms.
143 |
144 |
--------------------------------------------------------------------------------
/R/cabinets.R:
--------------------------------------------------------------------------------
1 | #' R6 class for a cabinet
2 | #'
3 | #' Constructs an R6 class of FileCabinet. Objects of class
4 | #' FileCabinet contain information that is used by \code{new_cabinet_proj()}
5 | #' to create project directories.
6 | #'
7 | #' @export
8 | FileCabinet <- R6Class('FileCabinet',
9 | public = list(
10 | #' @field name cabinet name.
11 | name = NULL,
12 |
13 | #' @field directory the path to where future directories will be created, a string.
14 | directory = NULL,
15 |
16 | #' @field structure the directory structure, a list.
17 | structure = NULL,
18 |
19 | #' @details
20 | #' Create a new `FileCabinet` object.
21 | #'
22 | #' @param name cabinet name.
23 | #' @param directory the path to where future directories will be created, a string.
24 | #' @param structure the directory structure, a list.
25 | #' @return A cabinet object.
26 | #'
27 | #' @examples
28 | #' FileCabinet$new("test", "a/path",
29 | #' list(code = NULL, 'data/derived' = NULL, 'data/source' = NULL))
30 | initialize = function(name, directory, structure) {
31 | stopifnot(is.character(name), length(name) == 1)
32 | stopifnot(is.character(directory))
33 | stopifnot(is.list(structure))
34 |
35 | self$name <- name
36 | self$directory <- fs::path_tidy(directory)
37 | self$structure <- structure
38 | },
39 |
40 | #' @details
41 | #' Print an object of class FileCabinet.
42 |
43 | print = function() {
44 | cat('Cabinet name: ',
45 | cat_green(self$name),
46 | '\n',
47 | sep = '')
48 | cat('Cabinet path: ',
49 | cat_path(self$directory),
50 | '\n',
51 | sep = '')
52 | cat('Cabinet structure: \n')
53 | print_structure(self$structure)
54 | }
55 | )
56 | )
57 |
58 | #' Create a cabinet template
59 | #'
60 | #' \code{create_cabinet} writes code to the .Rprofile file so
61 | #' that when new R sessions are started, the newly created
62 | #' cabinet, an R6 object of class FileCabinet, is available
63 | #' in the global environment as a hidden object. The cabinet
64 | #' simply stores file location and file template information
65 | #' that \code{new_cabinet_proj} uses to create new projects
66 | #' with the pre-defined structure.
67 | #'
68 | #' @param name Name of the cabinet; character of length 1.
69 | #' This is how the cabinet will be referenced, so best to
70 | #' chose something memorable.
71 | #' @param directory The file path for where the cabinet will exist.
72 | #' @param structure A list of paths of folders/files to
73 | #' create. See details for further explanation.
74 | #' @param .alias An optional name for the object the cabinet
75 | #' will be stored in R as. Defaults to \code{name}.
76 | #'
77 | #' @return An R6 object of class FileCabinet. The code to
78 | #' generate this object is written to the .Rprofile file
79 | #' of the home directory.
80 | #' @details Before writing to or creating a .Rprofile file,
81 | #' cabinets will explicitly ask for the user's permission to on exit.
82 | #' The cabinet structure should be defined using a list with the
83 | #' names defining folder paths. List values should be set to NULL.
84 | #' @seealso \code{\link{new_cabinet_proj}}
85 | #' @export
86 | create_cabinet <- function(name,
87 | directory,
88 | structure,
89 | .alias = name) {
90 | check_interactive()
91 | check_permissions()
92 | check_r_profile()
93 | check_name(name)
94 | write_cabinet(name, directory, structure, .alias)
95 | created_cabinet(.alias)
96 | }
97 |
98 | write_cabinet <- function(name, directory, structure, .alias) {
99 | directory <- fs::path_tidy(paste(directory, collapse = .Platform$file.sep))
100 |
101 | newFileCabinet <-
102 | call("$", x = call("::",
103 | pkg = substitute(cabinets),
104 | name = substitute(FileCabinet)),
105 | name = substitute(new))
106 |
107 | value <-
108 | as.call(list(newFileCabinet,
109 | name = name,
110 | directory = directory,
111 | structure = structure))
112 |
113 | cabinet <- call("<-", x = as.symbol(paste0(".", .alias)), value = value)
114 | con <- file(file.path(normalizePath("~"), ".Rprofile"), open = "a")
115 | writeLines(glue::glue("## {name} cabinet start"), con = con)
116 | capture.output(cabinet, file = con, append = TRUE)
117 | writeLines(glue::glue("## {name} cabinet end"), con = con)
118 | on.exit(close(con))
119 | }
120 |
121 | #' Create a new project using a cabinet template
122 | #'
123 | #' Generate new project directories using cabinet templates.
124 | #'
125 | #' @param cabinet The name of the cabinet template. Available cabinets can
126 | #' be found using \code{get_cabinets()}.
127 | #' @param project_name The name of the project to store in the cabinet,
128 | #' a character string. Can be a file path pointing to a directory
129 | #' within the specified cabinet.
130 | #' @param r_project Logical, should an Rproject be created. Default is
131 | #' TRUE if working in RStudio (only works in RStudio).
132 | #' @param open Logical, if creating an Rproject, should that project
133 | #' be opened once created. Default is TRUE if working in
134 | #' RStudio (only works in RStudio).
135 | #' @param renv Logical, should a \code{renv} project be initiated.
136 | #' If \code{TRUE}, \code{renv} project infrastructure will be created using
137 | #' \code{\link[renv]{scaffold}}.
138 | #' @param git Logical, should a git repository be initiated.
139 | #' @param git_root A path relative to the project to initiate the
140 | #' git repository. Default is NULL and the repository is
141 | #' initiated at the root of the project.
142 | #' @param git_ignore Character vector of files and directories
143 | #' to add to .gitignore file.
144 | #'
145 | #' @return Creates a new directory at the path specified in the
146 | #' cabinet template. If \code{r_project = TRUE}, a .Rproj file
147 | #' will also be created using the project name. If open is set
148 | #' to TRUE, the new R project will opened in a new R session.
149 | #' @seealso \code{\link{create_cabinet}}
150 | #' @export
151 | new_cabinet_proj <- function(cabinet,
152 | project_name,
153 | r_project = TRUE,
154 | open = TRUE,
155 | renv = TRUE,
156 | git = TRUE,
157 | git_root = NULL,
158 | git_ignore = NULL) {
159 |
160 | check_cabinet(deparse(substitute(cabinet)))
161 |
162 | if (in_rstudio() == FALSE) {
163 | r_project <- FALSE
164 | }
165 |
166 | proj_path <- file.path(cabinet$directory, project_name)
167 | proj_folders <- file.path(proj_path, names(cabinet$structure))
168 |
169 | check_project(proj_path)
170 | creating_project(project_name, cabinet$name)
171 |
172 | if (r_project) {
173 | rstudioapi::initializeProject(proj_path)
174 | } else {
175 | dir.create(proj_path, recursive = TRUE)
176 | open <- FALSE
177 | }
178 |
179 | create_subdirectories(proj_folders)
180 |
181 | if (renv) {
182 | capture.output(renv::scaffold(project = proj_path))
183 | initiating_renv()
184 | }
185 |
186 | if (git) {
187 | if (is.null(git_root)) {
188 | git_root <- proj_path
189 | } else {
190 | git_root <- file.path(proj_path, git_root)
191 | }
192 | use_git(git_root, git_ignore)
193 | }
194 |
195 | if (open) {
196 | opening_project(project_name)
197 | Sys.sleep(2)
198 | rstudioapi::openProject(proj_path, TRUE)
199 | }
200 | }
201 |
202 | #' Open .Rprofile for editing
203 | #'
204 | #' \code{edit_r_profile} opens the .Rprofile file for editing.
205 | #' If the .Rprofile file doesn't exist an error message will be returned.
206 | #' This is essentially a wrapper function for \code{file.edit}.
207 | #'
208 | #' @return A message that .Rprofile is being opened or that it doesn't exist.
209 | #' @export
210 | edit_r_profile <- function() {
211 | rprof_path <- file.path(normalizePath("~"), ".Rprofile")
212 | status <- tryCatch(if (file.exists(rprof_path)) {
213 | cli::cli_text("Opening .Rprofile")
214 | go(rprof_path)
215 | } else {
216 | stop()
217 | }, error = function(e) {
218 | no_r_profile()
219 | })
220 | invisible(status)
221 | }
222 |
--------------------------------------------------------------------------------