diff --git a/.github/workflows/check_on_branch.yml b/.github/workflows/check_on_branch.yml
index d2d3ca48..35c6fca6 100644
--- a/.github/workflows/check_on_branch.yml
+++ b/.github/workflows/check_on_branch.yml
@@ -17,4 +17,4 @@ jobs:
permissions:
contents: read
steps:
- - uses: inbo/actions/check_pkg@checklist-0.3.2
+ - uses: inbo/actions/check_pkg@main
diff --git a/.github/workflows/check_on_main.yml b/.github/workflows/check_on_main.yml
index ffc8ff1d..ac9d760d 100644
--- a/.github/workflows/check_on_main.yml
+++ b/.github/workflows/check_on_main.yml
@@ -19,4 +19,4 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ZENODO_SANDBOX: ${{ secrets.ZENODO_SANDBOX }}
steps:
- - uses: inbo/actions/check_pkg@checklist-0.3.2
+ - uses: inbo/actions/check_pkg@main
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 8445d7a5..3b2f57d3 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -19,10 +19,13 @@ jobs:
- uses: actions/checkout@v3
- name: Get tag message
run: |
- TAG_BODY=$(git tag --contains ${{ github.sha }} -n100 | awk '(NR>1)')
- echo "::set-output name=TAG_BODY::$TAG_BODY"
+ TAG=$(git tag --contains $(git rev-parse HEAD))
+ TAG_BODY=$(git tag --contains {{ github.sha }} -n100 | awk '(NR>1)')
+ echo "TAG=$TAG" >> $GITHUB_OUTPUT
+ echo "TAG_BODY=$TAG_BODY" >> $GITHUB_OUTPUT
id: tag-body
- uses: ncipollo/release-action@v1
with:
- name: Release ${{ github.ref }}
+ name: Release ${{ steps.tag-body.outputs.TAG }}
+ tag: ${{ steps.tag-body.outputs.TAG }}
body: ${{ steps.tag-body.outputs.TAG_BODY }}
diff --git a/.zenodo.json b/.zenodo.json
index c57c48ca..02d8a46b 100644
--- a/.zenodo.json
+++ b/.zenodo.json
@@ -1,6 +1,6 @@
{
"title": "checklist: A Thorough and Strict Set of Checks for R Packages and Source Code",
- "version": "0.3.2",
+ "version": "0.3.3",
"license": "GPL-3.0",
"upload_type": "software",
"description": "
An opinionated set of rules for R packages and R source code\nprojects.<\/p>",
diff --git a/CITATION.cff b/CITATION.cff
index 390340d5..60d0d5cb 100644
--- a/CITATION.cff
+++ b/CITATION.cff
@@ -25,4 +25,4 @@ identifiers:
value: 10.5281/zenodo.4028303
- type: url
value: https://inbo.github.io/checklist/
-version: 0.3.2
+version: 0.3.3
diff --git a/DESCRIPTION b/DESCRIPTION
index 97d9ae54..3777df73 100644
--- a/DESCRIPTION
+++ b/DESCRIPTION
@@ -1,7 +1,7 @@
Type: Package
Package: checklist
Title: A Thorough and Strict Set of Checks for R Packages and Source Code
-Version: 0.3.2
+Version: 0.3.3
Authors@R: c(
person("Thierry", "Onkelinx", , "thierry.onkelinx@inbo.be", role = c("aut", "cre"),
comment = c(ORCID = "0000-0001-8804-4216", affiliation = "Research Institute for Nature and Forest (INBO)")),
diff --git a/NAMESPACE b/NAMESPACE
index c06b6e98..80a51fbd 100644
--- a/NAMESPACE
+++ b/NAMESPACE
@@ -31,6 +31,7 @@ export(is_workdir_clean)
export(menu_first)
export(new_branch)
export(orcid2person)
+export(organisation)
export(prepare_ghpages)
export(read_checklist)
export(set_tag)
@@ -43,6 +44,7 @@ export(tidy_desc)
export(update_citation)
export(use_author)
export(validate_email)
+export(validate_orcid)
export(write_checklist)
export(write_citation_cff)
export(write_zenodo_json)
diff --git a/NEWS.md b/NEWS.md
index 05f88ca7..4a184bb0 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -1,3 +1,19 @@
+# checklist 0.3.3
+
+* New `organisation()` class to hold the requirements of the organisation.
+ For the time being this is hard-coded to the Research Institute for Nature
+ and Forest (INBO).
+* Author affiliations must match one of the affiliations set in
+ `organisation()`.
+ The membership of an author is determined by their e-mail or their
+ affiliation.
+ This is checked when creating or using author information and when updating
+ citation information.
+* `read_checklist()` looks for `checklist.yml` in parent folders when it can't
+ find it in the provided path.
+* `validate_orcid()` checks the format and the checksum of the ORCID.
+* Add `vignette("folder", package = "checklist")`.
+
# checklist 0.3.2
* `citation_meta()` gains support for [`bookdown`](https://pkgs.rstudio.com/bookdown/) reports.
diff --git a/R/check_description.R b/R/check_description.R
index 471d094d..564674ab 100644
--- a/R/check_description.R
+++ b/R/check_description.R
@@ -287,21 +287,31 @@ Please send a pull request if you need support for this license.",
#' @importFrom utils person
check_authors <- function(this_desc) {
authors <- this_desc$get_authors()
- inbo <- person(
- given = "Research Institute for Nature and Forest (INBO)",
- role = c("cph", "fnd"), email = "info@inbo.be"
+ org <- organisation$new()
+ stopifnot(
+ "TO DO: handle funder not equal to rightsholder" =
+ org$get_rightsholder == org$get_funder
)
- problems <- paste(
- "`Research Institute for Nature and Forest (INBO)` must be listed as",
- "copyright holder and funder and use info@inbo.be as email."
- )[!inbo %in% authors]
- authors <- lapply(authors, unlist, recursive = FALSE)
- authors <- authors[!authors %in% inbo]
- orcid <- sapply(authors, `[[`, "comment")
- c(
- problems,
- "Every author and contributor must have an ORCID"[
- any(names(orcid) != "ORCID")
- ]
+ rightsholder <- person(
+ given = org$get_rightsholder, role = c("cph", "fnd"), email = org$get_email
)
+ problems <- sprintf(
+ "`%s` must be listed as copyright holder and funder and use `%s` as email.",
+ org$get_rightsholder, org$get_email
+ )[!rightsholder %in% authors]
+ authors <- authors[!authors %in% rightsholder]
+ vapply(
+ authors, FUN.VALUE = vector(mode = "list", length = 1L),
+ FUN = function(author) {
+ email <- format(author, include = "email", braces = list(email = ""))
+ this_org <- org$get_organisation[[gsub(".*@", "", email)]]
+ format(author, include = c("given", "family")) |>
+ sprintf(fmt = "ORCID required for `%s`") -> problem
+ list(
+ problem[isTRUE(this_org$orcid) && !has_name(author$comment, "ORCID")]
+ )
+ }
+ ) |>
+ unlist() |>
+ c(problems)
}
diff --git a/R/citation_bookdown.R b/R/citation_bookdown.R
index a70c2f95..3e2bbedf 100644
--- a/R/citation_bookdown.R
+++ b/R/citation_bookdown.R
@@ -121,8 +121,8 @@ yaml_author <- function(yaml) {
data.frame(contributor = nrow(author) + 1, role = "funder") |>
rbind(roles) -> roles
data.frame(
- id = nrow(author) + 1, given = "", family = yaml$funder, orcid = "",
- affiliation = ""
+ id = nrow(author) + 1, given = yaml$funder, family = "", orcid = "",
+ affiliation = "", organisation = known_affiliation(yaml$funder)
) |>
rbind(author) -> author
}
@@ -130,8 +130,9 @@ yaml_author <- function(yaml) {
data.frame(contributor = nrow(author) + 1, role = "copyright holder") |>
rbind(roles) -> roles
data.frame(
- id = nrow(author) + 1, given = "", family = yaml$rightsholder,
- orcid = "", affiliation = ""
+ id = nrow(author) + 1, given = yaml$rightsholder, family = "",
+ orcid = "", affiliation = "",
+ organisation = known_affiliation(yaml$rightsholder)
) |>
rbind(author) -> author
}
@@ -144,7 +145,8 @@ yaml_author <- function(yaml) {
yaml_author_format <- function(person) {
person_df <- data.frame(
given = character(0), family = character(0), orcid = character(0),
- affiliation = character(0), contact = logical(0)
+ affiliation = character(0), contact = logical(0),
+ organisation = character(0)
)
if (!is.list(person)) {
attr(person_df, "errors") <- list("person must be a list")
@@ -170,7 +172,8 @@ yaml_author_format <- function(person) {
affiliation = paste0(person$affiliation, ""),
contact = ifelse(
is.null(person$corresponding), FALSE, person$corresponding
- )
+ ),
+ organisation = known_affiliation(paste0(person$affiliation, ""))
)
c(
"person `name` element is missing a `given` element"[
diff --git a/R/citation_description.R b/R/citation_description.R
index 5a40f86a..6836b83d 100644
--- a/R/citation_description.R
+++ b/R/citation_description.R
@@ -25,7 +25,7 @@ citation_description <- function(meta) {
upload_type = "software", description = abstract
) |>
c(
- authors$meta, keywords$meta, communities$meta, urls$meta,
+ authors, keywords$meta, communities$meta, urls$meta,
access_right = "open"
) -> cit_meta
lang <- descript$get_field("Language", default = "")
@@ -43,7 +43,7 @@ citation_description <- function(meta) {
list(
meta = cit_meta,
errors = c(urls$errors, keywords$errors), warnings = communities$warnings,
- notes = c(authors$notes, communities$notes)
+ notes = authors$notes
)
}
@@ -58,31 +58,10 @@ description_author <- function(authors) {
family = format(authors, include = "family")
) |>
merge(
- unique(roles[, c("contributor", "orcid", "affiliation")]), by.x = "id",
- by.y = "contributor"
+ unique(roles[, c("contributor", "orcid", "affiliation", "organisation")]),
+ by.x = "id", by.y = "contributor"
) -> contributors
- contributors[
- contributors$given == "Research Institute for Nature and Forest (INBO)",
- ] |>
- merge(
- roles[, c("contributor", "role")], by.x = "id", by.y = "contributor"
- ) -> inbo_roles
- notes <- c(
- paste(
- "`Research Institute for Nature and Forest (INBO)` not listed as",
- "copyright holder in `DESCRIPTION`."
- )[!"copyright holder" %in% inbo_roles$role],
- paste(
- "`Research Institute for Nature and Forest (INBO)` not listed as funder",
- "in `DESCRIPTION`."
- )[!"funder" %in% inbo_roles$role]
- )
- list(
- meta = list(
- authors = contributors, roles = roles[, c("contributor", "role")]
- ),
- notes = notes
- )
+ list(authors = contributors, roles = roles[, c("contributor", "role")])
}
description_author_format <- function(i, x) {
@@ -93,6 +72,9 @@ description_author_format <- function(i, x) {
cph = "copyright holder", fnd = "funder", rev = "reviewer"
)[x[[i]]$role]
)
+ formatted$organisation <- ifelse(
+ is.null(x[[i]]), "", gsub(".*@", "", x[[i]]$email)
+ )
if (is.null(x[[i]]$comment)) {
formatted$orcid <- ""
formatted$affiliation <- ""
@@ -104,9 +86,34 @@ description_author_format <- function(i, x) {
formatted$affiliation <- ifelse(
is.na(x[[i]]$comment["affiliation"]), "", x[[i]]$comment["affiliation"]
)
+ if (formatted$organisation[1] == "" && formatted$affiliation[1] != "") {
+ formatted$organisation <- known_affiliation(formatted$affiliation[1])
+ }
return(list(formatted))
}
+#' @importFrom assertthat assert_that
+known_affiliation <- function(target) {
+ target <- gsub("([.|()\\^{}+$*?]|\\[|\\])", "\\\\\\1", target)
+ org <- organisation$new()$get_organisation
+ vapply(
+ names(org), FUN.VALUE = logical(1), target = target, org = org,
+ FUN = function(x, org, target) {
+ grepl(target, org[[x]]$affiliation) |>
+ any()
+ }
+ ) -> org
+ assert_that(
+ sum(org) < 2,
+ msg = paste(
+ "multiple matching organisations:",
+ paste(names(org)[org], collapse = "; ")
+ )
+ )
+ c(names(org)[org], "") |>
+ head(1)
+}
+
description_url <- function(urls) {
urls <- urls[!grepl("https://github.com/", urls)]
doi_regexp <- "https://doi.org/(.*)"
@@ -146,20 +153,19 @@ description_keywords <- function(keywords) {
description_communities <- function(communities) {
if (length(communities) == 0) {
+ org <- organisation$new()
return(
list(
- meta = list(), notes = character(0),
+ meta = list(),
warnings = paste(
"no communities found in `DESCRIPTION`.",
- "Please add them with `Config/checklist/communities: inbo; second`"
+ "Please add them with `Config/checklist/communities:",
+ org$get_community
)
)
)
}
- communities <- strsplit(communities, "; ")[[1]]
- notes <-
- "inbo not listed as community in `DESCRIPTION`"[!"inbo" %in% communities]
list(
- meta = list(community = communities), warnings = character(0), notes = notes
+ meta = list(community = communities), warnings = character(0)
)
}
diff --git a/R/citation_meta_class.R b/R/citation_meta_class.R
index b049bd35..683a9d96 100644
--- a/R/citation_meta_class.R
+++ b/R/citation_meta_class.R
@@ -17,7 +17,6 @@ citation_meta <- R6Class(
assert_that(is.string(path), noNA(path))
path <- path_real(path)
assert_that(is_dir(path), msg = "path is not an existing directory")
-
private$path <- path
if (is_file(path(path, "_bookdown.yml"))) {
private$type <- "bookdown"
@@ -40,10 +39,15 @@ citation_meta <- R6Class(
private$errors <- meta$errors
private$notes <- meta$notes
private$warnings <- meta$warnings
+ if (length(private$errors) == 0) {
+ validated <- validate_citation(self)
+ private$errors <- c(private$errors, validated$errors)
+ private$notes <- c(private$notes, validated$notes)
+ }
if (length(private$errors) > 0) {
warning(
"Errors found parsing citation meta data. ",
- "Citation files not updated."
+ "Citation files not updated.", call. = FALSE, noBreaks. = TRUE
)
return(invisible(self))
}
@@ -160,6 +164,64 @@ citation_print <- function(errors, meta, notes, path, warnings) {
}
}
+#' @importFrom assertthat assert_that
+validate_citation <- function(meta) {
+ assert_that(inherits(meta, "citation_meta"))
+ org <- organisation$new()
+ roles <- meta$get_meta$roles
+ authors <- meta$get_meta$authors
+ rightsholder_id <- roles$contributor[roles$role == "copyright holder"]
+ funder_id <- roles$contributor[roles$role == "funder"]
+ notes <- c(
+ sprintf("rightsholder differs from `%s`", org$get_rightsholder)[
+ authors$given[authors$id == rightsholder_id] != org$get_rightsholder
+ ],
+ sprintf("funder differs from `%s`", org$get_funder)[
+ authors$given[authors$id == funder_id] != org$get_funder
+ ]
+ )
+ errors <- c(
+ sprintf("invalid ORCID for %s %s", authors$given, authors$family)[
+ !validate_orcid(authors$orcid)
+ ],
+ sprintf("missing required Zenodo community `%s`", org$get_community)[
+ !org$get_community %in% meta$get_meta$community
+ ]
+ )
+ authors <- authors[authors$given != org$get_rightsholder, ]
+ authors <- authors[authors$given != org$get_funder, ]
+ authors <- authors[authors$organisation %in% names(org$get_organisation), ]
+ vapply(
+ seq_along(authors$organisation),
+ FUN.VALUE = vector(mode = "list", length = 1), org = org$get_organisation,
+ FUN = function(i, org) {
+ paste(
+ "Non standard affiliation for %s %s as member of `%s`. ",
+ "Please use any of the following", collapse = ""
+ ) |>
+ sprintf(
+ authors$given[i], authors$family[i], authors$organisation[i]
+ ) -> error
+ error <- error[
+ !authors$affiliation[i] %in% org[[authors$organisation[i]]]$affiliation
+ ]
+ if (org[[authors$organisation[i]]]$orcid) {
+ error <- c(
+ error,
+ sprintf(
+ "No ORCID for %s %s. This is required for `%s`", authors$given[i],
+ authors$family[i], authors$organisation[i]
+ )[is.na(authors$orcid[i]) || authors$orcid[i] == ""]
+ )
+ }
+ return(list(error))
+ }
+ ) |>
+ unlist() |>
+ c(errors) -> errors
+ list(notes = notes, errors = errors)
+}
+
#' @importFrom assertthat assert_that has_name
#' @importFrom fs path
#' @importFrom jsonlite toJSON
diff --git a/R/citation_readme.R b/R/citation_readme.R
index 13eb2a37..79d611c4 100644
--- a/R/citation_readme.R
+++ b/R/citation_readme.R
@@ -183,12 +183,12 @@ readme_author <- function(text) {
head(1) -> empty_line
text$text[seq_len(empty_line - 1)] |>
gsub(pattern = ";\\s*$", replacement = "") -> authors
- authors_aff <- authors
- authors_aff[!grepl("\\[\\^.*\\]", authors_aff)] <- ""
- gsub(".*?\\[\\^(.*?)\\]", "\\1;", authors_aff) |>
+ orgs <- authors
+ orgs[!grepl("\\[\\^.*\\]", orgs)] <- ""
+ gsub(".*?\\[\\^(.*?)\\]", "\\1;", orgs) |>
gsub(pattern = "(aut|cph|cre|ctb|fnd|rev);", replacement = "") |>
gsub(pattern = ";$", replacement = "") |>
- strsplit(split = ";") -> authors_aff
+ strsplit(split = ";") -> orgs
data.frame(
contributor = grep("\\[\\^aut\\]", authors),
role = rep("author", , sum(grepl("\\[\\^aut\\]", authors)))
@@ -233,7 +233,7 @@ readme_author <- function(text) {
affiliations <- text$text[grepl("\\[\\^.*?\\]:", text$text)]
aff_code <- gsub(".*\\[\\^(.*?)\\]:.*", "\\1", affiliations)
aff_code_check <- vapply(
- authors_aff, FUN.VALUE = logical(1), aff_code = aff_code,
+ orgs, FUN.VALUE = logical(1), aff_code = aff_code,
FUN = function(z, aff_code) {
all(z %in% aff_code)
}
@@ -241,7 +241,7 @@ readme_author <- function(text) {
gsub("\\[\\^(.*?)\\]:\\s*(.*)", "\\2", affiliations) |>
setNames(aff_code) -> affiliations
authors_aff <- vapply(
- authors_aff, FUN.VALUE = character(1), z = affiliations,
+ orgs, FUN.VALUE = character(1), z = affiliations,
FUN = function(y, z) {
paste(z[y], collapse = "; ")
}
@@ -275,11 +275,13 @@ readme_author <- function(text) {
]
)
+ orgs[vapply(orgs, length, integer(1)) == 0] <- NA
text$text <- text$text[!grepl("\\[\\^.*?\\]:", text$text)]
text$meta$authors <- data.frame(
id = seq_along(authors), given = gsub(".*,\\s*(.*)", "\\1", authors),
family = ifelse(grepl(",", authors), gsub("(.*),.*", "\\1", authors), ""),
- affiliation = authors_aff, orcid = authors_orcid
+ affiliation = authors_aff, orcid = authors_orcid,
+ organisation = unlist(orgs)
)
return(text)
}
@@ -308,18 +310,14 @@ readme_community <- function(text) {
community_line <- grep(community_regexp, text$text)
text$warnings <- c(
text$warnings,
- "No community information found in README.md"[length(community_line) == 0]
+ "No Zenodo community information found in README.md"[
+ length(community_line) == 0
+ ]
)
if (length(community_line) > 0) {
text$meta$community <- gsub(
community_regexp, "\\1", text$text[community_line]
)
- text$notes <- c(
- text$notes,
- "`inbo` not listed as a community"[
- !"inbo" %in% text$meta$community
- ]
- )
text$text <- text$text[-community_line]
}
return(text)
diff --git a/R/create_package.R b/R/create_package.R
index 3e5fb1e9..ba96329a 100644
--- a/R/create_package.R
+++ b/R/create_package.R
@@ -25,6 +25,7 @@
#' @param communities An optional vector of Zenodo community id's.
#' @export
#' @importFrom assertthat assert_that is.string
+#' @importFrom desc description
#' @importFrom fs dir_create dir_ls file_copy is_dir path
#' @importFrom gert git_add git_init
#' @importFrom tools toTitleCase
@@ -64,8 +65,10 @@ create_package <- function(
maintainer <- c(maintainer, author2person())
}
}
-
assert_that(inherits(maintainer, "person"))
+ org <- organisation$new()
+ maintainer <- c(maintainer, org$as_person)
+
assert_that(is_dir(path), msg = sprintf("`%s` is not a directory", path))
assert_that(is.string(package))
assert_that(valid_package_name(package))
@@ -90,44 +93,32 @@ create_package <- function(
write_checklist(x)
git_add("checklist.yml", repo = repo)
+
+ # create DESCRIPTION
+ desc <- desc::description$new("!new")
+ desc$set("Package", package)
+ desc$set("Title", toTitleCase(title))
+ desc$set_version("0.0.0")
+ desc$set_authors(maintainer)
+ desc$set("Description", description)
+ desc$set("License", ifelse(license == "MIT", "MIT + file LICENSE", license))
+ desc$set_urls(sprintf("https://github.com/%s/%s", org$get_github, package))
+ desc$set(
+ "BugReports",
+ sprintf("https://github.com/%s/%s/issues", org$get_github, package)
+ )
if (length(communities)) {
- communities <- sprintf(
- "Config/checklist/communities: %s\n",
- paste(communities, collapse = "; ")
+ desc$set(
+ "Config/checklist/communities", paste(communities, collapse = "; ")
)
- } else {
- communities <- ""
}
-
- # create DESCRIPTION
- sprintf(
-"Type: Package
-Package: %1$s
-Title: %2$s
-Version: 0.0.0
-Authors@R:
- c(%3$s,
- person(given = \"Research Institute for Nature and Forest (INBO)\",
- role = c(\"cph\", \"fnd\"),
- email = \"info@inbo.be\"))
-Description: %4$s
-License: %7$s
-URL: https://github.com/inbo/%1$s
-BugReports: https://github.com/inbo/%1$s/issues
-%9$sConfig/checklist/keywords: %8$s
-Encoding: UTF-8
-Language: %6$s
-Roxygen: list(markdown = TRUE)
-RoxygenNote: %5$s
-",
- package, toTitleCase(title),
- paste(format(maintainer, style = "R"), collapse = "\n"),
- description, installed.packages()["roxygen2", "Version"], language,
- ifelse(license == "MIT", "MIT + file LICENSE", license),
- paste(keywords, collapse = "; "), communities
- ) |>
- writeLines(path(path, "DESCRIPTION"))
- tidy_desc(path)
+ desc$set("Config/checklist/keywords", paste(keywords, collapse = "; "))
+ desc$set("Encoding", "UTF-8")
+ desc$set("Language", language)
+ desc$set("Roxygen", "list(markdown = TRUE)")
+ desc$set("RoxygenNote", installed.packages()["roxygen2", "Version"])
+ desc$del("Maintainer")
+ desc$write(path(path, "DESCRIPTION"))
git_add("DESCRIPTION", repo = repo)
# create NAMESPACE
@@ -203,14 +194,12 @@ RoxygenNote: %5$s
file_copy(license_file)
if (license == "MIT") {
paste0("YEAR: ", format(Sys.Date(), "%Y")) |>
- c("COPYRIGHT HOLDER: Research Institute for Nature and Forest (INBO)") |>
+ c(sprintf("COPYRIGHT HOLDER: %s", org$get_rightsholder)) |>
writeLines(path(path, "LICENSE"))
git_add("LICENSE", repo = repo)
mit <- readLines(license_file)
mit[3] <- gsub("", format(Sys.Date(), "%Y"), mit[3])
- mit[3] <- gsub("",
- "Research Institute for Nature and Forest (INBO)",
- mit[3])
+ mit[3] <- gsub("", org$get_rightsholder, mit[3])
writeLines(mit, license_file)
}
git_add("LICENSE.md", repo = repo)
diff --git a/R/organisation_class.R b/R/organisation_class.R
new file mode 100644
index 00000000..db927ff0
--- /dev/null
+++ b/R/organisation_class.R
@@ -0,0 +1,94 @@
+#' @title The organisation R6 class
+#' @description A class with the organisation defaults
+#' @export
+#' @importFrom R6 R6Class
+#' @family class
+organisation <- R6Class(
+ "organisation",
+ public = list(
+ #' @description Initialize a new `organisation` object.
+ initialize = function() {
+ invisible(self)
+ },
+ #' @description Print the `organisation` object.
+ #' @param ... currently ignored.
+ print = function(...) {
+ dots <- list(...)
+ c(
+ "rightsholder: %s", "funder: %s", "organisation email: %s",
+ "GitHub organisation: %s", "Zenodo community: %s",
+ "email domain settings"
+ ) |>
+ paste(collapse = "\n") |>
+ sprintf(
+ self$get_rightsholder, self$get_funder, self$get_email,
+ self$get_github, self$get_community
+ ) |>
+ cat()
+ org <- self$get_organisation
+ for (domain in names(org)) {
+ cat(
+ "\n-", domain, "\n mandatory ORCID iD"[org[[domain]]$orcid],
+ "\n affiliations",
+ sprintf(
+ "\n %s: %s", names(org[[domain]]$affiliation),
+ org[[domain]]$affiliation
+ )
+ )
+ }
+ return(invisible(NULL))
+ }
+ ),
+ active = list(
+ #' @field as_person The default organisation funder and rightsholder.
+ #' @importFrom utils person
+ as_person = function() {
+ person(
+ given = private$rightsholder, email = private$email,
+ role = c("cph", "fnd")
+ )
+ },
+ #' @field get_community The default organisation Zenodo communities.
+ get_community = function() {
+ private$community
+ },
+ #' @field get_email The default organisation email.
+ get_email = function() {
+ private$email
+ },
+ #' @field get_funder The default funder.
+ get_funder = function() {
+ private$funder
+ },
+ #' @field get_github The default GitHub organisation domain.
+ get_github = function() {
+ private$github
+ },
+ #' @field get_organisation The organisation requirements.
+ get_organisation = function() {
+ private$organisation
+ },
+ #' @field get_rightsholder The default rightsholder.
+ get_rightsholder = function() {
+ private$rightsholder
+ }
+ ),
+ private = list(
+ community = "inbo",
+ email = "info@inbo.be",
+ funder = "Research Institute for Nature and Forest (INBO)",
+ github = "inbo",
+ organisation = list(
+ "inbo.be" = list(
+ affiliation = c(
+ en = "Research Institute for Nature and Forest (INBO)",
+ nl = "Instituut voor Natuur- en Bosonderzoek (INBO)",
+ fr = "Institut de Recherche sur la Nature et les For\u00eats (INBO)",
+ de = "Institut f\u00fcr Natur- und Waldforschung (INBO)"
+ ),
+ orcid = TRUE
+ )
+ ),
+ rightsholder = "Research Institute for Nature and Forest (INBO)"
+ )
+)
diff --git a/R/read_checklist.R b/R/read_checklist.R
index f5854465..fb50399c 100644
--- a/R/read_checklist.R
+++ b/R/read_checklist.R
@@ -4,12 +4,17 @@
#' file in the root of a project.
#' This function reads this configuration.
#' It is mainly used by the other functions inside the package.
+#' If no `checklist.yml` file is found at the path,
+#' the function walks upwards through the directory structure until it finds
+#' such file.
+#' The function returns an error when it reaches the root of the disk without
+#' finding a `checklist.yml` file.
#' @param x Either a `checklist` object or a path to the source code.
#' Defaults to `.`.
#' @return A `checklist` object.
#' @export
#' @importFrom assertthat assert_that has_name is.string
-#' @importFrom fs is_dir is_file path path_real
+#' @importFrom fs is_dir is_file path path_real path_split
#' @importFrom yaml read_yaml
#' @family both
read_checklist <- function(x = ".") {
@@ -18,28 +23,17 @@ read_checklist <- function(x = ".") {
}
assert_that(is.string(x), is_dir(x))
- x <- path_real(x)
- checklist_file <- path(x, "checklist.yml")
- if (!is_file(checklist_file)) {
- # no check list file found
- desc_file <- path(x, "DESCRIPTION")
- if (!is_file(desc_file)) {
- message(
- "No `checklist.yml` or `DESCRIPTION` found. ",
- "Assuming this is a project."
- )
- x <- checklist$new(x = x, language = "en-GB", package = FALSE)
- x <- x$allowed()
- return(x)
- }
- message(
- "No `checklist.yml` found and existing `DESCRIPTION`. ",
- "Assuming this is a package."
- )
- x <- checklist$new(x = x, package = TRUE)
- x <- x$allowed()
- return(x)
+ current <- path_real(x)
+ checklist_file <- path(current, "checklist.yml")
+ while (!is_file(checklist_file) && length(path_split(current)[[1]]) > 1) {
+ path(current, "..") |>
+ path_real() -> current
+ checklist_file <- path(current, "checklist.yml")
}
+ assert_that(
+ is_file(checklist_file),
+ msg = sprintf("no checklist.yml found `%s` or its parents", x)
+ )
# read existing check list file
allowed <- read_yaml(checklist_file)
@@ -48,7 +42,12 @@ read_checklist <- function(x = ".") {
allowed$spelling <- list(default = "en-GB")
}
if (allowed$package) {
- x <- checklist$new(x = x, package = TRUE)
+ x <- checklist$new(
+ x = x, package = TRUE,
+ language = ifelse(
+ has_name(allowed$spelling, "default"), allowed$spelling$default, "en-GB"
+ )
+ )
} else {
x <- checklist$new(
x = x, package = FALSE,
diff --git a/R/setup_package.R b/R/setup_package.R
index 83ab60d9..d1141b03 100644
--- a/R/setup_package.R
+++ b/R/setup_package.R
@@ -33,6 +33,15 @@ setup_package <- function(path = ".", license = c("GPL-3", "MIT")) {
assert_that(is_workdir_clean(repo = path))
+ # add checklist.yml
+ if (!file_exists(path(path, "checklist.yml"))) {
+ x <- checklist$new(x = path, language = "en-GB", package = TRUE)
+ x$set_required()
+ x$set_ignore(c(".github", "LICENSE.md"))
+ write_checklist(x)
+ git_add("checklist.yml", force = TRUE, repo = path)
+ }
+
# make DESCRIPTION tidy
suppressMessages(tidy_desc(path))
git_add(files = "DESCRIPTION", force = TRUE, repo = path)
@@ -73,16 +82,6 @@ setup_package <- function(path = ".", license = c("GPL-3", "MIT")) {
)
}
- # add checklist.yml
- suppressMessages({
- x <- read_checklist(x = path)
- })
- x$package <- TRUE
- x$set_required()
- x$set_ignore(c(".github", "LICENSE.md"))
- write_checklist(x)
- git_add("checklist.yml", force = TRUE, repo = path)
-
# add codecov.yml
insert_file(
repo = path, filename = "codecov.yml", template = "package_template",
diff --git a/R/setup_project.R b/R/setup_project.R
index fb2b57e7..7941178b 100644
--- a/R/setup_project.R
+++ b/R/setup_project.R
@@ -226,12 +226,16 @@ create_readme <- function(path) {
) -> badges
}
}
+ org <- organisation$new()
c(
"", badges, "", "",
paste("#", title), "", author,
- "Research Institute for Nature and Forest (INBO)[^cph][^fnd]", "", footnote,
- "", keywords, "", "", "",
- "",
+ paste0(org$get_rightsholder, "[^cph][^fnd]"), "", footnote,
+ "", keywords, "",
+ sprintf(
+ "", paste(org$get_community, collapse = "; ")
+ ),
+ "", "",
"Replace this with a short description of the project.",
"It becomes the abstract of the project in the citation information.",
"And the project description at https://zenodo.org",
@@ -247,18 +251,24 @@ create_readme <- function(path) {
#' @importFrom utils menu
#' @importFrom yaml read_yaml write_yaml
preferred_protocol <- function() {
+ config <- list()
R_user_dir("checklist", which = "config") |>
path("config.yml") -> config_file
- config <- ifelse(file_exists(config_file), read_yaml(config_file), list())
+ if (file_exists(config_file)) {
+ config <- read_yaml(config_file)
+ }
if (
!has_name(config, "git") || !has_name(config$git, "protocol") ||
!has_name(config$git, "organisation")
) {
- config[["git"]][["organisation"]] <- readline(
- "What is your default GitHub organisation. Leave empty for `inbo`."
- )
+ org <- organisation$new()
+ sprintf(
+ "What is your default GitHub organisation. Leave empty for `%s`.",
+ org$get_github
+ ) |>
+ readline() -> config[["git"]][["organisation"]]
if (config[["git"]][["organisation"]] == "") {
- config[["git"]][["organisation"]] <- "inbo"
+ config[["git"]][["organisation"]] <- org$get_github
}
c("https (easy)", "ssh (more secure)") |>
menu_first(title = "Which protocol do you prefer?") -> protocol
@@ -297,8 +307,7 @@ renv_activate <- function(path) {
if (
isFALSE(
ask_yes_no(
- "Use `renv` to lock package versions with the project?",
- default = !identical(Sys.getenv("TESTTHAT"), "true")
+ "Use `renv` to lock package versions with the project?", default = FALSE
)
)
) {
diff --git a/R/sysdata.rda b/R/sysdata.rda
index fbe51c85..42fed4ff 100644
Binary files a/R/sysdata.rda and b/R/sysdata.rda differ
diff --git a/R/use_author.R b/R/use_author.R
index 5991555d..63f874e4 100644
--- a/R/use_author.R
+++ b/R/use_author.R
@@ -22,12 +22,21 @@ use_author <- function() {
sprintf("%s, %s", current$family, current$given) |>
c("new person") |>
menu_first("Which person information do you want to use?") -> selected
+ if (selected < 1) {
+ cat("You must select a person\n")
+ next
+ }
+ if (selected > nrow(current)) {
+ current <- new_author(current = current, root = root)
+ }
cat(
"given name: ", current$given[selected],
"\nfamily name:", current$family[selected],
"\ne-mail: ", current$email[selected],
"\norcid: ", current$orcid[selected],
- "\naffiliation:", current$affiliation[selected])
+ "\naffiliation:", current$affiliation[selected]
+ )
+ current <- validate_author(current = current, selected = selected)
final <- menu_first(choices = c("use ", "update", "other"))
if (final == 1) {
break
@@ -37,11 +46,12 @@ use_author <- function() {
next
}
}
- current$usage[selected] <- current$usage[selected] + 1
+ current$usage[selected] <- pmax(current$usage[selected], 0) + 1
write.table(
current, file = path(root, "author.txt"), sep = "\t", row.names = FALSE,
fileEncoding = "UTF8"
)
+ message("author information stored at ", path(root, "author.txt"))
return(current[selected, ])
}
@@ -69,6 +79,7 @@ update_author <- function(current, selected, root) {
"\norcid: ", current$orcid[selected],
"\naffiliation:", current$affiliation[selected]
)
+ current <- validate_author(current = current, selected = selected)
command <- menu(
choices = c(item, "save and exit", "undo changes and exit"),
title = "\nWhich item to update?"
@@ -91,6 +102,7 @@ update_author <- function(current, selected, root) {
current, file = path(root, "author.txt"), sep = "\t", row.names = FALSE,
fileEncoding = "UTF8"
)
+ message("author information stored at ", path(root, "author.txt"))
return(current)
}
@@ -100,15 +112,31 @@ new_author <- function(current, root) {
given = readline(prompt = "given name: "),
family = readline(prompt = "family name: "),
email = readline(prompt = "e-mail: "),
- orcid = readline(prompt = "orcid: "),
- affiliation = readline(prompt = "affiliation: "),
- usage = 0
- ) |>
- rbind(current) -> current
+ orcid = ask_orcid(prompt = "orcid: ")
+ ) -> extra
+ org <- organisation$new()$get_organisation
+ gsub(".*@", "", extra$email) |>
+ grepl(names(org), ignore.case = TRUE) |>
+ which() -> which_org
+ if (extra$email != "" && length(which_org) > 0) {
+ org <- org[which_org]
+ while (org[[1]]$orcid && extra$orcid == "") {
+ cat("An ORCID is required for", names(org))
+ extra$orcid <- ask_orcid(prompt = "orcid: ")
+ }
+ names(org[[1]]$affiliation) |>
+ menu_first(title = "Which default language for the affiliation?") -> lang
+ extra$affiliation <- org[[1]]$affiliation[lang]
+ } else {
+ extra$affiliation <- readline(prompt = "affiliation: ")
+ }
+ extra$usage <- 0
+ rbind(current, extra) -> current
write.table(
current, file = path(root, "author.txt"), sep = "\t", row.names = FALSE,
fileEncoding = "UTF8"
)
+ message("author information stored at ", path(root, "author.txt"))
return(current)
}
@@ -136,6 +164,7 @@ author2person <- function(role = "aut") {
)
}
+#' @importFrom utils tail
author2badge <- function(role = "aut") {
df <- use_author()
sprintf("[^%s]", role) |>
@@ -157,8 +186,20 @@ author2badge <- function(role = "aut") {
if (is.na(df$affiliation) || df$affiliation == "") {
return(badge)
}
+ org <- organisation$new()$get_organisation
+ vapply(
+ names(org), FUN.VALUE = vector(mode = "list", length = 1L),
+ FUN = function(x) {
+ data.frame(domain = x, affiliation = org[[x]]$affiliation) |>
+ list()
+ }
+ ) |>
+ do.call(what = rbind) -> aff_domain
+ aff <- aff_domain$domain[aff_domain$affiliation == df$affiliation]
gsub(".*\\((.+)\\).*", "\\1", df$affiliation) |>
- gsub(pattern = "[a-z]*\\s*", replacement = "") -> aff
+ abbreviate() |>
+ c(aff) |>
+ tail(1) -> aff
sprintf("%s[^%s]", badge, aff) |>
`attr<-`(
which = "footnote",
@@ -167,3 +208,79 @@ author2badge <- function(role = "aut") {
)
)
}
+
+validate_author <- function(current, selected) {
+ org <- organisation$new()$get_organisation
+ names(org) |>
+ gsub(pattern = "\\.", replacement = "\\\\.") |>
+ paste(collapse = "|") |>
+ sprintf(fmt = "@%s$") -> rg
+ if (!grepl(rg, current$email[selected], ignore.case = TRUE)) {
+ return(current)
+ }
+ this_org <- org[gsub(".*@", "", current$email[selected])]
+ while (
+ this_org[[1]]$orcid &&
+ (is.na(current$orcid[selected]) || current$orcid[selected] == "")
+ ) {
+ cat("\nAn ORCID is required for", names(this_org))
+ current$orcid[selected] <- ask_orcid(prompt = "orcid: ")
+ }
+ if (current$affiliation[selected] %in% this_org[[1]]$affiliation) {
+ return(current)
+ }
+ names(this_org[[1]]$affiliation) |>
+ menu_first(
+ title = sprintf(
+ "\nNon standard affiliation for `%s`.\n
+Which default language for the affiliation?",
+ names(this_org)
+ )
+ ) -> lang
+ current$affiliation[selected] <- this_org[[1]]$affiliation[lang]
+ return(current)
+}
+
+#' Validate the structure of an ORCID id
+#'
+#' Checks whether the ORCID has the proper format and the checksum.
+#' @param orcid A vector of ORCID
+#' @returns A logical vector with the same length as the input vector.
+#' @export
+#' @importFrom assertthat assert_that noNA
+#' @family utils
+validate_orcid <- function(orcid) {
+ assert_that(is.character(orcid), noNA(orcid))
+ format_ok <- grepl("^(\\d{4}-){3}\\d{3}[\\dX]$", orcid, perl = TRUE)
+ if (all(!format_ok)) {
+ return(orcid == "" | format_ok)
+ }
+ gsub("-", "", orcid[format_ok]) |>
+ strsplit(split = "") |>
+ do.call(what = cbind) -> digits
+ checksum <- digits[16, ]
+ seq_len(15) |>
+ rev() |>
+ matrix(ncol = 1) -> powers
+ apply(digits[-16, , drop = FALSE], 1, as.integer, simplify = FALSE) |>
+ do.call(what = rbind) |>
+ crossprod(2 ^ powers) |>
+ as.vector() -> total
+ remainder <- (12 - (total %% 11)) %% 11
+ remainder <- as.character(remainder)
+ remainder[remainder == "10"] <- "X"
+ format_ok[format_ok] <- remainder == checksum
+ return(orcid == "" | format_ok)
+}
+
+ask_orcid <- function(prompt = "orcid: ") {
+ orcid <- readline(prompt = prompt)
+ if (orcid == "") {
+ return(orcid)
+ }
+ while (!validate_orcid(orcid)) {
+ cat("\nPlease provide a valid ORCiD in the format `0000-0000-0000-0000`\n")
+ orcid <- readline(prompt = prompt)
+ }
+ return(orcid)
+}
diff --git a/_pkgdown.yml b/_pkgdown.yml
index 0cc3d926..b48526ec 100644
--- a/_pkgdown.yml
+++ b/_pkgdown.yml
@@ -52,6 +52,7 @@ articles:
contents:
- philosophy
- folder
+ - path
- getting_started
- getting_started_project
- spelling
diff --git a/inst/CITATION b/inst/CITATION
index b1056616..142157aa 100644
--- a/inst/CITATION
+++ b/inst/CITATION
@@ -2,12 +2,12 @@ citHeader("To cite `checklist` in publications please use:")
# begin checklist entry
bibentry(
bibtype = "Manual",
- title = "checklist: A Thorough and Strict Set of Checks for R Packages and Source Code. Version 0.3.2",
+ title = "checklist: A Thorough and Strict Set of Checks for R Packages and Source Code. Version 0.3.3",
author = c( author = c(person(given = "Thierry", family = "Onkelinx"))),
year = 2023,
url = "https://inbo.github.io/checklist/",
abstract = "An opinionated set of rules for R packages and R source code projects.",
- textVersion = "Onkelinx, Thierry (2023) checklist: A Thorough and Strict Set of Checks for R Packages and Source Code. Version 0.3.2. https://github.com/inbo/checklist/; https://inbo.github.io/checklist/",
+ textVersion = "Onkelinx, Thierry (2023) checklist: A Thorough and Strict Set of Checks for R Packages and Source Code. Version 0.3.3. https://github.com/inbo/checklist/; https://inbo.github.io/checklist/",
keywords = "quality control; documentation; publication",
doi = "10.5281/zenodo.4028303",
)
diff --git a/inst/bookdown/index.Rmd b/inst/bookdown/index.Rmd
index 8ebf5d4a..00ded660 100644
--- a/inst/bookdown/index.Rmd
+++ b/inst/bookdown/index.Rmd
@@ -2,7 +2,7 @@
title: Dummy report for unit testing
author:
- name:
- given: Josiah
+ given: Josiah
family: Carberry
email: carberry@junk.com
orcid: 0000-0002-1825-0097
@@ -16,8 +16,8 @@ reviewer:
orcid: 0000-0002-1825-0097
affiliation: Wesleyan University
lang: nl
-keywords: "checklist; unit test"
-community: "inbo"
+keywords: checklist; unit test
+community: inbo
publication_type: report
funder: Research Institute for Nature and Forest (INBO)
rightsholder: Research Institute for Nature and Forest (INBO)
diff --git a/inst/generic_template/gitignore b/inst/generic_template/gitignore
index 1b9dd10b..c9155065 100644
--- a/inst/generic_template/gitignore
+++ b/inst/generic_template/gitignore
@@ -1,7 +1,8 @@
.DS_Store
-.Rproj.user
-.Rhistory
.RData
+.Renviron
+.Rhistory
+.Rproj.user
.Ruserdata
.httr-oauth
*.dbf
diff --git a/inst/package_template/release.yml b/inst/package_template/release.yml
index 8445d7a5..3b2f57d3 100644
--- a/inst/package_template/release.yml
+++ b/inst/package_template/release.yml
@@ -19,10 +19,13 @@ jobs:
- uses: actions/checkout@v3
- name: Get tag message
run: |
- TAG_BODY=$(git tag --contains ${{ github.sha }} -n100 | awk '(NR>1)')
- echo "::set-output name=TAG_BODY::$TAG_BODY"
+ TAG=$(git tag --contains $(git rev-parse HEAD))
+ TAG_BODY=$(git tag --contains {{ github.sha }} -n100 | awk '(NR>1)')
+ echo "TAG=$TAG" >> $GITHUB_OUTPUT
+ echo "TAG_BODY=$TAG_BODY" >> $GITHUB_OUTPUT
id: tag-body
- uses: ncipollo/release-action@v1
with:
- name: Release ${{ github.ref }}
+ name: Release ${{ steps.tag-body.outputs.TAG }}
+ tag: ${{ steps.tag-body.outputs.TAG }}
body: ${{ steps.tag-body.outputs.TAG_BODY }}
diff --git a/man/ask_yes_no.Rd b/man/ask_yes_no.Rd
index e338cca2..44c2679b 100644
--- a/man/ask_yes_no.Rd
+++ b/man/ask_yes_no.Rd
@@ -49,6 +49,7 @@ Other utils:
\code{\link{store_authors}()},
\code{\link{use_author}()},
\code{\link{validate_email}()},
+\code{\link{validate_orcid}()},
\code{\link{yesno}()}
}
\concept{utils}
diff --git a/man/bookdown_zenodo.Rd b/man/bookdown_zenodo.Rd
index cd1adb12..897e0c09 100644
--- a/man/bookdown_zenodo.Rd
+++ b/man/bookdown_zenodo.Rd
@@ -56,6 +56,7 @@ Other utils:
\code{\link{store_authors}()},
\code{\link{use_author}()},
\code{\link{validate_email}()},
+\code{\link{validate_orcid}()},
\code{\link{yesno}()}
}
\concept{utils}
diff --git a/man/c_sort.Rd b/man/c_sort.Rd
index 6dfd12a4..e375db8f 100644
--- a/man/c_sort.Rd
+++ b/man/c_sort.Rd
@@ -32,6 +32,7 @@ Other utils:
\code{\link{store_authors}()},
\code{\link{use_author}()},
\code{\link{validate_email}()},
+\code{\link{validate_orcid}()},
\code{\link{yesno}()}
}
\concept{utils}
diff --git a/man/checklist.Rd b/man/checklist.Rd
index f6b22e96..1c94219e 100644
--- a/man/checklist.Rd
+++ b/man/checklist.Rd
@@ -9,6 +9,7 @@ A class which contains all checklist results.
\seealso{
Other class:
\code{\link{citation_meta}},
+\code{\link{organisation}},
\code{\link{spelling}}
}
\concept{class}
diff --git a/man/citation_meta.Rd b/man/citation_meta.Rd
index fa9bea01..01b15acf 100644
--- a/man/citation_meta.Rd
+++ b/man/citation_meta.Rd
@@ -9,6 +9,7 @@ A class which contains citation information.
\seealso{
Other class:
\code{\link{checklist}},
+\code{\link{organisation}},
\code{\link{spelling}}
}
\concept{class}
diff --git a/man/clean_git.Rd b/man/clean_git.Rd
index d3543632..92778c1d 100644
--- a/man/clean_git.Rd
+++ b/man/clean_git.Rd
@@ -41,6 +41,7 @@ Other utils:
\code{\link{store_authors}()},
\code{\link{use_author}()},
\code{\link{validate_email}()},
+\code{\link{validate_orcid}()},
\code{\link{yesno}()}
}
\concept{utils}
diff --git a/man/create_hexsticker.Rd b/man/create_hexsticker.Rd
index f06c1ac5..1eb8f31b 100644
--- a/man/create_hexsticker.Rd
+++ b/man/create_hexsticker.Rd
@@ -58,6 +58,7 @@ Other utils:
\code{\link{store_authors}()},
\code{\link{use_author}()},
\code{\link{validate_email}()},
+\code{\link{validate_orcid}()},
\code{\link{yesno}()}
}
\concept{utils}
diff --git a/man/defunct.Rd b/man/defunct.Rd
index 8c68d6e9..95c64d8d 100644
--- a/man/defunct.Rd
+++ b/man/defunct.Rd
@@ -24,6 +24,7 @@ Other utils:
\code{\link{store_authors}()},
\code{\link{use_author}()},
\code{\link{validate_email}()},
+\code{\link{validate_orcid}()},
\code{\link{yesno}()}
}
\concept{utils}
diff --git a/man/execshell.Rd b/man/execshell.Rd
index 67c2d940..a2220a3b 100644
--- a/man/execshell.Rd
+++ b/man/execshell.Rd
@@ -39,6 +39,7 @@ Other utils:
\code{\link{store_authors}()},
\code{\link{use_author}()},
\code{\link{validate_email}()},
+\code{\link{validate_orcid}()},
\code{\link{yesno}()}
}
\concept{utils}
diff --git a/man/is_repository.Rd b/man/is_repository.Rd
index a17f3788..ec650a2b 100644
--- a/man/is_repository.Rd
+++ b/man/is_repository.Rd
@@ -33,6 +33,7 @@ Other utils:
\code{\link{store_authors}()},
\code{\link{use_author}()},
\code{\link{validate_email}()},
+\code{\link{validate_orcid}()},
\code{\link{yesno}()}
}
\concept{utils}
diff --git a/man/is_workdir_clean.Rd b/man/is_workdir_clean.Rd
index e16111b9..2ae6578b 100644
--- a/man/is_workdir_clean.Rd
+++ b/man/is_workdir_clean.Rd
@@ -35,6 +35,7 @@ Other utils:
\code{\link{store_authors}()},
\code{\link{use_author}()},
\code{\link{validate_email}()},
+\code{\link{validate_orcid}()},
\code{\link{yesno}()}
}
\concept{utils}
diff --git a/man/menu_first.Rd b/man/menu_first.Rd
index 80d28f98..4e3bd0d3 100644
--- a/man/menu_first.Rd
+++ b/man/menu_first.Rd
@@ -33,6 +33,7 @@ Other utils:
\code{\link{store_authors}()},
\code{\link{use_author}()},
\code{\link{validate_email}()},
+\code{\link{validate_orcid}()},
\code{\link{yesno}()}
}
\concept{utils}
diff --git a/man/new_branch.Rd b/man/new_branch.Rd
index 49be2981..befa4869 100644
--- a/man/new_branch.Rd
+++ b/man/new_branch.Rd
@@ -38,6 +38,7 @@ Other utils:
\code{\link{store_authors}()},
\code{\link{use_author}()},
\code{\link{validate_email}()},
+\code{\link{validate_orcid}()},
\code{\link{yesno}()}
}
\concept{utils}
diff --git a/man/organisation.Rd b/man/organisation.Rd
new file mode 100644
index 00000000..242d1a8f
--- /dev/null
+++ b/man/organisation.Rd
@@ -0,0 +1,87 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/organisation_class.R
+\name{organisation}
+\alias{organisation}
+\title{The organisation R6 class}
+\description{
+A class with the organisation defaults
+}
+\seealso{
+Other class:
+\code{\link{checklist}},
+\code{\link{citation_meta}},
+\code{\link{spelling}}
+}
+\concept{class}
+\section{Active bindings}{
+\if{html}{\out{}}
+\describe{
+\item{\code{as_person}}{The default organisation funder and rightsholder.}
+
+\item{\code{get_community}}{The default organisation Zenodo communities.}
+
+\item{\code{get_email}}{The default organisation email.}
+
+\item{\code{get_funder}}{The default funder.}
+
+\item{\code{get_github}}{The default GitHub organisation domain.}
+
+\item{\code{get_organisation}}{The organisation requirements.}
+
+\item{\code{get_rightsholder}}{The default rightsholder.}
+}
+\if{html}{\out{
}}
+}
+\section{Methods}{
+\subsection{Public methods}{
+\itemize{
+\item \href{#method-organisation-new}{\code{organisation$new()}}
+\item \href{#method-organisation-print}{\code{organisation$print()}}
+\item \href{#method-organisation-clone}{\code{organisation$clone()}}
+}
+}
+\if{html}{\out{
}}
+\if{html}{\out{}}
+\if{latex}{\out{\hypertarget{method-organisation-new}{}}}
+\subsection{Method \code{new()}}{
+Initialize a new \code{organisation} object.
+\subsection{Usage}{
+\if{html}{\out{}}\preformatted{organisation$new()}\if{html}{\out{
}}
+}
+
+}
+\if{html}{\out{
}}
+\if{html}{\out{}}
+\if{latex}{\out{\hypertarget{method-organisation-print}{}}}
+\subsection{Method \code{print()}}{
+Print the \code{organisation} object.
+\subsection{Usage}{
+\if{html}{\out{}}\preformatted{organisation$print(...)}\if{html}{\out{
}}
+}
+
+\subsection{Arguments}{
+\if{html}{\out{}}
+\describe{
+\item{\code{...}}{currently ignored.}
+}
+\if{html}{\out{
}}
+}
+}
+\if{html}{\out{
}}
+\if{html}{\out{}}
+\if{latex}{\out{\hypertarget{method-organisation-clone}{}}}
+\subsection{Method \code{clone()}}{
+The objects of this class are cloneable with this method.
+\subsection{Usage}{
+\if{html}{\out{}}\preformatted{organisation$clone(deep = FALSE)}\if{html}{\out{
}}
+}
+
+\subsection{Arguments}{
+\if{html}{\out{}}
+\describe{
+\item{\code{deep}}{Whether to make a deep clone.}
+}
+\if{html}{\out{
}}
+}
+}
+}
diff --git a/man/read_checklist.Rd b/man/read_checklist.Rd
index 9f09eb77..fb4f7f24 100644
--- a/man/read_checklist.Rd
+++ b/man/read_checklist.Rd
@@ -18,6 +18,11 @@ The checklist package stores configuration information in the \code{checklist.ym
file in the root of a project.
This function reads this configuration.
It is mainly used by the other functions inside the package.
+If no \code{checklist.yml} file is found at the path,
+the function walks upwards through the directory structure until it finds
+such file.
+The function returns an error when it reaches the root of the disk without
+finding a \code{checklist.yml} file.
}
\seealso{
Other both:
diff --git a/man/spelling.Rd b/man/spelling.Rd
index 4e4dca9a..6e67caca 100644
--- a/man/spelling.Rd
+++ b/man/spelling.Rd
@@ -9,7 +9,8 @@ A class with the configuration for spell checking
\seealso{
Other class:
\code{\link{checklist}},
-\code{\link{citation_meta}}
+\code{\link{citation_meta}},
+\code{\link{organisation}}
}
\concept{class}
\section{Active bindings}{
diff --git a/man/store_authors.Rd b/man/store_authors.Rd
index 0ee053c6..03b5610b 100644
--- a/man/store_authors.Rd
+++ b/man/store_authors.Rd
@@ -28,6 +28,7 @@ Other utils:
\code{\link{orcid2person}()},
\code{\link{use_author}()},
\code{\link{validate_email}()},
+\code{\link{validate_orcid}()},
\code{\link{yesno}()}
}
\concept{utils}
diff --git a/man/use_author.Rd b/man/use_author.Rd
index b4036b7c..4132a779 100644
--- a/man/use_author.Rd
+++ b/man/use_author.Rd
@@ -28,6 +28,7 @@ Other utils:
\code{\link{orcid2person}()},
\code{\link{store_authors}()},
\code{\link{validate_email}()},
+\code{\link{validate_orcid}()},
\code{\link{yesno}()}
}
\concept{utils}
diff --git a/man/validate_email.Rd b/man/validate_email.Rd
index 8aa1b28c..ac2b80f6 100644
--- a/man/validate_email.Rd
+++ b/man/validate_email.Rd
@@ -30,6 +30,7 @@ Other utils:
\code{\link{orcid2person}()},
\code{\link{store_authors}()},
\code{\link{use_author}()},
+\code{\link{validate_orcid}()},
\code{\link{yesno}()}
}
\concept{utils}
diff --git a/man/validate_orcid.Rd b/man/validate_orcid.Rd
new file mode 100644
index 00000000..13761dc5
--- /dev/null
+++ b/man/validate_orcid.Rd
@@ -0,0 +1,36 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/use_author.R
+\name{validate_orcid}
+\alias{validate_orcid}
+\title{Validate the structure of an ORCID id}
+\usage{
+validate_orcid(orcid)
+}
+\arguments{
+\item{orcid}{A vector of ORCID}
+}
+\value{
+A logical vector with the same length as the input vector.
+}
+\description{
+Checks whether the ORCID has the proper format and the checksum.
+}
+\seealso{
+Other utils:
+\code{\link{ask_yes_no}()},
+\code{\link{bookdown_zenodo}()},
+\code{\link{c_sort}()},
+\code{\link{clean_git}()},
+\code{\link{create_hexsticker}()},
+\code{\link{execshell}()},
+\code{\link{is_repository}()},
+\code{\link{is_workdir_clean}()},
+\code{\link{menu_first}()},
+\code{\link{new_branch}()},
+\code{\link{orcid2person}()},
+\code{\link{store_authors}()},
+\code{\link{use_author}()},
+\code{\link{validate_email}()},
+\code{\link{yesno}()}
+}
+\concept{utils}
diff --git a/man/yesno.Rd b/man/yesno.Rd
index c27068f7..03c3f7c9 100644
--- a/man/yesno.Rd
+++ b/man/yesno.Rd
@@ -30,7 +30,8 @@ Other utils:
\code{\link{orcid2person}()},
\code{\link{store_authors}()},
\code{\link{use_author}()},
-\code{\link{validate_email}()}
+\code{\link{validate_email}()},
+\code{\link{validate_orcid}()}
}
\author{
Hadley Wickham \href{mailto:Hadley@Rstudio.com}{Hadley@Rstudio.com}
diff --git a/tests/testthat/test_a_author.R b/tests/testthat/test_a_author.R
index 1b7b16fc..9520b28d 100644
--- a/tests/testthat/test_a_author.R
+++ b/tests/testthat/test_a_author.R
@@ -1,11 +1,17 @@
library(mockery)
test_that("author tools", {
+ stub(ask_orcid, "readline", mock(""))
+ expect_equal(ask_orcid(), "")
+ stub(ask_orcid, "readline", mock("junk", "0000-0002-1825-0097"))
+ expect_equal(ask_orcid(), "0000-0002-1825-0097")
+
root <- tempfile("author")
expect_false(is_dir(root))
expect_is(stored_authors(root), "data.frame")
expect_true(is_dir(root))
expect_is(stored_authors(root), "data.frame")
- stub(new_author, "readline", mock("John", "Doe", "", "", ""))
+ stub(new_author, "readline", mock("John", "Doe", "", ""))
+ stub(new_author, "ask_orcid", "")
expect_output(new_author(current = data.frame(), root = root))
expect_true(file_exists(path(root, "author.txt")))
current <- stored_authors(root)
@@ -44,22 +50,24 @@ test_that("author tools", {
})
expect_equal(ab, badge)
- stub(update_author, "menu", mock(3, 6))
- stub(update_author, "readline", "john@doe.com", depth = 2)
- expect_output(update_author(current = current, selected = 1, root = root))
- current$email <- "john@doe.com"
- expect_identical(current, stored_authors(root))
-
+ org <- organisation$new()
stub(update_author, "menu", mock(5, 6))
- stub(update_author, "readline", "University of Life", depth = 2)
+ stub(
+ update_author, "readline", org$get_organisation[["inbo.be"]]$affiliation[1],
+ depth = 2
+ )
expect_output(update_author(current = current, selected = 1, root = root))
- current$affiliation <- "University of Life"
+ current$affiliation <- org$get_organisation[["inbo.be"]]$affiliation[1]
expect_identical(current, stored_authors(root))
badge <- paste0(
"[Doe, John![ORCID logo](https://info.orcid.org/wp-content/uploads/2019/",
- "11/orcid_16x16.png)](https://orcid.org/0000-0002-1825-0097)[^aut][^UL]"
+ "11/orcid_16x16.png)]",
+ "(https://orcid.org/0000-0002-1825-0097)[^aut][^inbo.be]"
+ )
+ attr(badge, "footnote") <- c(
+ "[^aut]: author",
+ paste("[^inbo.be]:", org$get_organisation[["inbo.be"]]$affiliation[1])
)
- attr(badge, "footnote") <- c("[^aut]: author", "[^UL]: University of Life")
expect_output({
ab <- author2badge()
})
@@ -69,8 +77,22 @@ test_that("author tools", {
})
expect_is(ap, "person")
+ stub(update_author, "menu", mock(3, 6))
+ stub(update_author, "readline", "noreply@inbo.be", depth = 2)
+ expect_output(update_author(current = current, selected = 1, root = root))
+ current$email <- "noreply@inbo.be"
+ expect_identical(current, stored_authors(root))
+ expect_output({
+ ap <- author2person()
+ })
+ expect_is(ap, "person")
+
expect_null(coalesce(NULL))
expect_identical(coalesce(NULL, "a"), "a")
expect_identical(coalesce(NULL, "a", "b"), "a")
expect_identical(coalesce("a", NULL, "b"), "a")
+
+ stub(new_author, "readline", mock("Jane", "Doe", "noreply@inbo.be"))
+ stub(new_author, "ask_orcid", mock("", "0000-0002-1825-0097"))
+ expect_output(new_author(current, root = root))
})
diff --git a/tests/testthat/test_a_organisation.R b/tests/testthat/test_a_organisation.R
new file mode 100644
index 00000000..eb84267a
--- /dev/null
+++ b/tests/testthat/test_a_organisation.R
@@ -0,0 +1,4 @@
+test_that("organisation class", {
+ org <- organisation$new()
+ expect_output(print(org), regexp = "rightsholder")
+})
diff --git a/tests/testthat/test_a_read_checklist.R b/tests/testthat/test_a_read_checklist.R
index 68dce3b3..f40e7eee 100644
--- a/tests/testthat/test_a_read_checklist.R
+++ b/tests/testthat/test_a_read_checklist.R
@@ -1,7 +1,9 @@
test_that("read_checklist works", {
target <- tempfile("checklist")
- dir.create(target)
- # no checklist.yml
+ dir_create(target)
+ defer(dir_delete(target))
+ checklist$new(target, language = "en-GB", package = FALSE) |>
+ write_checklist()
suppressMessages(expect_is(x <- read_checklist(target), "checklist"))
expect_identical(read_checklist(x), x)
expect_identical(x$get_path, path_real(target))
@@ -11,5 +13,4 @@ test_that("read_checklist works", {
expect_length(x$.__enclos_env__$private$allowed_notes, 0)
expect_length(x$.__enclos_env__$private$allowed_warnings, 0)
expect_identical(x$default, "en-GB")
- unlink(target, recursive = TRUE)
})
diff --git a/tests/testthat/test_b_check_environment.R b/tests/testthat/test_b_check_environment.R
index a60d68d1..b6b19f92 100644
--- a/tests/testthat/test_b_check_environment.R
+++ b/tests/testthat/test_b_check_environment.R
@@ -1,7 +1,9 @@
test_that("check_environment() works", {
tmp_dir <- tempfile()
- dir.create(tmp_dir)
+ dir_create(tmp_dir)
defer(file_delete(tmp_dir))
+ checklist$new(tmp_dir, language = "en-GB", package = FALSE) |>
+ write_checklist()
old_gha <- Sys.getenv("GITHUB_ACTIONS")
old_codecov <- Sys.getenv("CODECOV_TOKEN")
diff --git a/tests/testthat/test_b_check_filename.R b/tests/testthat/test_b_check_filename.R
index b5c09973..ab46cabe 100644
--- a/tests/testthat/test_b_check_filename.R
+++ b/tests/testthat/test_b_check_filename.R
@@ -3,6 +3,8 @@ test_that("check_filename() works", {
path <- tempfile("check_filename")
dir_create(path)
defer(unlink(path, recursive = TRUE))
+ checklist$new(path, language = "en-GB", package = FALSE) |>
+ write_checklist()
# fail on white space in folder names
dir_create(path(path, "with space"))
diff --git a/tests/testthat/test_c_create_package.R b/tests/testthat/test_c_create_package.R
index 3c970468..76597b3d 100644
--- a/tests/testthat/test_c_create_package.R
+++ b/tests/testthat/test_c_create_package.R
@@ -4,7 +4,10 @@ test_that("create_package() works", {
maintainer <- person(
given = "Thierry", family = "Onkelinx", role = c("aut", "cre"),
email = "thierry.onkelinx@inbo.be",
- comment = c(ORCID = "0000-0001-8804-4216")
+ comment = c(
+ ORCID = "0000-0001-8804-4216",
+ affiliation = "Research Institute for Nature and Forest (INBO)"
+ )
)
path <- tempfile("create_package")
dir.create(path)
@@ -25,7 +28,8 @@ test_that("create_package() works", {
r_user_dir <- tempfile("author")
dir.create(r_user_dir)
- stub(new_author, "readline", mock("John", "Doe", "john@doe.com", "", ""))
+ stub(new_author, "readline", mock("John", "Doe", "john@doe.com", ""))
+ stub(new_author, "ask_orcid", mock(""))
expect_output(new_author(current = data.frame(), root = r_user_dir))
stub(store_authors, "R_user_dir", r_user_dir)
expect_invisible(store_authors(repo))
diff --git a/tests/testthat/test_d_check_license.R b/tests/testthat/test_d_check_license.R
index ba83943a..af646b53 100644
--- a/tests/testthat/test_d_check_license.R
+++ b/tests/testthat/test_d_check_license.R
@@ -42,11 +42,13 @@ test_that("check_license() works", {
git_config_set(name = "user.email", value = "junk@inbo.be", repo = repo)
gert::git_commit("initial commit", repo = repo)
+ org <- organisation$new()
mit <- readLines(path(repo, "LICENSE.md"))
expect_identical(
mit[3],
- paste0("Copyright (c) ", format(Sys.Date(), "%Y"),
- " Research Institute for Nature and Forest (INBO)")
+ sprintf(
+ "Copyright (c) %s %s", format(Sys.Date(), "%Y"), org$get_rightsholder
+ )
)
expect_identical(
file.exists(path(repo, "LICENSE")),
diff --git a/tests/testthat/test_d_check_spelling.R b/tests/testthat/test_d_check_spelling.R
index 38f2394d..386bcfc9 100644
--- a/tests/testthat/test_d_check_spelling.R
+++ b/tests/testthat/test_d_check_spelling.R
@@ -123,7 +123,8 @@ test_that("check_spelling() on a project", {
r_user_dir <- tempfile("author")
dir.create(r_user_dir)
- stub(new_author, "readline", mock("John", "Doe", "john@doe.com", "", ""))
+ stub(new_author, "readline", mock("John", "Doe", "john@doe.com", ""))
+ stub(new_author, "ask_orcid", mock(""))
expect_output(new_author(current = data.frame(), root = r_user_dir))
stub(create_project, "R_user_dir", r_user_dir, depth = 5)
stub(create_project, "readline", "test")
@@ -137,9 +138,10 @@ test_that("check_spelling() on a project", {
}
)
- path(path, "spelling", "bookdown") |>
+ path(path, "spelling", "source", "bookdown") |>
dir_create()
- path(path, "spelling", "bookdown", c("_bookdown.yml", "test.Rproj")) |>
+ path |>
+ path("spelling", "source", "bookdown", c("_bookdown.yml", "test.Rproj")) |>
fs::file_create()
stub(store_authors, "R_user_dir", r_user_dir)
@@ -182,7 +184,8 @@ test_that("check_spelling() on a project", {
r_user_dir <- tempfile("author")
dir.create(r_user_dir)
- stub(new_author, "readline", mock("John", "Doe", "john@doe.com", "", ""))
+ stub(new_author, "readline", mock("John", "Doe", "john@doe.com", ""))
+ stub(new_author, "ask_orcid", mock(""))
expect_output(new_author(current = data.frame(), root = r_user_dir))
hide_author <- tempfile(fileext = ".txt")
@@ -198,7 +201,8 @@ test_that("check_spelling() on a project", {
sink()
expect_is(check_project(path(path, "spelling"), quiet = TRUE), "checklist")
- x <- read_checklist(path)
+ path(path, "spelling") |>
+ read_checklist() -> x
stub(change_language_interactive, "menu", 3)
stub(change_language_interactive2, "menu", 1, 2)
expect_is(
@@ -325,6 +329,8 @@ test_that("check_spelling() works on a quarto project", {
path <- tempfile("quarto")
dir_create(path)
defer(unlink(path, recursive = TRUE))
+ checklist$new(path, language = "en-GB", package = FALSE) |>
+ write_checklist()
dir_create(path, "source")
writeLines(
c("project:", " type: book"),
diff --git a/tests/testthat/test_d_update_citation.R b/tests/testthat/test_d_update_citation.R
index 4393ceae..24962a85 100644
--- a/tests/testthat/test_d_update_citation.R
+++ b/tests/testthat/test_d_update_citation.R
@@ -1,7 +1,11 @@
test_that("update_citation() works", {
maintainer <- person(
given = "Thierry", family = "Onkelinx", role = c("aut", "cre"),
- email = "thierry.onkelinx@inbo.be"
+ email = "thierry.onkelinx@inbo.be",
+ comment = c(
+ ORCID = "0000-0001-8804-4216",
+ affiliation = "Research Institute for Nature and Forest (INBO)"
+ )
)
path <- tempfile("citation")
dir.create(path)
@@ -45,7 +49,8 @@ test_that("update_citation() works", {
this_description <- desc(path(path, package))
this_description$add_urls("https://doi.org/10.5281/zenodo.4028303")
- this_description$del_author("Research Institute for Nature and Forest (INBO)")
+ org <- organisation$new()
+ this_description$del_author(org$get_rightsholder)
this_description$add_author(given = "unit", family = "test", role = "ctb")
this_description$add_author(given = "test", family = "unit", role = "cph")
this_description$write(path(path, package))
diff --git a/tests/testthat/test_e_bookdown_zenodo.R b/tests/testthat/test_e_bookdown_zenodo.R
index aa0fc377..47745f31 100644
--- a/tests/testthat/test_e_bookdown_zenodo.R
+++ b/tests/testthat/test_e_bookdown_zenodo.R
@@ -29,12 +29,6 @@ test_that("bookdown_zenodo() works", {
)
)
sink()
- output <- readLines(zenodo_out)
- output <- output[!grepl("\\|(\\s|=|\\.)+\\|", output, perl = TRUE)]
- output <- output[!grepl("^(\\s|\\|)*$", output, perl = TRUE)]
- output <- output[!grepl("pandoc.+--to.+--from.+--output", output)]
- output <- output[!grepl("Nothing to remove", output)]
- expect_length(output, 0)
manager <- zen4R::ZenodoManager$new(sandbox = TRUE, token = sandbox_token)
expect_true(
manager$deleteRecord(x$record_id),
diff --git a/vignettes/path.Rmd b/vignettes/path.Rmd
new file mode 100644
index 00000000..dbe186fa
--- /dev/null
+++ b/vignettes/path.Rmd
@@ -0,0 +1,123 @@
+---
+title: "File paths in code"
+output: rmarkdown::html_vignette
+vignette: >
+ %\VignetteIndexEntry{File paths in code}
+ %\VignetteEngine{knitr::rmarkdown}
+ %\VignetteEncoding{UTF-8}
+---
+
+```{r setup, include = FALSE}
+library(knitr)
+opts_chunk$set(collapse = TRUE, comment = "#>")
+```
+
+## Do not use absolute file paths
+
+An absolute file path defines the location of a file or folder starting from the root of a disk.
+For example `C:\tmp\test.txt` on a Windows machine or `/tmp/test.txt` on a Unix machine.
+Using an absolute path in your code will likely break the code as soon as you run it on a different machine.
+Because that file or folder is probably located somewhere else on the other machine.
+
+`check_lintr()` looks for absolute paths in your code and turns them into an error.
+This check is optional (but strongly recommended) with `check_project()` and mandatory with `check_package()`.
+
+## Easiest solution: use relative paths within the project
+
+In `vignette("folder", package = "checklist")` we recommend what folder structure to use in an R project.
+
+We expect users to run the scripts with the base RStudio project as working directory.
+The base RStudio project is the one containing the `checklist.yml` file.
+Make every path relative to the location of this file.
+
+In case of **Rmarkdown** files, use paths relative to the location of the main Rmarkdown file.
+And make sure to work in an RStudio project at the location of that file.
+Note that you can have "nested" RStudio projects.
+
+Let's illustrate this with an example.
+We assume a base RStudio project with a `checklist.yml` file at its root.
+Suppose the main Rmarkdown file is `source/bookdown/index.Rmd`.
+We have an RStudio project at `source/bookdown`, so that the working directory is by default at this location.
+In one of the code chunks you want to read `data/observations.txt`.
+Then point the read function to `../../data/observations.txt`.
+`..` moves one step towards the root of the disk.
+The working directory within the Rmarkdown files will be `source/bookdown`.
+A single `..` points in this case at `source`.
+`../..` points from `source/bookdown` to the root of the project.
+Then we need to move up into the `data` folder by using `../../data`.
+
+Another option is to use the `get_path` method on the `checklist` object to get this location.
+Then use that information to define the absolute location of the files within the project.
+
+```{r checklist, eval = FALSE}
+library(checklist)
+x <- read_checklist()
+file.path(x$get_path, "data", "observations.txt")
+```
+
+## Alternative solution: use relative paths between projects
+
+Sometimes you need to store the data outside of the project.
+For example because several projects share the same data.
+In such case you can place the projects and the data in a shared folder structure.
+Then you can still rely on relative paths to point to the common data outside of the project.
+Note that this requires that every user complies to use the same structure.
+The maintainer should document this structure and give the user instructions on how to set up a project.
+A good example can be found in the [data storage vignette](https://inbo.github.io/n2khab/articles/v020_datastorage.html) of the [`n2khab`](https://inbo.github.io/n2khab/index.html) package.
+
+```
+superproject
+|-- shared_data
+|-- project_a
+ |-- source
+|-- project_b
+ |-- source
+```
+
+## Fallback solution: ask the user to specify the path
+
+This solution relies on setting a system variable on the computer of the user.
+Simply create a `.Renviron` text file at the root of the RStudio project.
+This file contains a list of key-value pairs as shown in the example below.
+Please choose a more suitable name than `MYPROJECT_DATA`.
+Using a project specific name minimises the potential of multiple projects using the same system variables.
+
+Content of `.Renviron`
+```
+MYPROJECT_DATA="C:\temp"
+```
+
+Starting the RStudio project will load the system variables set in `.Renviron`.
+Keep in mind that you need might a `.Renviron` in every RStudio project where you use this trick.
+
+Location of `.Renviron`
+```
+project_a
+|-- .Renviron
+|-- project_a.Rproj
+|-- data
+|-- source
+ |-- bookdown
+ |-- .Renviron
+ |-- bookdown.Rproj
+ |-- index.Rmd
+ |-- script.R
+```
+
+The following R code chunk illustrates how to read and use the system variable.
+Besides writing good documentation, you should check the content of the system variable.
+Your code should provide helpful errors when the user fails to set the correct system variable.
+In the example below we use `stopifnot()`.
+This function uses named expressions.
+Every expression must yield a single `TRUE` or `FALSE`.
+When the expression yields `FALSE`, the function returns an error using the name of the expression.
+When the expression yields `TRUE`, the code continues without a message.
+
+```{r sys-getenv, eval = FALSE}
+data_path <- Sys.getenv("MYPROJECT_DATA", NA)
+stopifnot(
+ "System variable `MYPROJECT_DATA` not set" = !is.na(data_path),
+ "`MYPROJECT_DATA` is not an existing directory" = file_test("-d", data_path)
+)
+file.path(data_path, "data", "observations.txt")
+```