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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ files/client.xsl.sef.json
/uploads/
/datasets/owner/
/datasets/secretary/
docker-compose.override.yml
67 changes: 59 additions & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,9 @@ since the rows differ in the image cell.
Containers and taxonomy scheme docs come from `app/` (one Turtle file per
container, PUT via LDH CLI by `make install`); each carries an
`rdf:_1 <#select-children>` → `ldh:Object`/`ldh:ChildrenView` block, without
which LDH renders no children listing at all.
which LDH renders no children listing at all. `app/persons.ttl` swaps the stock
`ldh:ChildrenView` for its own `ldh:View` + `sp:Select` (`ac:GridMode`) so the
listing gets person filter/sort columns — see the next bullet.
- **1:N entity views**: cross-entity listings (county → municipalities, committee
→ members, party → nominees) are `ldh:inverseView` definitions in `app/ns.ttl`
(the LDH namespace ontology, northwind-traders style): `<property>
Expand All @@ -114,6 +116,29 @@ since the rows differ in the image cell.
`cv:PublicOrganisation` for the Seimas). `app/import-ns.sh` (called by `make
install`) resets + POSTs the ontology into the admin `ontologies/namespace/`
document (served at `{base}ns`) and evicts the server-side ontology cache.
- **View filter/sort columns**: LDH builds a view's facet pills and its sort
dropdown from the BGP triples whose **subject is the query's FIRST projected
variable** and whose **object is a variable** (`ldh:RenderFacets` and
`$var-predicates` in LDH's `client/block/view.xsl`). Those variables must stay
**unprojected**: LDH wraps the SELECT into `DESCRIBE *`, so every projected term
becomes a rendered card/row — hence the "project a single entity type" rule in
`app/root.ttl`. Consequences for authoring a view query:
- the focus variable has to be the entity carrying the columns (`?person` =
`{doc}#this`, never the `dh:Item` document — the attributes hang off `#this`);
- a column is only a *filter* pill when its predicate is a plain URI (a property
path still yields a sort option, labelled with the bare variable name);
- column labels come from the `{base}ns` ontology, so every faceted predicate
needs an `rdfs:label` in `app/ns.ttl`;
- `ORDER BY` sets the initial sort; a second condition is the tie-breaker. Both
are re-applied client-side over the (unordered) DESCRIBEd graph;
- opening a pill makes LDH append a label lookup to the facet-value-count query
that ARQ cannot index inside a `GRAPH` block (~104 s per pill → 500);
`files/client.xsl` rewrites it — see the XSLT overrides below. Keep writing
`GRAPH ?graph` in view queries.
Person listings — the `persons/` container view and the `:CurrentMembers`,
`:FormerMembers`, `:NominatedMembers`, `:MembersElectedHere` ontology views —
therefore project only `?person` and carry `foaf:givenName`/`foaf:familyName`/
`foaf:gender`/`ltlod:nominatedBy` triples, sorted by family then given name.
- **XSLT overrides** (`files/`): custom Saxon-JS templates that change how LDH
renders, layered over the stock stylesheets — the linkeddatahub.com pattern.
Three files, all mounted over the running image's `ROOT/static` for the
Expand All @@ -128,8 +153,15 @@ since the rows differ in the image cell.
…)`). Imported by **both** client.xsl and layout.xsl so the blocks are omitted
server-side *and* client-side — server-only would still let CSR re-render them
(flash); client-only lets the server render them first (flash).
- `files/client.xsl` — imports `client.xsl` + `overrides.xsl`; adds the
**client-only** `:Memberships` table tidy (restrict columns to role /
- `files/client.xsl` — imports `client.xsl` + `overrides.xsl`; overrides the
stock `ldh:bgp-value-counts` **facet label lookup**: to label a filter pill's
values LDH appends `OPTIONAL { { ?value <7-way alt path> ?label } UNION
{ GRAPH ?g { … } } }`, and ARQ does not resolve an alternative path through the
quad indexes inside a `GRAPH` block — it walks the ~117k named graphs, so one
pill took ~104 s (a 500 in the browser). The override emits the equivalent
`?value ?labelProp ?label` + `FILTER (?labelProp IN (…))` (same predicates,
same order, so `SAMPLE(?label)` is unchanged) — ~0.1 s per pill. It also adds
the **client-only** `:Memberships` table tidy (restrict columns to role /
organization / memberDuring, drop the anchor column, render the `memberDuring`
cell as the interval's `dct:title` period *text* not a link). Compiled to the
SEF; the view table is client-rendered so these needn't run server-side.
Expand All @@ -142,15 +174,34 @@ since the rows differ in the image cell.
views (`:SubUnits`, the frontpage counties map) use their own `spin:query` and
are unaffected, so settlement points stay mappable there.
- `files/layout.xsl` — imports base `layout.xsl` + `overrides.xsl`; repoints the
client bootstrap (`xhtml:Script` → `client-stylesheet`) at our SEF. Mounted at
the end-user app's `ac:stylesheet` target `static/xsl/layout.xsl`; imports
`overrides.xsl` from `static/xsl/` (its own dir).
client bootstrap (`xhtml:Script` → `client-stylesheet`) at our SEF, and
**replaces the stock `bs2:Footer`** with the LTLOD one (own wordmark, dataset
shortcuts, source attribution, licence; the developer entry points — SPARQL,
example queries, dataset downloads, `{base}ns` — live there instead of on the
frontpage). The footer keeps the stock markup contract, since `app.css` styles
it structurally: `.ldh-footer > .cols` is a `1.4fr repeat(4, 1fr)` grid, so
exactly one `.brand-col` plus FOUR `.col` children, each a `.ftitle` followed
by bare `<a>`s, then `.legal` with two space-between spans. Only the wordmark
`.mark` deviates — restyled inline to the Lithuanian tricolour, class-supplied
geometry kept. Footer-only because `bs2:Footer` is applied ONCE server-side
(LDH `layout.xsl`) and never re-rendered by client.xsl — so it belongs here,
not in `overrides.xsl`, and needs no SEF rebuild. Mounted at the end-user app's
`ac:stylesheet` target `static/xsl/layout.xsl`; imports `overrides.xsl` from
`static/xsl/` (its own dir).
**Declare `xmlns="http://www.w3.org/1999/xhtml"` on `xsl:stylesheet`** whenever
this file emits literal result elements: LDH's own layout.xsl declares it, so
its bare `<div>`s are XHTML; without it ours land in no namespace and the
serialiser emits a stray `xmlns=""` on the block (HTML parsers ignore it, but
namespace-sensitive XPath/XSLT matching on `xhtml:*` then misses those nodes).
Wiring: `make sef` c14n's `client.xsl`+`overrides.xsl` and compiles the SEF
against the pinned image's `static/` tree; `docker-compose.yml` bind-mounts the
four files (`layout.xsl`, `overrides.xsl` under `static/xsl/`; `client.xsl`,
`client.xsl.sef.json` under `static/com/ltlod/xsl/`). Rebuild the SEF + recreate
the container after editing any of them. The `:Memberships` table replaces the
suppressed blocks; it needs a readable period, so `persons.rq` puts a `dct:title`
the container after editing any of them — `docker compose restart linkeddatahub`
does NOT pick up an edited mount (it keeps serving the stylesheet compiled at the
previous start); `docker compose up -d --force-recreate linkeddatahub` does, and
then restart `nginx`/varnish per the 502 gotcha below. The `:Memberships` table
replaces the suppressed blocks; it needs a readable period, so `persons.rq` puts a `dct:title`
(e.g. `"2024-11-14 – dabar"`) on each `time:Interval` — `ldh:View`/`ac:TableMode`
is DESCRIBE-based, so date literals can't be view columns; the interval's
`dct:title` surfaces in the `org:memberDuring` cell via object-label resolution.
Expand Down
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,10 +184,14 @@ Duomenų struktūra kuriama dviem lygiais:
schemos iš `app/` katalogo dokumentas po dokumento **per LDH CLI** (`put.sh`,
kaip [LinkedDataHub-Apps](https://github.com/AtomGraph/LinkedDataHub-Apps)
projektuose) — taip dokumentai gauna `ldh:ChildrenView` bloką, dėl kurio
konteinerių puslapiai rodo vaikų sąrašus. Kartu įdiegiama vardų erdvės
ontologija (`app/ns.ttl`) su 1:N rodiniais (`ldh:inverseView`): apskrities
puslapis rodo jos savivaldybes, komiteto — dabartinius ir buvusius narius,
Seimo — padalinius, partijos — jos iškeltus Seimo narius. Reikia šalia
konteinerių puslapiai rodo vaikų sąrašus. Asmenų konteineris
(`app/persons.ttl`) vietoj jo turi savo rodinį su užklausa: Seimo nariai
rodomi nuotraukų tinkleliu, kurį galima filtruoti ir rikiuoti pagal vardą,
pavardę, lytį ir iškėlusią partiją. Kartu įdiegiama vardų erdvės ontologija
(`app/ns.ttl`) su 1:N rodiniais (`ldh:inverseView`): apskrities puslapis rodo
jos savivaldybes, komiteto — dabartinius ir buvusius narius, Seimo —
padalinius, partijos — jos iškeltus Seimo narius; visi asmenų sąrašai turi tuos
pačius filtrus ir rikiavimą kaip asmenų konteineris. Reikia šalia
išklonintos [LinkedDataHub](https://github.com/AtomGraph/LinkedDataHub)
repozitorijos (`../LinkedDataHub`, keičiama per `make install LDH_HOME=…`).
- **Duomenys** (`make load`): ETL rinkiniai — vien `dh:Item` dokumentai su
Expand Down
76 changes: 60 additions & 16 deletions app/ns.ttl
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,25 @@
# image-bearing persons (foaf:depiction portraits from lrs.lt) use ac:GridMode so
# LDH lays them out as a thumbnail grid (bs2:ContainerGrid → foaf:depiction per
# card). Imageless targets (Seimas org-units) keep the default list.
#
# Filter/sort columns: LDH turns the BGP triples whose subject is the query's
# FIRST projected variable — and whose object is a variable — into the view's
# facet pills and sort-dropdown options. Those variables stay UNPROJECTED: LDH
# wraps the SELECT into DESCRIBE *, so anything else in the projection would be
# rendered as its own card. So the person views (:CurrentMembers, :FormerMembers,
# :NominatedMembers, :MembersElectedHere, and the persons/ container view in
# app/persons.ttl) project ?person alone and carry foaf:givenName /
# foaf:familyName / foaf:gender — plus ltlod:nominatedBy wherever the party is not
# already pinned by $about — then ORDER BY family, given name (the first ORDER BY
# condition is the initial sort, the second the tie-breaker). Facet and sort
# labels resolve against the property labels below, so a column predicate without
# an rdfs:label here degrades to its local name.
#
# Those pills only work because files/client.xsl overrides LDH's
# ldh:bgp-value-counts label lookup: its alternative property path is not indexed
# by ARQ inside a GRAPH block and scans all ~117k named graphs (~104 s per pill);
# rewritten to a variable predicate + FILTER IN it is ~0.1 s. Keep the GRAPH ?graph
# form in these queries — the override, not the query shape, is what pays for it.

@prefix : <#> .
@prefix ldh: <https://w3id.org/atomgraph/linkeddatahub#> .
Expand Down Expand Up @@ -366,9 +385,10 @@ ORDER BY ?org
rdfs:isDefinedBy : .

# Memberships are n-ary org:Membership fragments in the person's graph; the
# interval lives in the same graph, so one GRAPH block suffices. Current
# membership = org:memberDuring interval without time:hasEnd. A person with an
# ended stint and a current one appears in both lists — accepted.
# interval and the person's own attributes live in the same graph, so one GRAPH
# block suffices. Current membership = org:memberDuring interval without
# time:hasEnd. A person with an ended stint and a current one appears in both
# lists — accepted.

:CurrentMembers a ldh:View ;
dct:title "Dabartiniai nariai"@lt, "Current members"@en ;
Expand All @@ -379,19 +399,25 @@ ORDER BY ?org
:SelectCurrentMembers a sp:Select ;
rdfs:label "Select current members" ;
sp:text """
PREFIX org: <http://www.w3.org/ns/org#>
PREFIX time: <http://www.w3.org/2006/time#>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
PREFIX ltlod: <http://linkeddata.lt/ns#>
PREFIX org: <http://www.w3.org/ns/org#>
PREFIX time: <http://www.w3.org/2006/time#>

SELECT DISTINCT ?person
WHERE
{ GRAPH ?graph
{ ?membership org:organization $about ;
org:member ?person ;
org:memberDuring ?interval
org:memberDuring ?interval .
?person foaf:givenName ?givenName ;
foaf:familyName ?familyName ;
foaf:gender ?gender ;
ltlod:nominatedBy ?party
FILTER NOT EXISTS { ?interval time:hasEnd ?end }
}
}
ORDER BY ?person
ORDER BY ?familyName ?givenName
""" ;
rdfs:isDefinedBy : .

Expand All @@ -404,17 +430,24 @@ ORDER BY ?person
:SelectFormerMembers a sp:Select ;
rdfs:label "Select former members" ;
sp:text """
PREFIX org: <http://www.w3.org/ns/org#>
PREFIX time: <http://www.w3.org/2006/time#>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
PREFIX ltlod: <http://linkeddata.lt/ns#>
PREFIX org: <http://www.w3.org/ns/org#>
PREFIX time: <http://www.w3.org/2006/time#>

SELECT DISTINCT ?person
WHERE
{ GRAPH ?graph
{ ?membership org:organization $about ;
org:member ?person ;
org:memberDuring/time:hasEnd ?end }
org:memberDuring/time:hasEnd ?end .
?person foaf:givenName ?givenName ;
foaf:familyName ?familyName ;
foaf:gender ?gender ;
ltlod:nominatedBy ?party
}
}
ORDER BY ?person
ORDER BY ?familyName ?givenName
""" ;
rdfs:isDefinedBy : .

Expand Down Expand Up @@ -446,14 +479,19 @@ ORDER BY ?unit
:SelectNominatedMembers a sp:Select ;
rdfs:label "Select persons nominated by party" ;
sp:text """
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
PREFIX ltlod: <http://linkeddata.lt/ns#>

SELECT DISTINCT ?person
WHERE
{ GRAPH ?graph
{ ?person ltlod:nominatedBy $about }
{ ?person ltlod:nominatedBy $about ;
foaf:givenName ?givenName ;
foaf:familyName ?familyName ;
foaf:gender ?gender
}
}
ORDER BY ?person
ORDER BY ?familyName ?givenName
""" ;
rdfs:isDefinedBy : .

Expand All @@ -469,16 +507,22 @@ ORDER BY ?person
:SelectMembersElectedHere a sp:Select ;
rdfs:label "Select members elected in this district" ;
sp:text """
PREFIX org: <http://www.w3.org/ns/org#>
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
PREFIX ltlod: <http://linkeddata.lt/ns#>
PREFIX org: <http://www.w3.org/ns/org#>

SELECT DISTINCT ?person
WHERE
{ GRAPH ?graph
{ ?membership org:member ?person ;
ltlod:electoralDistrict $about }
ltlod:electoralDistrict $about .
?person foaf:givenName ?givenName ;
foaf:familyName ?familyName ;
foaf:gender ?gender ;
ltlod:nominatedBy ?party
}
}
ORDER BY ?person
ORDER BY ?familyName ?givenName
""" ;
rdfs:isDefinedBy : .

Expand Down
61 changes: 54 additions & 7 deletions app/persons.ttl
Original file line number Diff line number Diff line change
@@ -1,11 +1,58 @@
@prefix dh: <https://www.w3.org/ns/ldt/document-hierarchy#> .
@prefix ldh: <https://w3id.org/atomgraph/linkeddatahub#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix dct: <http://purl.org/dc/terms/> .
@prefix dh: <https://www.w3.org/ns/ldt/document-hierarchy#> .
@prefix ldh: <https://w3id.org/atomgraph/linkeddatahub#> .
@prefix ac: <https://w3id.org/atomgraph/client#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix dct: <http://purl.org/dc/terms/> .
@prefix sp: <http://spinrdf.org/sp#> .
@prefix spin: <http://spinrdf.org/spin#> .

<> a dh:Container ;
dct:title "Asmenys"@lt ;
rdf:_1 <#select-children> .
rdf:_1 <#persons-block> .

<#select-children> a ldh:Object ;
rdf:value ldh:ChildrenView .
# Custom children listing instead of the stock ldh:ChildrenView. LDH derives the
# view's filter pills and sort options from the BGP triples whose SUBJECT is the
# query's FIRST projected variable and whose OBJECT is a variable (see the
# "View filter/sort columns" note in CLAUDE.md) — so the focus variable has to be
# the person entity (?person = {doc}#this), not the document, because that is
# where foaf:givenName / foaf:familyName / foaf:gender / ltlod:nominatedBy sit.
# The stock ChildrenView projects the document (?child) and therefore offers no
# person columns at all.
#
# Filter pills need the ldh:bgp-value-counts override in files/client.xsl to be
# usable here: the label lookup LDH appends to the facet-value query uses an
# alternative property path, which ARQ does not index inside a GRAPH block (~104 s
# per pill over this dataspace's ~117k graphs). The override rewrites it to a
# variable predicate + FILTER IN, so a pill loads in ~0.1 s.

<#persons-block> a ldh:Object ;
rdf:value <#persons-view> .

<#persons-view> a ldh:View ;
dct:title "Seimo nariai"@lt, "Members of the Seimas"@en ;
spin:query <#persons-query> ;
ac:mode ac:GridMode .

# dct:title (not just rdfs:label) is mandatory on sp:Select — LDH enforces
# def:MissingTitle as a SPIN constraint and rejects the PUT with 422 otherwise.
<#persons-query> a sp:Select ;
dct:title "Seimo nariai"@lt, "Members of the Seimas"@en ;
sp:text """
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
PREFIX sioc: <http://rdfs.org/sioc/ns#>
PREFIX ltlod: <http://linkeddata.lt/ns#>

SELECT DISTINCT ?person
WHERE
{ GRAPH ?graph
{ ?doc sioc:has_container $this ;
foaf:primaryTopic ?person .
?person a foaf:Person ;
foaf:givenName ?givenName ;
foaf:familyName ?familyName ;
foaf:gender ?gender ;
ltlod:nominatedBy ?party
}
}
ORDER BY ?familyName ?givenName
""" .
Loading
Loading