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/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/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/helpers.R b/R/helpers.R
index 7b73d6d..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,10 +155,81 @@ 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
}
}
+
+
+#' 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')} fuer den naechsten Status.")
+ } else {
+ 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:")
+ 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 moeglich.")
+ }
+ }
+
+ 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)
}
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.
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
@@ -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
@@ -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.}
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))
+})