diff --git a/.gitignore b/.gitignore index 3ef414f..9082b2f 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ files/client.xsl.sef.json /uploads/ /datasets/owner/ /datasets/secretary/ +docker-compose.override.yml diff --git a/CLAUDE.md b/CLAUDE.md index 725645b..9c716f1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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): ` @@ -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 @@ -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. @@ -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 ``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 `
`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. diff --git a/README.md b/README.md index a66c2eb..0257656 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/app/ns.ttl b/app/ns.ttl index 211abf0..7ca9116 100644 --- a/app/ns.ttl +++ b/app/ns.ttl @@ -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: . @@ -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 ; @@ -379,19 +399,25 @@ ORDER BY ?org :SelectCurrentMembers a sp:Select ; rdfs:label "Select current members" ; sp:text """ -PREFIX org: -PREFIX time: +PREFIX foaf: +PREFIX ltlod: +PREFIX org: +PREFIX 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 : . @@ -404,17 +430,24 @@ ORDER BY ?person :SelectFormerMembers a sp:Select ; rdfs:label "Select former members" ; sp:text """ -PREFIX org: -PREFIX time: +PREFIX foaf: +PREFIX ltlod: +PREFIX org: +PREFIX 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 : . @@ -446,14 +479,19 @@ ORDER BY ?unit :SelectNominatedMembers a sp:Select ; rdfs:label "Select persons nominated by party" ; sp:text """ +PREFIX foaf: PREFIX ltlod: 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 : . @@ -469,16 +507,22 @@ ORDER BY ?person :SelectMembersElectedHere a sp:Select ; rdfs:label "Select members elected in this district" ; sp:text """ -PREFIX org: +PREFIX foaf: PREFIX ltlod: +PREFIX 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 : . diff --git a/app/persons.ttl b/app/persons.ttl index 2d9f988..b12b71e 100644 --- a/app/persons.ttl +++ b/app/persons.ttl @@ -1,11 +1,58 @@ -@prefix dh: . -@prefix ldh: . -@prefix rdf: . -@prefix dct: . +@prefix dh: . +@prefix ldh: . +@prefix ac: . +@prefix rdf: . +@prefix dct: . +@prefix sp: . +@prefix 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: +PREFIX sioc: +PREFIX ltlod: + +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 +""" . diff --git a/app/root.ttl b/app/root.ttl index bc7518d..a56a43c 100644 --- a/app/root.ttl +++ b/app/root.ttl @@ -8,7 +8,7 @@ <> a def:Root ; dct:title "Lietuvos susietieji atvirieji duomenys"@lt ; - dct:description "Lietuvos atvirieji duomenys, sujungti į vientisą RDF žinių grafą: administraciniai vienetai, Seimo nariai, juridiniai asmenys, klasifikatoriai."@lt ; + dct:description "Oficialių Lietuvos registrų duomenys vienoje vietoje: administraciniai vienetai, Seimo nariai, juridiniai asmenys, klasifikatoriai."@lt ; rdf:_1 <#intro> ; rdf:_2 <#counties-intro> ; rdf:_3 <#counties-block> ; @@ -26,8 +26,8 @@ <#intro> a ldh:XHTML ; rdf:value """
-

Lietuvos atvirieji duomenys iš oficialių registrų, sujungti į vientisą, standartais grįstą RDF žinių grafą — kiekvienas objektas turi pasaulinį URI, tad rinkiniai susijungia savaime ir yra susieti su Wikidata bei ES žodynais.

-

Visą grafą galima užklausti SPARQL kalba; pavyzdinės užklausos su rezultatais — etl/queries/EXAMPLES.md. Žemiau — keli pjūviai iš gyvo grafo; konteineriai su visais rinkiniais — puslapio apačioje.

+

Oficialių Lietuvos registrų duomenys vienoje vietoje: administraciniai vienetai, Seimo nariai, partijos, juridiniai asmenys ir klasifikatoriai. Duomenys susieti tarpusavyje, tad nuo vieno objekto galite pereiti prie visų su juo susijusių — nuo apskrities prie savivaldybių, nuo Seimo nario prie jo komitetų ir partijos.

+

Žemiau — keli pjūviai iš duomenų: apskritys žemėlapyje, Seimo frakcijos, partijos ir komitetų pirmininkai. Visi duomenų rinkiniai — puslapio apačioje.

"""^^rdf:XMLLiteral . # rdf:_2 / rdf:_3 — top-level administrative units (counties) as a grid of coats of arms @@ -84,7 +84,7 @@ ORDER BY ?label""" . <#chart-factions-intro> a ldh:XHTML ; rdf:value """

Frakcijų dydžiai

-

Dabartinių Seimo frakcijų sudėtis pagal narių skaičių. Rodomi tik dabartiniai nariai — tie, kurių narystė dar neturi pabaigos datos.

+

Kiek narių šiuo metu turi kiekviena Seimo frakcija. Skaičiuojami tik dabartiniai frakcijų nariai.

"""^^rdf:XMLLiteral . <#chart-factions-block> a ldh:Object ; @@ -202,7 +202,7 @@ ORDER BY ?person""" . <#browse-intro> a ldh:XHTML ; rdf:value """

Naršyti duomenų rinkinius

-

Visi rinkiniai — administraciniai vienetai, asmenys, organizacijos, partijos, juridiniai asmenys, gatvės ir taksonomijos — pasiekiami per konteinerius žemiau.

+

Visi rinkiniai — administraciniai vienetai, asmenys, organizacijos, partijos, juridiniai asmenys, gatvės ir klasifikatoriai. Pasirinkite rinkinį žemiau ir naršykite jo įrašus.

"""^^rdf:XMLLiteral . <#select-children> a ldh:Object ; diff --git a/files/client.xsl b/files/client.xsl index aafef6c..441971b 100644 --- a/files/client.xsl +++ b/files/client.xsl @@ -30,6 +30,7 @@ xmlns:ldh="&ldh;" xmlns:ac="∾" xmlns:rdf="&rdf;" + xmlns:json="http://www.w3.org/2005/xpath-functions" xmlns:org="&org;" xmlns:time="&time;" xmlns:bs2="http://graphity.org/xsl/bootstrap/2.3.2" @@ -117,4 +118,126 @@ ORDER BY ?title + + + + + + + + + + + + + + + optional + + + union + + + + bgp + + + + + + + + + + + graph + + + bgp + + + + + + + + + + ? + + + + + + filter + + operation + in + + ? + + + + + + + + + + + + + + + + + + + + + ? + ? + ? + + + diff --git a/files/layout.xsl b/files/layout.xsl index 78e8efc..02b494d 100644 --- a/files/layout.xsl +++ b/files/layout.xsl @@ -3,13 +3,18 @@ Server-side layout override for the LTLOD end-user app. - Two jobs: + Three jobs: 1. repoint the client-side XSLT bootstrap at our custom Saxon-JS stylesheet (files/client.xsl → client.xsl.sef.json) instead of the stock one (xhtml:Script below); 2. import the shared files/overrides.xsl so the membership plumbing blocks are suppressed in the SERVER render too — otherwise the server emits them - and the client removes them during CSR, causing a visible flash. + and the client removes them during CSR, causing a visible flash; + 3. replace the stock LinkedDataHub footer (bs2:Footer) with the LTLOD one — + our own wordmark, dataset shortcuts, source attribution and licence. + The footer is server-rendered ONLY (layout.xsl applies bs2:Footer once, + client.xsl never re-renders it), so it lives here and not in + overrides.xsl — no SEF rebuild needed after editing it. Imports the base layout.xsl and overrides only the xhtml:Script template's client-stylesheet parameter. @@ -23,14 +28,18 @@ --> + ]> @@ -46,4 +55,76 @@ + + + + + +