Merge remote-tracking branch 'origin/development' into feature/remove-duplicate-memberships

This commit is contained in:
wolfez
2025-07-28 11:22:47 +02:00
15 changed files with 431 additions and 6 deletions

View File

@@ -1,5 +1,27 @@
# Changelog
## Unreleased
### Frontend
- Bump to TODO: link to release with scope of operation functionality
### Backend
- Add a service to manage organisation's their scope of operation [OP-3205]
- datamodel: Allow a location to be within multiple other locations [OP-3205]
- datafix: set scope of operation for worship administrative units [OP-3626]
- Clean addresses linked to both sites and contact points of mandatarissen and addresses that are in the wrong graph (admin unit graph but linked to a contact point in the worship graph) [DL-6701]
- datafix: cleanup of general memberships if specific ones exist [OP-3640]
### Deploy notes
```
drc pull scope-of-operation; drc up -d scope-of-operation
drc restart db resource dispatcher
drc pull frontend; drc up -d frontend
drc restart migrations; drc logs -ft --tail=200 migrations
```
The labels for new location resources created when setting the scope of operation for worship services are not necessarily alphabetically sorted. To correct this run the `correct-location-labels` project script using [mu-cli](https://github.com/mu-semtech/mu-cli) and restart the migrations service once more to execute the generated local migrations.
```
mu script project-scripts correct-location-labels
drc restart migrations; drc logs -ft --tail=200 migrations
```
## v1.34.0 (2025-07-11)
### Frontend

View File

@@ -53,7 +53,8 @@ defmodule Acl.UserGroups.Config do
"http://lblod.data.gift/vocabularies/organisatie/VoorwaardenBedienaarCriterium",
"http://lblod.data.gift/vocabularies/organisatie/BedienaarCriteriumBewijsstuk",
"http://lblod.data.gift/vocabularies/organisatie/EredienstBeroepen",
"http://lblod.data.gift/vocabularies/organisatie/Rechtsvormtype"
"http://lblod.data.gift/vocabularies/organisatie/Rechtsvormtype",
"http://www.w3.org/ns/prov#Location"
]
@org_type [
@@ -80,11 +81,10 @@ defmodule Acl.UserGroups.Config do
"http://data.lblod.info/vocabularies/contacthub/AgentInPositie",
"http://mu.semte.ch/vocabularies/ext/KboOrganisatie",
"http://www.w3.org/ns/org#Membership",
"http://www.w3.org/2006/time#ProperInterval"
"http://www.w3.org/2006/time#ProperInterval",
]
@shared_protected_type [
"http://www.w3.org/ns/prov#Location",
"http://xmlns.com/foaf/0.1/Image",
"http://www.semanticdesktop.org/ontologies/2007/03/22/nfo#FileDataObject"
]
@@ -117,7 +117,7 @@ defmodule Acl.UserGroups.Config do
?account <http://mu.semte.ch/vocabularies/ext/sessionRole> ?session_role.
FILTER( ?session_role = \"dashboard-user\" )
}"
}
}
end
def user_groups do
@@ -256,7 +256,7 @@ defmodule Acl.UserGroups.Config do
"http://www.semanticdesktop.org/ontologies/2007/03/22/nfo#FileDataObject"
]
}
},
}
]
},

View File

@@ -393,6 +393,10 @@ defmodule Dispatcher do
forward(conn, path, "http://kbo-data-sync/sync-kbo-data/")
end
match "/scope-of-operation/*path", %{layer: api_services, accept: %{json: true}} do
forward(conn, path, "http://scope-of-operation/")
end
get "/ldes/*path", %{layer: :api_services} do
forward(conn, path, "http://ldes-backend/")
end

View File

@@ -0,0 +1,112 @@
PREFIX ere: <http://data.lblod.info/vocabularies/erediensten/>
PREFIX org: <http://www.w3.org/ns/org#>
PREFIX besluit: <http://data.vlaanderen.be/ns/besluit#>
# This migration extracts the relations between (central) worship services and
# their locations according to the rules provided by business. These links are
# inserted into a temporary graph for further processing.
# Worship services
## Municipality organised
INSERT {
GRAPH <http://mu.semte.ch/graphs/temp-for-worship-organizations-scope> {
?ws besluit:werkingsgebied ?scope .
}
} WHERE {
GRAPH <http://mu.semte.ch/graphs/worship-service> {
?ws a ere:BestuurVanDeEredienst ;
ere:typeEredienst ?worshipType .
VALUES ?worshipType {
<http://lblod.data.gift/concepts/99536dd6eb0d2ef38a89efafb17e7389> # Anglicaans
<http://lblod.data.gift/concepts/1a1abeafc973d27cebcb2d7a15b2d823> # Israëlitisch
<http://lblod.data.gift/concepts/e8cba1540b35a32e9cb45126c38c03c6> # Protestants
<http://lblod.data.gift/concepts/b13d1d623626bc1ee75c7d20bc60e3c0> # Rooms-Katholiek
}
?involvement a ere:BetrokkenLokaleBesturen ;
org:organization ?ws ;
ere:typebetrokkenheid ?involvementType .
VALUES ?involvementType {
<http://lblod.data.gift/concepts/86fcbbbff764f1cba4c7e10dbbae578e> # Mede-financierend
<http://lblod.data.gift/concepts/ac400cc9f135ac7873fb3e551ec738c1> # Toezichthoudend
}
?org ere:betrokkenBestuur ?involvement .
}
GRAPH <http://mu.semte.ch/graphs/administrative-unit> {
?org besluit:werkingsgebied ?scope .
}
}
;
## Province organised
INSERT {
GRAPH <http://mu.semte.ch/graphs/temp-for-worship-organizations-scope> {
?ws besluit:werkingsgebied ?scope .
}
} WHERE {
GRAPH <http://mu.semte.ch/graphs/worship-service> {
?ws a ere:BestuurVanDeEredienst ;
ere:typeEredienst ?worshipType .
VALUES ?worshipType {
<http://lblod.data.gift/concepts/9d8bd472a00bf0a5c7b7186606365a52> # Islamitisch
<http://lblod.data.gift/concepts/84bcd6896f575bae4857ff8d2764bed8> # Orthodox
}
?involvement a ere:BetrokkenLokaleBesturen ;
org:organization ?ws ;
ere:typebetrokkenheid ?involvementType .
VALUES ?involvementType {
<http://lblod.data.gift/concepts/0f845f00ee76099c89518cbaf6a7b77f> # Adviserend
}
?org ere:betrokkenBestuur ?involvement .
}
GRAPH <http://mu.semte.ch/graphs/administrative-unit> {
?org besluit:werkingsgebied ?scope .
}
}
;
# Central worship services
## Municipality organised
INSERT {
GRAPH <http://mu.semte.ch/graphs/temp-for-worship-organizations-scope> {
?ws besluit:werkingsgebied ?scope .
}
} WHERE {
GRAPH <http://mu.semte.ch/graphs/worship-service> {
?ws a ere:CentraalBestuurVanDeEredienst ;
ere:typeEredienst ?worshipType .
VALUES ?worshipType {
<http://lblod.data.gift/concepts/b13d1d623626bc1ee75c7d20bc60e3c0> # Rooms-Katholiek
}
?involvement a ere:BetrokkenLokaleBesturen ;
org:organization ?ws ;
ere:typebetrokkenheid ?involvementType .
VALUES ?involvementType {
<http://lblod.data.gift/concepts/ac400cc9f135ac7873fb3e551ec738c1> # Toezichthoudend
}
?org ere:betrokkenBestuur ?involvement .
}
GRAPH <http://mu.semte.ch/graphs/administrative-unit> {
?org besluit:werkingsgebied ?scope .
}
}
;
## Province organised
## Note: Business requested to use the location for the municipality of Ghent as
## default value. They will update it manually afterwards.
INSERT {
GRAPH <http://mu.semte.ch/graphs/temp-for-worship-organizations-scope> {
?ws besluit:werkingsgebied ?locationGhent .
}
} WHERE {
GRAPH <http://mu.semte.ch/graphs/worship-service> {
?ws a ere:CentraalBestuurVanDeEredienst ;
ere:typeEredienst ?worshipType .
VALUES ?worshipType {
<http://lblod.data.gift/concepts/9d8bd472a00bf0a5c7b7186606365a52> # Islamitisch
<http://lblod.data.gift/concepts/84bcd6896f575bae4857ff8d2764bed8> # Orthodox
}
BIND(URI("http://data.lblod.info/id/werkingsgebieden/ce94eeae827cdc5c00f3f2f4276ff628580edefb3aa5861e4669b0c5d93adc57") AS ?locationGhent)
}
}

View File

@@ -0,0 +1,24 @@
PREFIX besluit: <http://data.vlaanderen.be/ns/besluit#>
# This migration sets the scope of operation for worship organisations that have
# single, already existing, location as scope. This migration depends on
# `20250707132349-chore--extract-scopes-for-worship-organisations` being
# executed first, as it uses the data created by the migration.
INSERT {
GRAPH <http://mu.semte.ch/graphs/worship-service> {
?ws besluit:werkingsgebied ?scope .
}
} WHERE {
{
SELECT DISTINCT ?ws (count(DISTINCT ?scope) as ?count)
WHERE {
GRAPH <http://mu.semte.ch/graphs/temp-for-worship-organizations-scope> {
?ws besluit:werkingsgebied ?scope .
}
}
}
FILTER (?count = 1)
GRAPH <http://mu.semte.ch/graphs/temp-for-worship-organizations-scope> {
?ws besluit:werkingsgebied ?scope .
}
}

View File

@@ -0,0 +1,49 @@
PREFIX prov: <http://www.w3.org/ns/prov#>
PREFIX mu: <http://mu.semte.ch/vocabularies/core/>
PREFIX ext: <http://mu.semte.ch/vocabularies/ext/>
PREFIX geo: <http://www.opengis.net/ont/geosparql#>
PREFIX besluit: <http://data.vlaanderen.be/ns/besluit#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
# This migration sets the scope of operation for worship organisations that have
# multiple location as scope. It creates new aggregate locations to contain the
# already existing municipality/province level ones. This migration depends on
# `20250707132349-chore--extract-scopes-for-worship-organisations` being
# executed first, as it uses the data created by the migration.
INSERT {
GRAPH <http://mu.semte.ch/graphs/public> {
?location a prov:Location ;
mu:uuid ?locationUuid ;
rdfs:label ?scopeLabel ;
ext:werkingsgebiedNiveau """Samengesteld werkingsgebied""" .
?scope geo:sfWithin ?location .
}
GRAPH <http://mu.semte.ch/graphs/worship-service> {
?ws besluit:werkingsgebied ?location .
}
} WHERE {
{
SELECT DISTINCT ?ws (count(DISTINCT ?scope) AS ?count) (GROUP_CONCAT(?uuid," ") AS ?scopeUuids) (GROUP_CONCAT(?label,", ") AS ?scopeLabel)
WHERE {
GRAPH <http://mu.semte.ch/graphs/temp-for-worship-organizations-scope> {
?ws besluit:werkingsgebied ?scope .
}
GRAPH <http://mu.semte.ch/graphs/public> {
?scope mu:uuid ?uuid ;
rdfs:label ?label .
}
}
}
FILTER (?count > 1)
GRAPH <http://mu.semte.ch/graphs/temp-for-worship-organizations-scope> {
?ws besluit:werkingsgebied ?scope .
}
# NOTE (10/07/2025): The SPARQL specification does not specify any order for
# `GROUP_CONCAT`. But virtuoso does seem to concatenate elements always in the
# same order. Therefore, this way of generating a UUID is a bit of a hack to
# ensure that new locations that contain the same locations will receive the
# same UUUID, which results in the same location resource being inserted
# twice.
BIND (STR(SHA256(?scopeUuids)) AS ?locationUuid)
BIND (URI(CONCAT("http://data.lblod.info/id/werkingsgebieden/", ?locationUuid)) AS ?location)
}

View File

@@ -0,0 +1 @@
CLEAR GRAPH <http://mu.semte.ch/graphs/temp-for-worship-organizations-scope>

View File

@@ -0,0 +1,30 @@
DELETE {
GRAPH ?h {
?site <https://data.vlaanderen.be/ns/organisatie#bestaatUit> ?oldAddressUri .
}
} INSERT {
GRAPH ?g {
?newAddressUri a <http://www.w3.org/ns/locn#Address> ;
<http://mu.semte.ch/vocabularies/core/uuid> ?newAddressUuid ;
?p ?o .
?site <https://data.vlaanderen.be/ns/organisatie#bestaatUit> ?newAddressUri .
}
} WHERE {
GRAPH ?g {
?oldAddressUri a <http://www.w3.org/ns/locn#Address> ;
?p ?o .
FILTER (?p NOT IN (<http://mu.semte.ch/vocabularies/core/uuid>))
}
?mandatarisContact <http://www.w3.org/ns/locn#address> ?oldAddressUri .
GRAPH ?h {
?site a <http://www.w3.org/ns/org#Site>;
<https://data.vlaanderen.be/ns/organisatie#bestaatUit> ?oldAddressUri .
}
BIND(MD5(CONCAT(?oldAddressUri,"NEW_ADDRESS")) AS ?newAddressUuid) .
BIND(IRI(CONCAT("http://data.lblod.info/id/adressen/", STR(?newAddressUuid))) AS ?newAddressUri) .
}

View File

@@ -0,0 +1,33 @@
PREFIX org: <http://www.w3.org/ns/org#>
PREFIX mu: <http://mu.semte.ch/vocabularies/core/>
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
DELETE {
GRAPH ?g {
?membership ?p ?o .
}
}
WHERE {
GRAPH ?g {
# Find memberships that use the general 'gerelateerd' role
?membership a org:Membership ;
org:role <http://data.lblod.info/id/rollen/4ec7d5c39bdc4e84b4174379b9e22ad8> ;
org:member ?orgMember ;
org:organization ?organization .
# Only delete if there's a specific role for the inverse membership
FILTER EXISTS {
?specific_membership a org:Membership ;
org:member ?organization ;
org:organization ?orgMember ;
org:role ?specificRole .
VALUES ?specificRole {
<http://data.lblod.info/id/rollen/2152eb830b1143bfb97a7dd9596d6c63> # participant
<http://data.lblod.info/id/rollen/73d5e1cf250d42fab15926771f07505a> # stichtend lid
}
}
?membership ?p ?o .
}
}

View File

@@ -0,0 +1,18 @@
DELETE {
GRAPH <http://mu.semte.ch/graphs/administrative-unit> {
?address ?p ?o .
}
} INSERT {
GRAPH <http://mu.semte.ch/graphs/worship-service> {
?address ?p ?o .
}
} WHERE {
GRAPH <http://mu.semte.ch/graphs/worship-service> {
?contactPoint a <http://schema.org/ContactPoint> ;
<http://www.w3.org/ns/locn#address> ?address .
}
GRAPH <http://mu.semte.ch/graphs/administrative-unit> {
?address ?p ?o .
}
}

View File

@@ -945,7 +945,7 @@
"located-within": {
"predicate": "geo:sfWithin",
"target": "locations",
"cardinality": "one"
"cardinality": "many"
},
"locations": {
"predicate": "geo:sfWithin",

View File

@@ -74,6 +74,8 @@ services:
restart: "no"
construct-organization-relationships:
restart: "no"
scope-of-operation:
restart: "no"
mandatarissen-consumer:
restart: "no"
leidinggevenden-consumer:

View File

@@ -285,6 +285,14 @@ services:
- "logging=true"
restart: always
logging: *default-logging
scope-of-operation:
image: lblod/scope-of-operation-service:0.1.0
links:
- db:database
labels:
- "logging=true"
restart: always
logging: *default-logging
report-generation:
image: lblod/loket-report-generation-service:0.8.3
environment:

View File

@@ -17,6 +17,22 @@
"app": "/app/"
}
},
{
"documentation": {
"command": "correct-location-labels",
"description": "generate migrations to correct the labels of aggregated locations",
"arguments": []
},
"environment": {
"image": "nvdk/ruby-semantic-works-cli",
"interactive": true,
"script": "correct-location-labels/run.rb",
"join_networks": true
},
"mounts": {
"app": "/app/"
}
},
{
"documentation": {
"command": "mu-search-delta",

View File

@@ -0,0 +1,106 @@
#!/usr/bin/env ruby
require 'date'
require 'rdf/turtle'
require 'fileutils'
$stdout.sync = true
ENV['LOG_SPARQL_ALL']='false'
ENV['MU_SPARQL_ENDPOINT']='http://triplestore:8890/sparql'
ENV['MU_SPARQL_TIMEOUT']='180'
require 'mu/auth-sudo'
def ensure_dir(path)
unless Dir.exist?(path)
FileUtils.mkdir_p(path)
end
end
def sparql_escape_uri(value)
'<' + value.to_s.gsub(/[\\"<>]/) { |s| '\\' + s } + '>'
end
def sparql_escape_string(str)
'"""' + str.gsub(/[\\"]/) { |s| '\\' + s } + '"""'
end
def get_locations()
query = <<~EOF
PREFIX prov: <http://www.w3.org/ns/prov#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX ext: <http://mu.semte.ch/vocabularies/ext/>
SELECT DISTINCT ?location ?label
WHERE {
GRAPH <http://mu.semte.ch/graphs/public> {
?location a prov:Location ;
rdfs:label ?label ;
ext:werkingsgebiedNiveau """Samengesteld werkingsgebied""" .
}
}
EOF
Mu::AuthSudo.query(query)
end
def sort_literal(literal)
literal
.humanize
.split(", ")
.sort
.join(", ")
end
# Create output directory
timestamp=`date +%Y%0m%0d%0H%0M%0S`.strip.to_i
output_dir = "/app/config/migrations/local/2025/#{timestamp}-correct-labels-aggregate-locations/"
ensure_dir(output_dir)
# Generate SPARQL migration to remove existing labels
path = File.join(output_dir, "#{timestamp}-remove-labels-aggregate-locations.sparql")
File.open(path, 'w') do |file|
file.write <<EOF
PREFIX prov: <http://www.w3.org/ns/prov#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX ext: <http://mu.semte.ch/vocabularies/ext/>
DELETE {
GRAPH <http://mu.semte.ch/graphs/public> {
?location rdfs:label ?label
}
} WHERE {
GRAPH <http://mu.semte.ch/graphs/public> {
?location a prov:Location ;
rdfs:label ?label ;
ext:werkingsgebiedNiveau """Samengesteld werkingsgebied""" .
}
}
EOF
end
# Generate TTL migration to insert alphabetically sorted labels
timestamp += 1
filename = "#{timestamp}-insert-labels-aggregate-locations"
path = File.join(output_dir, "#{filename}.graph")
File.open(path, 'w') do |file|
file.write <<EOF
http://mu.semte.ch/graphs/public
EOF
end
path = File.join(output_dir, "#{filename}.ttl")
File.open(path, 'w') do |file|
file.write <<EOF
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
EOF
end
locations = get_locations()
locations.each do |location|
File.open(path, 'a') do |file|
file.write <<EOF
#{sparql_escape_uri(location[:location])} rdfs:label "#{(sort_literal(location[:label]))}" .
EOF
end
end