diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ca78d3..31d1d2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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`. diff --git a/README.md b/README.md index b160337..18ca9bb 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/config/dct-format-list.php b/config/dct-format-list.php index 4907c6a..4358a9d 100644 --- a/config/dct-format-list.php +++ b/config/dct-format-list.php @@ -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 */ @@ -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, ), ); diff --git a/includes/class-fields.php b/includes/class-fields.php index 7af53e6..b594d72 100644 --- a/includes/class-fields.php +++ b/includes/class-fields.php @@ -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. * diff --git a/includes/class-quality.php b/includes/class-quality.php index a6aafdc..84dc18a 100644 --- a/includes/class-quality.php +++ b/includes/class-quality.php @@ -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; } /** @@ -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 @@ -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 ) ); diff --git a/languages/open-data-wizard-en_US.mo b/languages/open-data-wizard-en_US.mo index 0dd6d78..8dadac7 100644 Binary files a/languages/open-data-wizard-en_US.mo and b/languages/open-data-wizard-en_US.mo differ diff --git a/languages/open-data-wizard-en_US.po b/languages/open-data-wizard-en_US.po index 4e2f65c..e7cf5ab 100644 --- a/languages/open-data-wizard-en_US.po +++ b/languages/open-data-wizard-en_US.po @@ -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)." diff --git a/open-data-wizard.php b/open-data-wizard.php index e85fda3..f082152 100644 --- a/open-data-wizard.php +++ b/open-data-wizard.php @@ -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 @@ -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__ ); diff --git a/tests/test-fields.php b/tests/test-fields.php index 824be50..a573067 100644 --- a/tests/test-fields.php +++ b/tests/test-fields.php @@ -87,6 +87,32 @@ public function test_get_spatial_options_links_geonames(): void { $this->assertArrayHasKey( 'Bayern', $options ); } + /** + * Format metadata exposes the MQA machine-readable / non-proprietary flags. + */ + public function test_get_format_meta_exposes_mqa_flags(): void { + require_once ODW_PLUGIN_DIR . 'includes/class-fields.php'; + + $csv = ODW_Fields::get_format_meta( 'CSV' ); + $this->assertTrue( $csv['machine_readable'] ); + $this->assertTrue( $csv['non_proprietary'] ); + $this->assertNotSame( '', $csv['eu_uri'] ); + + // XLSX is machine-readable but proprietary. + $xlsx = ODW_Fields::get_format_meta( 'XLSX' ); + $this->assertTrue( $xlsx['machine_readable'] ); + $this->assertFalse( $xlsx['non_proprietary'] ); + + // "Sonstiges" is neither and has no EU vocabulary URI. + $other = ODW_Fields::get_format_meta( 'Sonstiges' ); + $this->assertFalse( $other['machine_readable'] ); + $this->assertFalse( $other['non_proprietary'] ); + $this->assertSame( '', $other['eu_uri'] ); + + // Unknown format returns an empty array. + $this->assertSame( array(), ODW_Fields::get_format_meta( 'NOPE' ) ); + } + /** * License descriptions provide plain-language text for the standard licenses. */