From 82bdc3886461614da016c574997fdcf7461af743 Mon Sep 17 00:00:00 2001 From: philbosch Date: Mon, 27 Oct 2025 14:38:11 +0000 Subject: [PATCH 1/5] docu: reflect changes after user testings --- NAMESPACE | 1 + README.Rmd | 14 ++++++++++++-- README.md | 16 ++++++++++++++- man/create_dataset.Rd | 2 +- man/create_distribution.Rd | 3 ++- man/dataset_is_valid_for_status.Rd | 31 ++++++++++++++++++++++++++++++ man/update_distribution.Rd | 3 ++- 7 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 man/dataset_is_valid_for_status.Rd diff --git a/NAMESPACE b/NAMESPACE index 0d30ca5..fd0f0cc 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -5,6 +5,7 @@ export(Dataset) export(create_dataset) export(create_distribution) export(create_file) +export(dataset_is_valid_for_status) export(get_api_key) export(get_dataset) export(get_keywords) diff --git a/README.Rmd b/README.Rmd index 00f0fb4..bdb4e31 100644 --- a/README.Rmd +++ b/README.Rmd @@ -13,7 +13,7 @@ knitr::opts_chunk$set( ) ``` -# 📦 zhapir zhapir Hex-Sticker +# 📦 zhapir zhapir Hex-Sticker @@ -25,7 +25,6 @@ knitr::opts_chunk$set( Damit können Inhalte für [zh.ch/opendata](https://zh.ch/opendata) und [opendata.swiss](https://opendata.swiss) effizient gepflegt werden. - ## 🚀 Installation Das Paket wird über GitHub installiert: @@ -87,6 +86,7 @@ ds <- zhapir::create_dataset( contact_email = "team@example.org", theme_ids = c("Verkehr"), periodicity_id = "Jährlich", + start_date = "2020-01-01", # 🟢 Startdatum ist für Veröffentlichung erforderlich use_dev = FALSE ) ``` @@ -96,6 +96,8 @@ Eine Publikation ist nur über die grafische Oberfläche möglich und erfolgt im ℹ️ Es ist nicht notwendig das Ergebnis der Funktionen (z.B. `zhapir::create_distribution()`) per `<-` zuzuweisen. Wir nutzen dies hier, um mit der ID eines Datensatzes oder einer Distribution weiterzuarbeiten. +🟢 Nach dem Erstellen oder Aktualisieren prüft `zhapir` automatisch, ob der Datensatz valid für den nächsten Status ist (z. B. ob Pflichtfelder wie `start_date` oder `keywords` gesetzt sind). Fehlende Felder werden im CLI mit entsprechenden Hinweisen ausgegeben. + ### Distribution hinzufügen ```{r eval=FALSE} @@ -110,10 +112,18 @@ dist <- zhapir::create_distribution( file_path = tmpfile, ogd_flag = TRUE, zh_web_flag = TRUE, + license_id = 1, # 🟢 Lizenz-ID (siehe unten) use_dev = FALSE ) ``` +🟢 Lizenztypen (license_id) + +| ID | Bedeutung | Entspricht | +|-------------|---------------------------------------------|--------------| +| 1 | kommerzielle & nicht-kommerzielle Nutzung **mit Quellenangabe** | ≈ CC BY 4.0 | +| 2 | kommerzielle & nicht-kommerzielle Nutzung **ohne Quellenangabe** | ≈ CC0 1.0 Public Domain | + 👉 **Kniff:** Über `update_distribution()` kannst du auch **Parameter auf Dataset-Ebene** anpassen, ohne separat `update_dataset()` aufzurufen – z. B. `end_date` oder `modified_next`: ```{r eval=FALSE} diff --git a/README.md b/README.md index 9eee1ad..8534a4c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -# 📦 zhapir zhapir Hex-Sticker +# 📦 zhapir zhapir Hex-Sticker @@ -83,6 +83,7 @@ ds <- zhapir::create_dataset( contact_email = "team@example.org", theme_ids = c("Verkehr"), periodicity_id = "Jährlich", + start_date = "2020-01-01", # 🟢 Startdatum ist für Veröffentlichung erforderlich use_dev = FALSE ) ``` @@ -98,6 +99,11 @@ aber vollständig über die API/R erledigen. hier, um mit der ID eines Datensatzes oder einer Distribution weiterzuarbeiten. +🟢 Nach dem Erstellen oder Aktualisieren prüft `zhapir` automatisch, ob +der Datensatz valid für den nächsten Status ist (z. B. ob Pflichtfelder +wie `start_date` oder `keywords` gesetzt sind). Fehlende Felder werden +im CLI mit entsprechenden Hinweisen ausgegeben. + ### Distribution hinzufügen ``` r @@ -112,10 +118,18 @@ dist <- zhapir::create_distribution( file_path = tmpfile, ogd_flag = TRUE, zh_web_flag = TRUE, + license_id = 1, # 🟢 Lizenz-ID (siehe unten) use_dev = FALSE ) ``` +🟢 Lizenztypen (license_id) + +| ID | Bedeutung | Entspricht | +|----|----|----| +| 1 | kommerzielle & nicht-kommerzielle Nutzung **mit Quellenangabe** | ≈ CC BY 4.0 | +| 2 | kommerzielle & nicht-kommerzielle Nutzung **ohne Quellenangabe** | ≈ CC0 1.0 Public Domain | + 👉 **Kniff:** Über `update_distribution()` kannst du auch **Parameter auf Dataset-Ebene** anpassen, ohne separat `update_dataset()` aufzurufen – z. B. `end_date` oder `modified_next`: diff --git a/man/create_dataset.Rd b/man/create_dataset.Rd index 56fc3fb..5985b88 100644 --- a/man/create_dataset.Rd +++ b/man/create_dataset.Rd @@ -36,7 +36,7 @@ create_dataset( \item{landing_page}{Optional landing page URL} -\item{start_date}{Optional ISO datetime string or POSIXct} +\item{start_date}{ISO datetime string or POSIXct} \item{end_date}{Optional ISO datetime string or POSIXct} diff --git a/man/create_distribution.Rd b/man/create_distribution.Rd index 0f1e6c1..f8526f1 100644 --- a/man/create_distribution.Rd +++ b/man/create_distribution.Rd @@ -46,7 +46,8 @@ create_distribution( \item{status_id}{Optional character; status ID (will be set via follow-up PATCH).} -\item{license_id}{Optional integer; license ID.} +\item{license_id}{integer; license ID. +Use \code{1} = "CC BY 4.0 (Attribution required)" or \code{2} = "CC0 (No attribution required)".} \item{file_format_id}{Optional file format ID.} diff --git a/man/dataset_is_valid_for_status.Rd b/man/dataset_is_valid_for_status.Rd new file mode 100644 index 0000000..e0e0d8c --- /dev/null +++ b/man/dataset_is_valid_for_status.Rd @@ -0,0 +1,31 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/helpers.R +\name{dataset_is_valid_for_status} +\alias{dataset_is_valid_for_status} +\title{Check if a dataset is valid for the next status (generic handling)} +\usage{ +dataset_is_valid_for_status( + id, + api_key = NULL, + use_dev = TRUE, + verbosity = 0, + fail_on_invalid = TRUE +) +} +\arguments{ +\item{id}{integer; dataset ID} + +\item{api_key}{API key (optional; falls back to env var)} + +\item{use_dev}{logical; use dev environment} + +\item{verbosity}{integer; passed to httr2::req_perform()} + +\item{fail_on_invalid}{logical; abort if server says not valid (default TRUE)} +} +\value{ +Invisibly returns parsed response list (type, errors, is_valid, can_delete, next_status) +} +\description{ +Check if a dataset is valid for the next status (generic handling) +} diff --git a/man/update_distribution.Rd b/man/update_distribution.Rd index 5d45b3c..26fc623 100644 --- a/man/update_distribution.Rd +++ b/man/update_distribution.Rd @@ -47,7 +47,8 @@ update_distribution( \item{status_id}{Optional status ID (applied via PATCH after update).} -\item{license_id}{Optional license ID.} +\item{license_id}{Optional license ID. +Use \code{1} = "CC BY 4.0 (Attribution required)" or \code{2} = "CC0 (No attribution required)".} \item{file_format_id}{Optional file format ID.} From 79e2faae900e6dbea67f177c88fc04b72fe3722e Mon Sep 17 00:00:00 2001 From: philbosch Date: Mon, 27 Oct 2025 14:38:42 +0000 Subject: [PATCH 2/5] feat: add status check functionality --- R/create_dataset.R | 21 ++++++++++++-- R/helpers.R | 71 ++++++++++++++++++++++++++++++++++++++++++++++ R/update_dataset.R | 20 ++++++++++++- 3 files changed, 109 insertions(+), 3 deletions(-) diff --git a/R/create_dataset.R b/R/create_dataset.R index 4627c49..50636ab 100644 --- a/R/create_dataset.R +++ b/R/create_dataset.R @@ -5,7 +5,7 @@ #' @param description Optional description string #' @param contact_email Optional contact email #' @param landing_page Optional landing page URL -#' @param start_date Optional ISO datetime string or POSIXct +#' @param start_date ISO datetime string or POSIXct #' @param end_date Optional ISO datetime string or POSIXct #' @param modified_next Optional ISO datetime string or POSIXct #' @param keyword_ids Optional character vector @@ -94,7 +94,24 @@ create_dataset <- function( # Dispatch create method if (!preview) { - create(ds, api_key, use_dev, verbosity = verbosity) + + # collect response + resp <- create(ds, api_key, use_dev, verbosity = verbosity) + + # extract ID from response + id <- resp$id + + # ---- run post-create validation check ---- + dataset_is_valid_for_status( + id, + api_key = api_key, + use_dev = use_dev, + verbosity = verbosity, + fail_on_invalid = FALSE + ) + + invisible(resp) + } else { return(ds) } diff --git a/R/helpers.R b/R/helpers.R index 7b73d6d..864bfe2 100644 --- a/R/helpers.R +++ b/R/helpers.R @@ -162,3 +162,74 @@ to_list <- function(vec_var) { } } + + +#' Check if a dataset is valid for the next status (generic handling) +#' +#' @param id integer; dataset ID +#' @param api_key API key (optional; falls back to env var) +#' @param use_dev logical; use dev environment +#' @param verbosity integer; passed to httr2::req_perform() +#' @param fail_on_invalid logical; abort if server says not valid (default TRUE) +#' @return Invisibly returns parsed response list (type, errors, is_valid, can_delete, next_status) +#' @export +dataset_is_valid_for_status <- function( + id, + api_key = NULL, + use_dev = TRUE, + verbosity = 0, + fail_on_invalid = TRUE +) { + endpoint <- sprintf("/api/v1/datasets/%s/is-valid-for-status", as.integer(id)) + + api_key <- get_api_key(api_key) + + + resp <- api_request( + method = "GET", + endpoint = endpoint, + object = NULL, # kein Payload + object_label = "Dataset Validation", + api_key = api_key, + verbosity = verbosity, + use_dev = use_dev + ) + + is_valid <- isTRUE(resp$is_valid) + errs <- resp$errors %||% list() + + if (is_valid) { + cli::cli_alert_success("Dataset ID {.val {id}} ist {cli::col_green('valid')} für den nächsten Status.") + } else { + cli::cli_alert_warning("Dataset ID {.val {id}} ist {cli::col_yellow('nicht valid')} für den nächsten Status. Das Dataset wurde angelegt, kann aber so nicht veröffentlicht werden.") + + if (length(errs) > 0) { + cli::cli_h2("Fehlerdetails:") + for (e in errs) { + code <- e$code %||% "" + attr <- e$attr %||% "" + det <- e$detail %||% "" + + # Formatting + if (nzchar(attr) && nzchar(code)) { + cli::cli_bullets(c("x" = "{det} [{cli::col_silver(code)} @ {cli::col_cyan(attr)}]")) + } else if (nzchar(attr)) { + cli::cli_bullets(c("x" = "{det} [@ {cli::col_cyan(attr)}]")) + } else if (nzchar(code)) { + cli::cli_bullets(c("x" = "{det} [{cli::col_silver(code)}]")) + } else { + cli::cli_bullets(c("x" = "{det}")) + } + } + } else { + cli::cli_bullets(c("x" = "Server lieferte keine Fehlerdetails.")) + } + + if (isTRUE(fail_on_invalid)) { + cli::cli_abort("Servervalidierung fehlgeschlagen – Statuswechsel nicht möglich.") + } + } + + invisible(resp) +} + diff --git a/R/update_dataset.R b/R/update_dataset.R index c0ec432..68405f5 100644 --- a/R/update_dataset.R +++ b/R/update_dataset.R @@ -94,7 +94,25 @@ update_dataset <- function( # Dispatch the update method if (!preview) { - update(ds, api_key, use_dev, verbosity = verbosity) + + # collect response + resp <- update(ds, api_key, use_dev, verbosity = verbosity) + + # extract ID from response + id <- resp$id + + # ---- run post-create validation check ---- + dataset_is_valid_for_status( + id, + api_key = api_key, + use_dev = use_dev, + verbosity = verbosity, + fail_on_invalid = FALSE + ) + + invisible(resp) + + } else { return(ds) } From 60ce135d619388f3ca8fdaab12f77e21b09e8d26 Mon Sep 17 00:00:00 2001 From: philbosch Date: Mon, 27 Oct 2025 14:39:07 +0000 Subject: [PATCH 3/5] doco: add explanation to license argument --- R/create_distribution.R | 3 ++- R/update_distribution.R | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/R/create_distribution.R b/R/create_distribution.R index 66f9339..f031c28 100644 --- a/R/create_distribution.R +++ b/R/create_distribution.R @@ -9,7 +9,8 @@ #' @param access_url Optional URL to access the distribution (must start with http:// or https://). #' @param byte_size Optional file size in bytes (must be a positive number). #' @param status_id Optional character; status ID (will be set via follow-up PATCH). -#' @param license_id Optional integer; license ID. +#' @param license_id integer; license ID. +#' Use `1` = "CC BY 4.0 (Attribution required)" or `2` = "CC0 (No attribution required)". #' @param file_format_id Optional file format ID. #' @param periodicity_id Optional character periodicity ID. #' @param file_path Optional local file path; if provided, the file will be uploaded and linked. diff --git a/R/update_distribution.R b/R/update_distribution.R index 4556638..e62b8d4 100644 --- a/R/update_distribution.R +++ b/R/update_distribution.R @@ -11,6 +11,7 @@ #' @param byte_size Optional file size in bytes (must be a positive number). #' @param status_id Optional status ID (applied via PATCH after update). #' @param license_id Optional license ID. +#' Use `1` = "CC BY 4.0 (Attribution required)" or `2` = "CC0 (No attribution required)". #' @param file_format_id Optional file format ID. #' @param periodicity_id Optional update frequency ID. #' @param file_path Optional local file path; if provided, the file will be uploaded and linked. From 88dd4da987ae40762a2f5988e2ae297f860c6695 Mon Sep 17 00:00:00 2001 From: philbosch Date: Mon, 27 Oct 2025 14:39:24 +0000 Subject: [PATCH 4/5] feat: add E2E tests for status check --- tests/testthat/test-e2e-user-flows.R | 161 +++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) diff --git a/tests/testthat/test-e2e-user-flows.R b/tests/testthat/test-e2e-user-flows.R index c852150..c231805 100644 --- a/tests/testthat/test-e2e-user-flows.R +++ b/tests/testthat/test-e2e-user-flows.R @@ -112,3 +112,164 @@ test_that("E2E: update distribution, bump dataset end_date, and advance status w ) expect_true(is.list(res_upd)) }) + + +# --- E2E: Status-Validierung nach Create/Update --- + +test_that("E2E: dataset ohne start_date ist nicht valid für nächsten Status", { + + skip_if_not_e2e() + + + # Minimaler Datensatz OHNE start_date (bewusst invalid für Publish) + ds <- create_dataset( + title = paste0("E2E DS missing start_date ", format(Sys.time(), "%Y-%m-%d %H:%M:%S")), + organisation_id = 14, + description = "Dataset ohne start_date für Negativtest", + contact_email = "team@example.org", + keyword_ids = c("abfall"), + theme_ids = c("Energie"), + periodicity_id = "Jährlich" + # start_date absichtlich weggelassen + ) + ds_id <- ds$id + + # expect that id is a positive number + expect_true(ds_id > 0) + + # expect the CLI message + expect_message( + { + resp_invalid <- dataset_is_valid_for_status( + id = ds_id, + use_dev = TRUE, + verbosity = 0, + fail_on_invalid = FALSE + ) + }, + regexp = "nicht valid", # German snippet is stable enough + perl = TRUE + ) + + # also assert programmatically + testthat::expect_false(isTRUE(resp_invalid$is_valid)) + +}) + + +test_that("E2E: dataset wird valid nach Setzen von start_date und Anlegen einer gültigen Distribution", { + skip_if_not_e2e() + + # Create dataset WITH start_date + ds <- create_dataset( + title = paste0("E2E DS valid path ", format(Sys.time(), "%Y-%m-%d %H:%M:%S")), + organisation_id = 14, + description = "Dataset mit start_date; Distribution wird ergänzt", + contact_email = "team@example.org", + start_date = format(Sys.Date() - 365, "%Y-%m-%d"), + keyword_ids = c("abfall"), + theme_ids = c("Energie"), + periodicity_id = "Jährlich" + ) + ds_id <- ds$id + expect_true(ds_id > 0) + + # Prepare a small CSV for upload + tf <- tempfile(fileext = ".csv") + on.exit(unlink(tf, force = TRUE), add = TRUE) + utils::write.csv(data.frame(a = 1:3), tf, row.names = FALSE) + + # Create a valid distribution + dist <- create_distribution( + title = paste0("E2E Dist valid for DS ", format(Sys.time(), "%H:%M:%S")), + dataset_id = ds_id, + file_path = tf, + license_id = 1, + file_format_id = "CSV", + status_id = 1 + ) + expect_true(is.list(dist)) + expect_true(dist$id > 0) + + # Now the status check should be valid + expect_message( + { + resp_ok <- dataset_is_valid_for_status( + id = ds_id, + use_dev = TRUE, + verbosity = 0, + fail_on_invalid = FALSE + ) + }, + regexp = "ist .*valid", + perl = TRUE + ) + expect_true(isTRUE(resp_ok$is_valid)) + # and definitely not "nicht valid" + msg_ok <- testthat::capture_messages( + dataset_is_valid_for_status( + id = ds_id, + use_dev = TRUE, + verbosity = 0, + fail_on_invalid = FALSE + ) + ) + expect_false(grepl("nicht valid", msg_ok, perl = TRUE)) +}) + + +test_that("E2E: update_dataset setzt start_date nachträglich; mit Distribution wird Dataset valid", { + skip_if_not_e2e() + + # Start invalid (no start_date) + ds <- create_dataset( + title = paste0("E2E DS to fix via update ", format(Sys.time(), "%Y-%m-%d %H:%M:%S")), + organisation_id = 14, + description = "Wird via update_dataset repariert", + contact_email = "team@example.org", + keyword_ids = c("abfall"), + theme_ids = c("Energie"), + periodicity_id = "Jährlich" + # start_date intentionally omitted + ) + ds_id <- ds$id + expect_true(ds_id > 0) + + # Fix: set start_date + upd <- update_dataset( + id = ds_id, + start_date = format(Sys.Date() - 30, "%Y-%m-%d") + ) + expect_true(is.list(upd)) + + # Add a valid distribution + tf <- tempfile(fileext = ".csv") + on.exit(unlink(tf, force = TRUE), add = TRUE) + utils::write.csv(data.frame(b = 4:6), tf, row.names = FALSE) + + dist <- create_distribution( + title = paste0("E2E Dist after update ", format(Sys.time(), "%H:%M:%S")), + dataset_id = ds_id, + file_path = tf, + license_id = 1, + file_format_id = "CSV", + status_id = 1 + ) + expect_true(is.list(dist)) + expect_true(dist$id > 0) + + # Now should be valid + expect_message( + { + resp_ok2 <- dataset_is_valid_for_status( + id = ds_id, + use_dev = TRUE, + verbosity = 0, + fail_on_invalid = FALSE + ) + }, + regexp = "ist .*valid", + perl = TRUE + ) + expect_true(isTRUE(resp_ok2$is_valid)) +}) From 542300bb7bb233b2eb8848c30bb77022903bbdca Mon Sep 17 00:00:00 2001 From: philbosch Date: Mon, 27 Oct 2025 14:54:21 +0000 Subject: [PATCH 5/5] fix: remove non-ASCII character to silence warning --- R/helpers.R | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/R/helpers.R b/R/helpers.R index 864bfe2..14f767d 100644 --- a/R/helpers.R +++ b/R/helpers.R @@ -71,7 +71,7 @@ object_to_payload <- function(object) { fmt_date <- function(d) format(d, "%Y-%m-%d") # 2. Transform properties: - # - POSIXct/Date → YYYY-MM-DD + # - POSIXct/Date -> YYYY-MM-DD p <- purrr::map(p, function(x) { if (inherits(x, c("POSIXct", "Date"))) { # if no date at all or only NA, emit a single NA_character_ @@ -155,7 +155,7 @@ to_date <- function(x) { to_list <- function(vec_var) { if (!inherits(vec_var, "S7_missing")) { - # c(42) → list(42); c(1,2,3) → list(1,2,3) + # c(42) -> list(42); c(1,2,3) -> list(1,2,3) as.list(vec_var) } else { S7::class_missing @@ -199,9 +199,9 @@ dataset_is_valid_for_status <- function( errs <- resp$errors %||% list() if (is_valid) { - cli::cli_alert_success("Dataset ID {.val {id}} ist {cli::col_green('valid')} für den nächsten Status.") + cli::cli_alert_success("Dataset ID {.val {id}} ist {cli::col_green('valid')} fuer den naechsten Status.") } else { - cli::cli_alert_warning("Dataset ID {.val {id}} ist {cli::col_yellow('nicht valid')} für den nächsten Status. Das Dataset wurde angelegt, kann aber so nicht veröffentlicht werden.") + cli::cli_alert_warning("Dataset ID {.val {id}} ist {cli::col_yellow('nicht valid')} fuer den naechsten Status. Das Dataset wurde angelegt, kann aber so nicht veroeffentlicht werden.") if (length(errs) > 0) { cli::cli_h2("Fehlerdetails:") @@ -226,7 +226,7 @@ dataset_is_valid_for_status <- function( } if (isTRUE(fail_on_invalid)) { - cli::cli_abort("Servervalidierung fehlgeschlagen – Statuswechsel nicht möglich.") + cli::cli_abort("Servervalidierung fehlgeschlagen - Statuswechsel nicht moeglich.") } }