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") +```