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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ You can also check the

- Features
- add link to https://status.lindas.admin.ch/ in footer
- Show the abbreviation (`schema:alternateName`) next to the name of cube
creators from https://lindas.admin.ch/fch/rvov, so that e.g. "Bundeskanzlei"
is displayed as "Bundeskanzlei BK" like the entries from the old termset.
Creators from both graphs are now also resolved on dataset metadata and
visualization detail pages
- Fixes
- Statistics page no longer fails when the database contains malformed chart
configs; unsafe cube IRIs are filtered out and SPARQL metadata lookups
Expand Down
43 changes: 43 additions & 0 deletions app/rdf/parse-search-results.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Quad } from "rdf-js";
import { describe, expect, it } from "vitest";

import { SearchCube } from "@/domain/data";
import * as ns from "@/rdf/namespace";
import { mergeSearchCubes } from "@/rdf/query-search";

import { buildSearchCubes } from "./parse-search-results";
Expand Down Expand Up @@ -341,6 +342,48 @@ describe("parse-search-results", () => {
]
`);
});

it("should merge creator alternateName into creator label", () => {
const cubeIri = "https://example.com/cube";
const creatorIri = "https://ld.admin.ch/FCh";
const q = (subject: string, predicate: string, object: string) =>
({
subject: { value: subject },
predicate: { value: predicate },
object: { value: object },
}) as Quad;

const makeCubeQuads = (creatorQuads: Quad[]) => [
q(cubeIri, ns.rdf.type.value, ns.cube.Cube.value),
q(
cubeIri,
ns.schema.creativeWorkStatus.value,
ns.adminVocabulary("CreativeWorkStatus/Published").value
),
q(cubeIri, ns.schema.creator.value, creatorIri),
...creatorQuads,
];

const withAlternateName = buildSearchCubes(
makeCubeQuads([
q(creatorIri, ns.schema.name.value, "Bundeskanzlei"),
q(creatorIri, ns.schema.alternateName.value, "BK"),
q(creatorIri, ns.schema.alternateName.value, "ChF"),
])
);
expect(withAlternateName[0].creator).toEqual({
iri: creatorIri,
label: "Bundeskanzlei BK ChF",
});

const withoutAlternateName = buildSearchCubes(
makeCubeQuads([q(creatorIri, ns.schema.name.value, "Bundeskanzlei")])
);
expect(withoutAlternateName[0].creator).toEqual({
iri: creatorIri,
label: "Bundeskanzlei",
});
});
});

it("should merge search cubes together", async () => {
Expand Down
22 changes: 18 additions & 4 deletions app/rdf/parse-search-results.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { group } from "d3-array";
import { Quad } from "rdf-js";

import { SearchCube } from "@/domain/data";
import { truthy } from "@/domain/types";
import { stringifyComponentId } from "@/graphql/make-component-id";
import { DataCubePublicationStatus } from "@/graphql/resolver-types";
import * as ns from "@/rdf/namespace";
Expand Down Expand Up @@ -49,6 +50,22 @@ function buildSearchCubes(
const dimensions = cubeQuads.get(visualizePredicates.hasDimension);
const creatorIri = cubeQuads.get(ns.schema.creator.value)?.[0]?.object
.value;
const creatorSubjectQuads = creatorIri
? bySubjectAndPredicate.get(creatorIri)
: undefined;
const creatorAlternateNames = [
...new Set(
creatorSubjectQuads
?.get(ns.schema.alternateName.value)
?.map((x) => x.object.value) ?? []
),
].sort();
const creatorLabel = [
creatorSubjectQuads?.get(ns.schema.name.value)?.[0]?.object.value,
...creatorAlternateNames,
]
.filter(truthy)
.join(" ");
const publicationStatus = cubeQuads.get(
ns.schema.creativeWorkStatus.value
)?.[0].object.value;
Expand All @@ -73,10 +90,7 @@ function buildSearchCubes(
creator: creatorIri
? {
iri: creatorIri,
label:
bySubjectAndPredicate
.get(creatorIri)
?.get(ns.schema.name.value)?.[0].object.value ?? "",
label: creatorLabel,
}
: null,
themes:
Expand Down
122 changes: 122 additions & 0 deletions app/rdf/query-cube-metadata.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { describe, expect, it, vi } from "vitest";

import { getCubeMetadata } from "@/rdf/query-cube-metadata";
import { GROUP_SEPARATOR } from "@/rdf/query-utils";

const term = (value: string) => ({ value }) as any;

const buildRow = (overrides: Record<string, any> = {}) => ({
iri: term("https://example.com/cube"),
unversionedIri: term("https://example.com/cube"),
identifier: term("id"),
title: term("Title"),
description: term("Description"),
version: term("1.0"),
datePublished: term("2024-01-01"),
dateModified: term("2024-01-02"),
status: term("http://publishers.schema.org/Published"),
themeIris: term(""),
themeLabels: term(""),
creatorIri: undefined,
creatorLabel: undefined,
creatorAlternateNames: term(""),
contactPointEmails: term(""),
contactPointNames: term(""),
publisher: term("https://example.com/publisher"),
landingPage: term("https://example.com"),
expires: term(""),
workExamples: term(""),
...overrides,
});

const mockClient = (row: any, onQuery?: (q: string) => void) =>
({
query: {
select: vi.fn(async (q: string) => {
onQuery?.(q);
return [row];
}),
},
}) as any;

describe("getCubeMetadata creator", () => {
it("should combine creator name and alternate name", async () => {
const metadata = await getCubeMetadata("https://example.com/cube", {
locale: "de",
sparqlClient: mockClient(
buildRow({
creatorIri: term("https://ld.admin.ch/FCh"),
creatorLabel: term("Bundeskanzlei"),
creatorAlternateNames: term("BK"),
})
),
});

expect(metadata.creator).toEqual({
iri: "https://ld.admin.ch/FCh",
label: "Bundeskanzlei BK",
});
});

it("should sort multiple alternate names", async () => {
const metadata = await getCubeMetadata("https://example.com/cube", {
locale: "de",
sparqlClient: mockClient(
buildRow({
creatorIri: term("https://ld.admin.ch/office/VI.1.4"),
creatorLabel: term(
"Staatssekretariat für Bildung, Forschung und Innovation"
),
creatorAlternateNames: term(["SBFI", "BFI"].join(GROUP_SEPARATOR)),
})
),
});

expect(metadata.creator?.label).toBe(
"Staatssekretariat für Bildung, Forschung und Innovation BFI SBFI"
);
});

it("should keep legacy creator label without alternate names", async () => {
const metadata = await getCubeMetadata("https://example.com/cube", {
locale: "de",
sparqlClient: mockClient(
buildRow({
creatorIri: term(
"https://register.ld.admin.ch/opendataswiss/org/bundesamt-fur-energie-bfe"
),
creatorLabel: term("Swiss Federal Office of Energy SFOE"),
creatorAlternateNames: term(""),
})
),
});

expect(metadata.creator).toEqual({
iri: "https://register.ld.admin.ch/opendataswiss/org/bundesamt-fur-energie-bfe",
label: "Swiss Federal Office of Energy SFOE",
});
});

it("should return no creator when creator is missing", async () => {
const metadata = await getCubeMetadata("https://example.com/cube", {
locale: "de",
sparqlClient: mockClient(buildRow()),
});

expect(metadata.creator).toBeUndefined();
});

it("should query both creator graphs", async () => {
let query = "";
await getCubeMetadata("https://example.com/cube", {
locale: "de",
sparqlClient: mockClient(buildRow(), (q) => (query = q)),
});

expect(query).toContain("https://lindas.admin.ch/fch/rvov");
expect(query).toContain("https://lindas.admin.ch/sfa/opendataswiss");
expect(query).toContain("schema:alternateName");
expect(query).toContain("schema:creator");
expect(query).toContain("dcterms:creator");
});
});
44 changes: 35 additions & 9 deletions app/rdf/query-cube-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Literal, NamedNode } from "rdf-js";
import { ParsingClient } from "sparql-http-client/ParsingClient";

import { DataCubeMetadata } from "@/domain/data";
import { truthy } from "@/domain/types";
import { DataCubePublicationStatus } from "@/graphql/resolver-types";
import { pragmas } from "@/rdf/create-source";
import * as ns from "@/rdf/namespace";
Expand All @@ -22,6 +23,7 @@ type RawDataCubeMetadata = {
themeLabels: Literal;
creatorIri: NamedNode;
creatorLabel: Literal;
creatorAlternateNames: Literal;
contactPointEmails: NamedNode;
contactPointNames: NamedNode;
publisher: NamedNode;
Expand Down Expand Up @@ -60,7 +62,17 @@ const parseRawMetadata = (cube: RawDataCubeMetadata): DataCubeMetadata => {
: [],
creator:
cube.creatorIri && cube.creatorLabel
? { iri: cube.creatorIri.value, label: cube.creatorLabel.value }
? {
iri: cube.creatorIri.value,
label: [
cube.creatorLabel.value,
...(cube.creatorAlternateNames?.value
.split(GROUP_SEPARATOR)
.sort() ?? []),
]
.filter(truthy)
.join(" "),
}
: undefined,
contactPoints:
contactPointEmails.length === contactPointNames.length
Expand All @@ -83,6 +95,7 @@ export const getCubeMetadata = async (
const query = SELECT`
?iri ?identifier ?title ?description ?version ?datePublished ?dateModified ?status ?creatorIri ?creatorLabel ?unversionedIri ?publisher ?landingPage ?expires
(GROUP_CONCAT(DISTINCT ?themeIri; SEPARATOR="${GROUP_SEPARATOR}") AS ?themeIris)
(GROUP_CONCAT(DISTINCT ?creatorAlternateName; SEPARATOR="${GROUP_SEPARATOR}") AS ?creatorAlternateNames)
(GROUP_CONCAT(DISTINCT ?themeLabel; SEPARATOR="${GROUP_SEPARATOR}") AS ?themeLabels)
(GROUP_CONCAT(DISTINCT ?subthemeIri; SEPARATOR="${GROUP_SEPARATOR}") AS ?subthemeIris)
(GROUP_CONCAT(DISTINCT ?subthemeLabel; SEPARATOR="${GROUP_SEPARATOR}") AS ?subthemeLabels)
Expand All @@ -103,18 +116,31 @@ export const getCubeMetadata = async (
OPTIONAL { ?iri ${ns.schema.dateModified} ?dateModified . }
?iri ${ns.schema.creativeWorkStatus} ?status .
OPTIONAL {
?iri ${ns.dcterms.creator} ?creatorIri .
GRAPH <https://lindas.admin.ch/sfa/opendataswiss> {
?creatorIri a ${ns.schema.Organization} ;
${
ns.schema.inDefinedTermSet
} <https://register.ld.admin.ch/opendataswiss/org> .
{
?iri ${ns.dcterms.creator} ?creatorIri .
} UNION {
?iri ${ns.schema.creator} ?creatorIri .
}
VALUES ?creatorGraph {
<https://lindas.admin.ch/fch/rvov>
<https://lindas.admin.ch/sfa/opendataswiss>
}
GRAPH ?creatorGraph {
VALUES ?creatorType { schema:Organization schema:GovernmentOrganization }
?creatorIri a ?creatorType .
${buildLocalizedSubQuery("creatorIri", "schema:name", "creatorLabel", {
locale,
})}
}
OPTIONAL {
GRAPH <https://lindas.admin.ch/fch/rvov> {
${buildLocalizedSubQuery(
"creatorIri",
"schema:name",
"creatorLabel",
"schema:alternateName",
"creatorAlternateName",
{ locale }
)}
}
}
}
OPTIONAL {
Expand Down
22 changes: 21 additions & 1 deletion app/rdf/query-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,9 @@ const mkScoresQuery = (
meta:isUsedIn ?iri ;
schema:name ?termsetLabel .

?creatorIri schema:name ?creatorLabel .
?creatorIri
schema:name ?creatorLabel ;
schema:alternateName ?creatorAlternateName .

?subthemeIri
schema:inDefinedTermSet ?subthemeTermset ;
Expand All @@ -237,6 +239,7 @@ const mkScoresQuery = (
?termsetIri
?termsetLabel
?creatorLabel
?creatorAlternateName
?subthemeIri
?subthemeTermset
?subthemeLabel
Expand Down Expand Up @@ -339,6 +342,16 @@ const mkScoresQuery = (
{ locale }
)}
}
OPTIONAL {
GRAPH <https://lindas.admin.ch/fch/rvov> {
${buildLocalizedSubQuery(
"creatorIri",
"schema:alternateName",
"creatorAlternateName",
{ locale }
)}
}
}
}

OPTIONAL {
Expand Down Expand Up @@ -417,6 +430,12 @@ const mkScoresQuery = (
CONTAINS(LCASE(?creatorLabel_en), LCASE(?keyword)) ||
CONTAINS(LCASE(?creatorLabel_), LCASE(?keyword)) ||

CONTAINS(LCASE(?creatorAlternateName_de), LCASE(?keyword)) ||
CONTAINS(LCASE(?creatorAlternateName_fr), LCASE(?keyword)) ||
CONTAINS(LCASE(?creatorAlternateName_it), LCASE(?keyword)) ||
CONTAINS(LCASE(?creatorAlternateName_en), LCASE(?keyword)) ||
CONTAINS(LCASE(?creatorAlternateName_), LCASE(?keyword)) ||

CONTAINS(LCASE(?themeLabel_de), LCASE(?keyword)) ||
CONTAINS(LCASE(?themeLabel_fr), LCASE(?keyword)) ||
CONTAINS(LCASE(?themeLabel_it), LCASE(?keyword)) ||
Expand Down Expand Up @@ -452,6 +471,7 @@ const mkScoresQuery = (
?termsetIri
?termsetLabel
?creatorLabel
?creatorAlternateName
?subthemeIri
?subthemeTermset
?subthemeLabel
Expand Down
Loading