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
34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,40 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/spec/v2.0.0.ht

---

## [2.13.0] — 2026-07-07

Neues Qualitäts-Scoring nach der EU-MQA-Methodik (Phase 1). Siehe `docs/MQA-KONZEPT.md`.

### ✨ Changed — Qualitätsprüfung auf MQA umgestellt
- Die Qualitätsprüfung folgt jetzt dem **Metadata Quality Assessment (MQA)** von data.europa.eu:
**5 FAIR-Dimensionen** (Auffindbarkeit, Zugänglichkeit, Interoperabilität, Wiederverwendbarkeit,
Kontext), **405 Punkte**, **4 Bewertungsstufen** (Ausgezeichnet / Gut / Ausreichend / Mangelhaft).
- Die Metriken stehen in der neuen Single-Source-of-Truth `config/mqa-metrics.php`.
- Die Meta-Box zeigt die Bewertung pro Dimension inkl. Punkte und Metrik-Status.
- Das JSON-LD/REST-Feld `odw:qualityScore` weist nun MQA-Punkte (von 405), die Bewertungsstufe und
die Dimensions-Aufschlüsselung aus (verweist auf die MQA-Methodik-URL).

### 🔧 Phasenumfang
- **Phase 1 (dieses Release):** Alle „gesetzt?"-Metriken werden bewertet (offline).
- Vokabular-, URL-Erreichbarkeits- und DCAT-AP-SHACL-Metriken sind als **„nicht bewertet"** verdrahtet
und werden aus dem bewertbaren Maximum herausgerechnet; die Bewertungsstufen werden **proportional**
auf das bewertbare Maximum skaliert. Diese Metriken folgen in Phase 2 (Vokabular) und Phase 3
(Netzwerk/SHACL).

### ♻️ Abwärtskompatibilität
- Die Publish-Validierung (`config/dcat-ap-fields.php`) bleibt unverändert.
- Der abgeleitete 0–100-Score und das Level bleiben in `_odw_quality_score` / `_odw_quality_level`
erhalten (Admin-Listenspalte, Sortierung). Das vollständige MQA-Ergebnis liegt in `_odw_mqa`.

### 🌍 i18n
- 33 neue MQA-UI-Strings in `.po`/`.pot` ergänzt und `en_US.mo` neu kompiliert.

### ✅ Tests
- Neue Tests für Bewertungsstufen (proportionale Schwellen), Metrik-Summen je Dimension (405 gesamt)
und die MQA-Persistierung/JSON-LD-Ausgabe.

---

## [2.12.0] — 2026-06-28

UX-Feinschliff und Vervollständigung der optionalen DCAT-AP-3.0-Felder.
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.12.0-brightgreen?style=flat-square)
![Version](https://img.shields.io/badge/Version-2.13.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
17 changes: 17 additions & 0 deletions assets/css/admin.css
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,23 @@
.odw-quality-fail .odw-quality-col-status { color: var(--odw-color-quality-low-dot); }
.odw-quality-fail { color: var(--odw-color-text-muted); }

/* MQA: not-assessed metrics (network / vocabulary / SHACL — pending phases) */
.odw-quality-notassessed { color: var(--odw-color-text-muted); }
.odw-quality-notassessed .odw-quality-col-status { color: var(--odw-color-border); }

/* MQA: per-dimension score in the section header */
.odw-quality-dim-score {
float: right;
font-weight: 400;
color: var(--odw-color-text-muted);
}

.odw-quality-of-max {
font-weight: 400;
color: var(--odw-color-text-muted);
font-size: 12px;
}

/* Footer */
.odw-quality-footer {
margin-top: 12px;
Expand Down
261 changes: 261 additions & 0 deletions config/mqa-metrics.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
<?php
/**
* MQA-Metrik-Definitionen — Single Source of Truth für das Qualitäts-Scoring
*
* Bildet die Metadata Quality Assessment (MQA) Methodik von data.europa.eu ab:
* 5 FAIR-Dimensionen, 405 Punkte, 4 Bewertungsstufen. Siehe docs/MQA-KONZEPT.md.
*
* Format pro Eintrag:
* key — interner Schlüssel
* dimension — findability | accessibility | interoperability | reusability | contextuality
* dcat_prop — DCAT-AP Property (Dokumentation/Anzeige)
* label — benutzerfreundliches Label (Qualitätsbericht)
* points — Gewicht laut MQA
* type — Prüfungsart:
* 'present' — Eigenschaft gesetzt? (offline, Phase 1)
* 'vocab' — Wert aus kontrolliertem Vokabular? (offline, Phase 2)
* 'reachable' — URL per HTTP HEAD erreichbar? (Netzwerk, Phase 3)
* 'shacl' — DCAT-AP-SHACL-konform? (extern, Phase 3)
* check — Schlüssel für ODW_Quality::check_metric() (nur bei type='present')
*
* Nur Metriken vom Typ 'present' werden aktuell bewertet; 'vocab'/'reachable'/'shacl'
* liefern „nicht bewertet" und werden aus dem bewertbaren Maximum herausgerechnet,
* bis die jeweilige Phase implementiert ist.
*
* @package OpenDataWizard
*/

if ( ! defined( 'ABSPATH' ) ) {
exit;
}

return array(

// -------------------------------------------------------------------------
// Auffindbarkeit (max. 100)
// -------------------------------------------------------------------------
array(
'key' => 'keyword',
'dimension' => 'findability',
'dcat_prop' => 'dcat:keyword',
'label' => __( 'Schlüsselwörter (dcat:keyword)', 'open-data-wizard' ),
'points' => 30,
'type' => 'present',
'check' => 'keyword',
),
array(
'key' => 'theme',
'dimension' => 'findability',
'dcat_prop' => 'dcat:theme',
'label' => __( 'Kategorien (dcat:theme)', 'open-data-wizard' ),
'points' => 30,
'type' => 'present',
'check' => 'theme',
),
array(
'key' => 'spatial',
'dimension' => 'findability',
'dcat_prop' => 'dct:spatial',
'label' => __( 'Räumliche Abdeckung (dct:spatial)', 'open-data-wizard' ),
'points' => 20,
'type' => 'present',
'check' => 'spatial',
),
array(
'key' => 'temporal',
'dimension' => 'findability',
'dcat_prop' => 'dct:temporal',
'label' => __( 'Zeitliche Abdeckung (dct:temporal)', 'open-data-wizard' ),
'points' => 20,
'type' => 'present',
'check' => 'temporal',
),

// -------------------------------------------------------------------------
// Zugänglichkeit (max. 100)
// -------------------------------------------------------------------------
array(
'key' => 'access_url_reachable',
'dimension' => 'accessibility',
'dcat_prop' => 'dcat:accessURL',
'label' => __( 'AccessURL erreichbar (dcat:accessURL)', 'open-data-wizard' ),
'points' => 50,
'type' => 'reachable',
'check' => 'access_url',
),
array(
'key' => 'download_url',
'dimension' => 'accessibility',
'dcat_prop' => 'dcat:downloadURL',
'label' => __( 'DownloadURL gesetzt (dcat:downloadURL)', 'open-data-wizard' ),
'points' => 20,
'type' => 'present',
'check' => 'download_url',
),
array(
'key' => 'download_url_reachable',
'dimension' => 'accessibility',
'dcat_prop' => 'dcat:downloadURL',
'label' => __( 'DownloadURL erreichbar (dcat:downloadURL)', 'open-data-wizard' ),
'points' => 30,
'type' => 'reachable',
'check' => 'download_url',
),

// -------------------------------------------------------------------------
// Interoperabilität (max. 110)
// -------------------------------------------------------------------------
array(
'key' => 'format',
'dimension' => 'interoperability',
'dcat_prop' => 'dct:format',
'label' => __( 'Format gesetzt (dct:format)', 'open-data-wizard' ),
'points' => 20,
'type' => 'present',
'check' => 'format',
),
array(
'key' => 'media_type',
'dimension' => 'interoperability',
'dcat_prop' => 'dcat:mediaType',
'label' => __( 'Media-Type gesetzt (dcat:mediaType)', 'open-data-wizard' ),
'points' => 10,
'type' => 'present',
'check' => 'media_type',
),
array(
'key' => 'format_vocab',
'dimension' => 'interoperability',
'dcat_prop' => 'dct:format',
'label' => __( 'Format/Media-Type aus Vokabular', 'open-data-wizard' ),
'points' => 10,
'type' => 'vocab',
'check' => 'format_vocab',
),
array(
'key' => 'format_nonproprietary',
'dimension' => 'interoperability',
'dcat_prop' => 'dct:format',
'label' => __( 'Nicht-proprietäres Format', 'open-data-wizard' ),
'points' => 20,
'type' => 'vocab',
'check' => 'format_nonproprietary',
),
array(
'key' => 'format_machine_readable',
'dimension' => 'interoperability',
'dcat_prop' => 'dct:format',
'label' => __( 'Maschinenlesbares Format', 'open-data-wizard' ),
'points' => 20,
'type' => 'vocab',
'check' => 'format_machine_readable',
),
array(
'key' => 'dcat_ap_compliance',
'dimension' => 'interoperability',
'dcat_prop' => 'dcat-ap',
'label' => __( 'DCAT-AP-Konformität (SHACL)', 'open-data-wizard' ),
'points' => 30,
'type' => 'shacl',
'check' => 'shacl',
),

// -------------------------------------------------------------------------
// Wiederverwendbarkeit (max. 75)
// -------------------------------------------------------------------------
array(
'key' => 'license',
'dimension' => 'reusability',
'dcat_prop' => 'dct:license',
'label' => __( 'Lizenz gesetzt (dct:license)', 'open-data-wizard' ),
'points' => 20,
'type' => 'present',
'check' => 'license',
),
array(
'key' => 'license_vocab',
'dimension' => 'reusability',
'dcat_prop' => 'dct:license',
'label' => __( 'Lizenz aus Vokabular', 'open-data-wizard' ),
'points' => 10,
'type' => 'vocab',
'check' => 'license_vocab',
),
array(
'key' => 'access_rights',
'dimension' => 'reusability',
'dcat_prop' => 'dct:accessRights',
'label' => __( 'Zugriffsrechte gesetzt (dct:accessRights)', 'open-data-wizard' ),
'points' => 10,
'type' => 'present',
'check' => 'access_rights',
),
array(
'key' => 'access_rights_vocab',
'dimension' => 'reusability',
'dcat_prop' => 'dct:accessRights',
'label' => __( 'Zugriffsrechte aus Vokabular', 'open-data-wizard' ),
'points' => 5,
'type' => 'vocab',
'check' => 'access_rights_vocab',
),
array(
'key' => 'contact_point',
'dimension' => 'reusability',
'dcat_prop' => 'dcat:contactPoint',
'label' => __( 'Kontaktpunkt (dcat:contactPoint)', 'open-data-wizard' ),
'points' => 20,
'type' => 'present',
'check' => 'contact_point',
),
array(
'key' => 'publisher',
'dimension' => 'reusability',
'dcat_prop' => 'dct:publisher',
'label' => __( 'Herausgeber (dct:publisher)', 'open-data-wizard' ),
'points' => 10,
'type' => 'present',
'check' => 'publisher',
),

// -------------------------------------------------------------------------
// Kontext (max. 20)
// -------------------------------------------------------------------------
array(
'key' => 'rights',
'dimension' => 'contextuality',
'dcat_prop' => 'dct:rights',
'label' => __( 'Rechte (dct:rights)', 'open-data-wizard' ),
'points' => 5,
'type' => 'present',
'check' => 'rights',
),
array(
'key' => 'byte_size',
'dimension' => 'contextuality',
'dcat_prop' => 'dcat:byteSize',
'label' => __( 'Dateigröße (dcat:byteSize)', 'open-data-wizard' ),
'points' => 5,
'type' => 'present',
'check' => 'byte_size',
),
array(
'key' => 'issued',
'dimension' => 'contextuality',
'dcat_prop' => 'dct:issued',
'label' => __( 'Ausstellungsdatum (dct:issued)', 'open-data-wizard' ),
'points' => 5,
'type' => 'present',
'check' => 'issued',
),
array(
'key' => 'modified',
'dimension' => 'contextuality',
'dcat_prop' => 'dct:modified',
'label' => __( 'Änderungsdatum (dct:modified)', 'open-data-wizard' ),
'points' => 5,
'type' => 'present',
'check' => 'modified',
),
);
// Summe: 100 + 100 + 110 + 75 + 20 = 405.
2 changes: 0 additions & 2 deletions config/phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,3 @@ parameters:
# PHPDoc type strictness causes false positives with WordPress types
- identifier: function.impossibleType
message: '/Call to function is_wp_error.*will always evaluate/'

- identifier: identical.alwaysTrue
3 changes: 2 additions & 1 deletion includes/class-admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,8 @@ public static function render_column( string $column, int $post_id ): void {
} else {
$level = $quality['level'];
$score = $quality['score'];
$label = ODW_Quality::get_level_label( $level );
$rating = (string) ( $quality['rating'] ?? '' );
$label = '' !== $rating ? ODW_Quality::get_rating_label( $rating ) : ODW_Quality::get_level_label( $level );
$title_attr = sprintf( '%s · %d/100 %s', $label, $score, __( 'Punkte', 'open-data-wizard' ) );
printf(
'<span class="odw-quality-badge odw-quality--%s" title="%s"><span class="odw-quality-dot" aria-hidden="true">●</span> %d</span>',
Expand Down
Loading
Loading