Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,31 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/spec/v2.0.0.ht

---

## [2.14.0] — 2026-07-07

MQA-Qualitäts-Scoring **Phase 2**: Vokabular-Prüfungen (offline). Siehe `docs/MQA-KONZEPT.md`.

### ✨ Added — Vokabular-Metriken bewertet
- Die MQA-Metriken vom Typ `vocab` werden jetzt bewertet (offline, ohne neue Felder):
- **Format/Media-Type aus Vokabular** (10) — Format ist Teil des kontrollierten EU-Vokabulars.
- **Nicht-proprietäres Format** (20) und **Maschinenlesbares Format** (20) — aus den neuen Flags
in `config/dct-format-list.php`.
- **Lizenz aus Vokabular** (10) — Lizenz-URI ist im bekannten Lizenz-Set (`config/licenses.txt`
bzw. Standardlizenzen).
- **Zugriffsrechte aus Vokabular** (5) — Wert stammt aus dem EU-Access-Right-Vokabular.
- Das bewertbare Maximum steigt damit von ~230 auf **~295 von 405 Punkten** (rein offline).
- `config/dct-format-list.php` um `machine_readable` und `non_proprietary` je Format erweitert
(orientiert an den EU-Vokabularen).

### 🔧 Verbleibend (Phase 3, opt-in)
- Nur noch **URL-Erreichbarkeit** (80) und **DCAT-AP-SHACL-Konformität** (30) sind „nicht bewertet".

### ✅ Tests
- Neuer Test für die Format-Flags (`get_format_meta`: CSV offen/maschinenlesbar, XLSX proprietär,
„Sonstiges" ohne Vokabular).

---

## [2.13.0] — 2026-07-07

Neues Qualitäts-Scoring nach der EU-MQA-Methodik (Phase 1). Siehe `docs/MQA-KONZEPT.md`.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
![PHP Version](https://img.shields.io/badge/PHP-%3E%3D%208.1-8892BF?style=flat-square&logo=php&logoColor=white)
![WordPress](https://img.shields.io/badge/WordPress-compatible-21759B?style=flat-square&logo=wordpress&logoColor=white)
![DCAT-AP](https://img.shields.io/badge/DCAT--AP-3.0-brightgreen?style=flat-square)
![Version](https://img.shields.io/badge/Version-2.13.0-brightgreen?style=flat-square)
![Version](https://img.shields.io/badge/Version-2.14.0-brightgreen?style=flat-square)
![PRs Welcome](https://img.shields.io/badge/PRs-willkommen-brightgreen?style=flat-square)

📖 [Dokumentation](DOCUMENTATION.md) · 📐 [Technische Spezifikation](TECHNICAL-SPEC.md) · 📝 [Changelog](CHANGELOG.md) · 🛡️ [Security](SECURITY.md) · ⚖️ [Lizenz](LICENSE)
Expand Down
118 changes: 80 additions & 38 deletions config/dct-format-list.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,15 @@
/**
* Dateiformatliste für DCAT-AP Distributionen
*
* Jeder Eintrag: Kurzbezeichnung => ['mime' => MIME-Typ, 'eu_uri' => EU-Publications-Office-Kürzel]
* Jeder Eintrag: Kurzbezeichnung => [
* 'mime' => MIME-Typ,
* 'eu_uri' => EU-Publications-Office-Kürzel (leer = kein EU-Standard-URI),
* 'machine_readable' => bool (MQA: maschinenlesbares Format),
* 'non_proprietary' => bool (MQA: nicht-proprietäres Format),
* ]
* eu_uri wird zu http://publications.europa.eu/resource/authority/file-type/{eu_uri} aufgelöst.
* Leere eu_uri bedeutet: kein EU-Standard-URI vorhanden → Kurzbezeichnung als Fallback.
* Die MQA-Flags orientieren sich an den EU-Vokabularen für maschinenlesbare bzw.
* nicht-proprietäre Formate (data.europa.eu) und werden für das Qualitäts-Scoring genutzt.
*
* @package OpenDataWizard
*/
Expand All @@ -15,75 +21,111 @@

return array(
'CSV' => array(
'mime' => 'text/csv',
'eu_uri' => 'CSV',
'mime' => 'text/csv',
'eu_uri' => 'CSV',
'machine_readable' => true,
'non_proprietary' => true,
),
'JSON' => array(
'mime' => 'application/json',
'eu_uri' => 'JSON',
'mime' => 'application/json',
'eu_uri' => 'JSON',
'machine_readable' => true,
'non_proprietary' => true,
),
'XLSX' => array(
'mime' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'eu_uri' => 'XLSX',
'mime' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'eu_uri' => 'XLSX',
'machine_readable' => true,
'non_proprietary' => false,
),
'ODS' => array(
'mime' => 'application/vnd.oasis.opendocument.spreadsheet',
'eu_uri' => 'ODS',
'mime' => 'application/vnd.oasis.opendocument.spreadsheet',
'eu_uri' => 'ODS',
'machine_readable' => true,
'non_proprietary' => true,
),
'PDF' => array(
'mime' => 'application/pdf',
'eu_uri' => 'PDF',
'mime' => 'application/pdf',
'eu_uri' => 'PDF',
'machine_readable' => false,
'non_proprietary' => true,
),
'XML' => array(
'mime' => 'application/xml',
'eu_uri' => 'XML',
'mime' => 'application/xml',
'eu_uri' => 'XML',
'machine_readable' => true,
'non_proprietary' => true,
),
'GeoJSON' => array(
'mime' => 'application/geo+json',
'eu_uri' => 'GEOJSON',
'mime' => 'application/geo+json',
'eu_uri' => 'GEOJSON',
'machine_readable' => true,
'non_proprietary' => true,
),
'GML' => array(
'mime' => 'application/gml+xml',
'eu_uri' => 'GML',
'mime' => 'application/gml+xml',
'eu_uri' => 'GML',
'machine_readable' => true,
'non_proprietary' => true,
),
'SHP' => array(
'mime' => 'application/x-esri-shape',
'eu_uri' => 'SHP',
'mime' => 'application/x-esri-shape',
'eu_uri' => 'SHP',
'machine_readable' => true,
'non_proprietary' => false,
),
'GPKG' => array(
'mime' => 'application/geopackage+sqlite3',
'eu_uri' => 'GPKG',
'mime' => 'application/geopackage+sqlite3',
'eu_uri' => 'GPKG',
'machine_readable' => true,
'non_proprietary' => true,
),
'KML' => array(
'mime' => 'application/vnd.google-earth.kml+xml',
'eu_uri' => 'KML',
'mime' => 'application/vnd.google-earth.kml+xml',
'eu_uri' => 'KML',
'machine_readable' => true,
'non_proprietary' => true,
),
'ZIP' => array(
'mime' => 'application/zip',
'eu_uri' => 'ZIP',
'mime' => 'application/zip',
'eu_uri' => 'ZIP',
'machine_readable' => false,
'non_proprietary' => true,
),
'TIFF' => array(
'mime' => 'image/tiff',
'eu_uri' => 'TIFF',
'mime' => 'image/tiff',
'eu_uri' => 'TIFF',
'machine_readable' => true,
'non_proprietary' => true,
),
'RDF' => array(
'mime' => 'application/rdf+xml',
'eu_uri' => 'RDF_XML',
'mime' => 'application/rdf+xml',
'eu_uri' => 'RDF_XML',
'machine_readable' => true,
'non_proprietary' => true,
),
'TURTLE' => array(
'mime' => 'text/turtle',
'eu_uri' => 'TURTLE',
'mime' => 'text/turtle',
'eu_uri' => 'TURTLE',
'machine_readable' => true,
'non_proprietary' => true,
),
'N-TRIPLES' => array(
'mime' => 'application/n-triples',
'eu_uri' => 'N_TRIPLES',
'mime' => 'application/n-triples',
'eu_uri' => 'N_TRIPLES',
'machine_readable' => true,
'non_proprietary' => true,
),
'JSON-LD' => array(
'mime' => 'application/ld+json',
'eu_uri' => 'JSON_LD',
'mime' => 'application/ld+json',
'eu_uri' => 'JSON_LD',
'machine_readable' => true,
'non_proprietary' => true,
),
'Sonstiges' => array(
'mime' => '',
'eu_uri' => '',
'mime' => '',
'eu_uri' => '',
'machine_readable' => false,
'non_proprietary' => false,
),
);
11 changes: 11 additions & 0 deletions includes/class-fields.php
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,17 @@ public static function get_format_mime( string $format ): string {
return $list[ $format ]['mime'] ?? $format;
}

/**
* Returns the config entry for a format label (mime, eu_uri and MQA flags).
*
* @param string $format Short format label (e.g. "CSV").
* @return array{mime?: string, eu_uri?: string, machine_readable?: bool, non_proprietary?: bool}
*/
public static function get_format_meta( string $format ): array {
$list = self::load_format_list();
return $list[ $format ] ?? array();
}

/**
* Maps a short format label to its EU Publications Office file-type URI.
*
Expand Down
99 changes: 94 additions & 5 deletions includes/class-quality.php
Original file line number Diff line number Diff line change
Expand Up @@ -190,12 +190,18 @@ public static function calculate( int $post_id ): array {
* @return bool|null true = erfüllt, false = nicht erfüllt, null = nicht bewertet (Vokabular/Netzwerk/SHACL).
*/
private static function evaluate_metric( array $metric, \WP_Post $post ): ?bool {
if ( 'present' !== ( $metric['type'] ?? '' ) ) {
// Vokabular-, Erreichbarkeits- und SHACL-Prüfungen folgen in späteren Phasen.
return null;
$type = (string) ( $metric['type'] ?? '' );

if ( 'present' === $type ) {
return self::check_metric( (string) $metric['check'], $post );
}

if ( 'vocab' === $type ) {
return self::check_vocab_metric( (string) $metric['check'], $post );
}

return self::check_metric( (string) $metric['check'], $post );
// Erreichbarkeits- und SHACL-Prüfungen folgen in Phase 3.
return null;
}

/**
Expand Down Expand Up @@ -272,6 +278,89 @@ private static function check_metric( string $check, \WP_Post $post ): bool {
return false;
}

/**
* „Stammt der Wert aus einem kontrollierten Vokabular?"-Prüfung (MQA Phase 2).
*
* @param string $check Check-Schlüssel aus der Metrik-Definition.
* @param \WP_Post $post Dataset post object.
* @return bool True wenn der Wert einem kontrollierten Vokabular entspricht.
*/
private static function check_vocab_metric( string $check, \WP_Post $post ): bool {
if ( ! class_exists( 'ODW_Fields' ) ) {
return false;
}

$id = $post->ID;

switch ( $check ) {
case 'format_vocab':
$format = (string) carbon_get_post_meta( $id, 'odw_format' );
if ( '' === $format ) {
return false;
}
return '' !== (string) ( ODW_Fields::get_format_meta( $format )['eu_uri'] ?? '' );

case 'format_nonproprietary':
$format = (string) carbon_get_post_meta( $id, 'odw_format' );
return true === ( ODW_Fields::get_format_meta( $format )['non_proprietary'] ?? false );

case 'format_machine_readable':
$format = (string) carbon_get_post_meta( $id, 'odw_format' );
return true === ( ODW_Fields::get_format_meta( $format )['machine_readable'] ?? false );

case 'license_vocab':
return self::license_in_vocab( self::effective_license( $id ) );

case 'access_rights_vocab':
$value = (string) carbon_get_post_meta( $id, 'odw_access_rights' );
if ( '' === $value ) {
return false;
}
foreach ( ODW_Fields::load_vocabulary( 'access-right' ) as $entry ) {
if ( $entry['value'] === $value ) {
return true;
}
}
return false;
}

return false;
}

/**
* Effektive Lizenz-URI (bei „sonstige" die eigene URI).
*
* @param int $id Post ID.
* @return string
*/
private static function effective_license( int $id ): string {
$lic = (string) carbon_get_post_meta( $id, 'odw_license' );
if ( 'sonstige' === $lic ) {
return (string) carbon_get_post_meta( $id, 'odw_license_custom' );
}
return $lic;
}

/**
* Prüft, ob eine Lizenz-URI in einem bekannten Lizenz-Vokabular enthalten ist.
*
* @param string $uri Lizenz-URI.
* @return bool
*/
private static function license_in_vocab( string $uri ): bool {
if ( '' === $uri || 'sonstige' === $uri ) {
return false;
}

$options = ODW_Fields::get_license_options();
if ( isset( $options[ $uri ] ) ) {
return true;
}

$extended = ODW_Fields::load_license_list();
return isset( $extended[ $uri ] );
}

/**
* Ermittelt die MQA-Bewertungsstufe aus erreichten/bewertbaren Punkten.
* Die MQA-Schwellen (351/221/121 von 405) werden proportional auf das
Expand Down Expand Up @@ -498,7 +587,7 @@ public static function render_meta_box( \WP_Post $post ): void {
echo esc_html(
sprintf(
/* translators: %d: number of points not yet assessed */
__( '%d Punkte werden derzeit nicht bewertet (URL-Erreichbarkeit, Vokabular- und DCAT-AP-SHACL-Prüfung folgen).', 'open-data-wizard' ),
__( '%d Punkte werden derzeit nicht bewertet (URL-Erreichbarkeit und DCAT-AP-SHACL-Prüfung folgen).', 'open-data-wizard' ),
$not_assessed
)
);
Expand Down
Binary file modified languages/open-data-wizard-en_US.mo
Binary file not shown.
3 changes: 3 additions & 0 deletions languages/open-data-wizard-en_US.po
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ msgstr " created and are then available for editing."
msgid "%1$d von %2$d Pflichtangaben ausgefüllt"
msgstr "%1$d of %2$d required fields completed"

msgid "%d Punkte werden derzeit nicht bewertet (URL-Erreichbarkeit und DCAT-AP-SHACL-Prüfung folgen)."
msgstr "%d points are currently not assessed (URL reachability and DCAT-AP SHACL checks are coming)."

msgid "%d Punkte werden derzeit nicht bewertet (URL-Erreichbarkeit, Vokabular- und DCAT-AP-SHACL-Prüfung folgen)."
msgstr "%d points are currently not assessed (URL reachability, vocabulary and DCAT-AP SHACL checks are coming)."

Expand Down
4 changes: 2 additions & 2 deletions open-data-wizard.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Plugin Name: Open Data Wizard
* Plugin URI: https://github.com/daimpad/OpenDataWizard
* Description: DCAT-AP 3.0 konforme Open Data Metadatenverwaltung für WordPress. Bereitstellung als maschinenlesbarer JSON-LD-Endpoint für offene Daten.
* Version: 2.13.0
* Version: 2.14.0
* Requires at least: 6.4
* Requires PHP: 8.1
* Author: nozilla
Expand All @@ -26,7 +26,7 @@
exit;
}

define( 'ODW_VERSION', '2.13.0' );
define( 'ODW_VERSION', '2.14.0' );
define( 'ODW_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
define( 'ODW_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
define( 'ODW_PLUGIN_FILE', __FILE__ );
Expand Down
Loading
Loading