Gyldige kryssløp det kan søkes på i et gitt tidspunkt (et litt vrient SPARQL case) - Utdanningsdirektoratet/Grep_SPARQL GitHub Wiki

Bakgrunn

I forkant av hver vår får vi spørsmål om hvilke gyldige kryssløp elever, lærlinger og praksiskandidater kan søke på, enten fra et vg1- eller vg2-programområde til vg3. Like ofte som spørsmålet kommer, skaper dette litt utfordringer for oss som skal hjelpe til. Beskrivelsen nedenfor er ment som en "note to self" og en dokumentasjon vi kan falle tilbake til neste gang spørsmålet kommer. Hvorfor dette viser seg å være en liten SPARQL-nøtt, kommer vi tilbake til nede i teksten.

Først en liten oversikt over behovet

I visningen av et gitt utdanningsprogram på udir.no, gis brukeren mulighet til å vise hvilke kryssløp som er mulig. Se bildeeksemplene nedenfor:

image Her ser vi litt nede på siden til utdanningsprogrammet BA, Bygg- og anleggsteknikk, og ser blant annet at vi kan krysse av for å vise kryssløp for f.eks. BATRT2----, vg2 Treteknikk. Bildet nedenfor viser hva som skjer når vi haker av for å vise kryssløp: image Her ser vi at en fra utdanningsprogrammet ST, vg1 Studiespesialisering kan søke seg til vg2 BATRT2----, Treteknikk (det skal vi filtrere bort i vårt eksempel), men vi ser også at vi kan søke oss over fra BATRT2----, vg2 Treteknikk til TPISN3----, Industrisnekkerfaget som ligger i utdanningsprogrammet TP, Teknologi- og industrifag.

I eksempelet nedenfor ser vi at vi kan søke oss fra et vg1 yrkesfag til et vg3 særløp i et annet utdanningsprogram. De som søker om dette i skrivende stund (våren 2023) vil kun finne ett eksempel på dette, men vi tar det med for å ikke misse det. image Her kan vi se at en som tar BABAT1----, vg1 Bygg- og anleggsteknikk fra utdanningsprogrammet BA, Bygg- og anleggsteknikk, kan søke om å krysse over til programområdet DTGIP3----, Gipsmakerfaget (særløp) som ligger i utdanningsprogrammet DT, Håndverk, design og produktutvikling. I slike tilfeller avviker visningen på udir.no på den måten at avkrysningsboksen for å vise kryssløp vises på hva en kan krysse til (og ikke fra).

Husk at vi øverst på utdanningsprogram-siden, må velge "Skoleår" og "Trinn/år" for å finne gyldige kryssløp for det vi vil søke om: image

Vår SPARQL-case

Visningssidene på udir.no ovenfor er basert på data fra json/REST-apiet til Grep, men på grunn av grunnleggende forskjeller på json/REST og RDF/SPARQL, vil det å vise en liste over gyldige kryssløp by på noen utfordringer. En av årsakene er at gyldighetsinformasjonen (gyldighetsdatoer) til koblingen mellom programområdene som er involvert i et kryssløp er skjult bak blanke noder (pga. flat datastruktur i SPARQL). For å kunne forstå løsningsforslaget vi beskriver nedenfor, foreslår jeg at du først leser den egne artikkelen som beskriver blanke-noder-for-gyldighetsinformasjon-i-referanseobjekter der vi beskriver denne utfordringen mer i detalj.

Én ting er å kunne takle blanke noder i seg selv, men i vårt tilfelle viser det seg at ikke alle de involverte programområdene har noen gyldighetsinformasjon for koblingen mellom dem. Egenskapen gyldighet-bygger-paa-programomraade-[programområdekode] vises ikke i datagrunnlaget for de som ikke har denne informasjonen (vi opererer ikke med "null"-verdier i SPARQL, og det er det flere grunner til). Derfor må vi ty til flere OPTIONAL i spørringen (også nøstede (OPTIONAL i OPTIONAL)).

I tillegg er det slik at egenskapen gyldighet-bygger-paa-programomraade-[programområdekode] ikke har noen informasjon som sier om dette er et kryssløp eller ikke. Egenskapen bygger-paa-programomraade i seg selv kan både gi programområder i et "normalt løp", men også kryssløp. Da må vi også ta hensyn til egenskapen loepstype-kryssloep som gir kobling til et programområde som faktisk er et kryssløp.

For å kun vise gyldighet-bygger-paa-programomraade-[programområdekode] der [programområdekode] er et kryssløp, må vi derfor sammenligne den fraksjonen av gyldighet-bygger-paa-programomraade-[programområdekode] som faktisk er en programområdekode med loepstype-kryssloep sin programområdekode. Se FILTER (?gyldighetKryssloep = ?kryss) i spørringen nedenfor. Derfor har vi i spørringen valgt å "gjøre om" gyldighet-bygger-paa-programomraade-[programområdekode] til variabelen ?gyldighetKryssloep ved hjelp av konkatinering. På den måten får vi ut kun programområdekoden, som vi altså binder til ?gyldighetKryssloep (som altså er skrevet om til en ren URI) som vi så kan sammenligne med ?kryss som vi tidligere har bundet til loepstype-kryssloep (?po u:loepstype-kryssloep ?kryss).

Spørringen

Vi håper beskrivelsen over er en lesehjelp til å forstå løsningsforlaget nedenfor. Ikke nøl med å gi oss forslag til forbedringer og forenklinger, både når det gjelder beskrivelse og spørringen i seg selv.

# Gyldige kryssløp som kan søkes våren 2023 (se datofiltere i linje 73 og 74)
# ?poFraKode = det programområdet som er oppført som kryssløp i ?poTilKode
# ?fraFsem = ?poFraKode sitt første semester
# ?tilFsem = ?poTilKode sitt første semester
# ?gyldigFra "9999-12-31T00:00:00" = uten gyldig-fra-verdi (m.a.o: koblingen til kryssløpet er gyldig "fra tidenes morgen")
# ?gyldigTil "9999-12-31T00:00:00" = uten gyldig-til-verdi (m.a.o: koblingen til kryssløpet er foreløpig gyldig til evt dato er satt)
PREFIX d: <http://psi.udir.no/kl06/>
PREFIX u: <http://psi.udir.no/ontologi/kl06/>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
SELECT DISTINCT ?poFraKode ?poFraTittel ?fraFsem ?poTilKode ?poTilTittel ?tilFsem ?gyldigFra ?gyldigTil ?tilleggsopplTil ?typeKryssloep
# ?fraSsem og ?tilSsem er tatt bort fra SELECT fordi det ikke var forekomster i utvalget
where {
    BIND  (NOW() AS ?detteAar)
	?po a u:programomraade ;
        u:utdanningsprogram-referanse ?upTil ;
        u:kode ?poTilKode ;
        u:foerste-semester ?tilFsem ;
        u:tittel ?poTilTittel ;
        u:loepstype-kryssloep ?kryss .
    OPTIONAL {
        ?po a u:programomraade ; 
            u:tilleggsopplysninger ?tilleggsopplTil .
          OPTIONAL  { ?po a u:programomraade ; u:siste-semester ?tilSsem .}
            FILTER (lang(?tilleggsopplTil) = "default")
    }
    FILTER (lang(?poTilTittel) = "default")
    ?kryss u:tittel ?poFraTittel ;
           u:utdanningsprogram-referanse ?upFra ;
           u:foerste-semester ?fraFsem ;
           u:kode ?poFraKode .
    OPTIONAL {
        ?kryss u:tilleggsopplysninger ?tilleggsopplFra ;
               u:siste-semester ?fraSsem .
        FILTER (lang(?tilleggsopplFra) = "default") }
    FILTER (lang(?poFraTittel) = "default")
    MINUS { ?po u:loepstype-kryssloep d:STUSP1---- . }
    OPTIONAL {
                ?po a u:programomraade ;
                u:loepstype-kryssloep ?kryss ;
                ?p ?o .
                ?kryss u:kode ?poFraKode .
# Jukser til så ?p (gyldighet-bygger-paa-programomraade-[programområdekode]) blir til en enkel PO-refgeranse
                BIND(STRAFTER(STRAFTER(STR(?p),"http://psi.udir.no/ontologi/kl06/gyldighet-bygger-paa-programomraade-") ,"") as ?gyldighetByggerPaaPO) 
                BIND(IF(?p = true, IRI(CONCAT("http://psi.udir.no/kl06/", ?gyldighetByggerPaaPO)),
                IRI(CONCAT("http://psi.udir.no/kl06/", ?gyldighetByggerPaaPO))) AS ?gyldighetKryssloep
                    )
# slutt på juks ;-)
                FILTER REGEX(str(?p), "gyldighet-bygger-paa-programomraade-")
                FILTER REGEX(str(?p), ?poFraKode)
                FILTER (?gyldighetKryssloep = ?kryss)

                OPTIONAL {?o u:gyldig-fra ?gf} 
                OPTIONAL {?o u:gyldig-til ?gt .}
    }

BIND (xsd:boolean(exists{
    ?po a u:programomraade ;
        ?p ?o .
    FILTER isBlank(?o)
    FILTER REGEX(str(?p), "gyldighet-bygger-paa-programomraade-")
    ?o u:gyldig-fra ?gf .
    }) AS ?gyldigFraTest)
BIND (IF(xsd:boolean(?gyldigFraTest) = "true"^^xsd:boolean, ?gf, "1000-01-01T00:00:00") AS ?gyldigFra)
    
BIND (xsd:boolean(exists{
    ?po a u:programomraade ;
        ?p ?o .
    FILTER isBlank(?o)
    FILTER REGEX(str(?p), "gyldighet-bygger-paa-programomraade-")
    ?o u:gyldig-til ?gt .
    }) AS ?gyldigTilTest)
BIND (IF(xsd:boolean(?gyldigTilTest) = "true"^^xsd:boolean, ?gt, "9999-12-31T00:00:00") AS ?gyldigTil)  
FILTER (xsd:dateTime(?gyldigFra) <= "2023-07-31T00:00:00"^^xsd:dateTime)   
FILTER (xsd:dateTime(?gyldigTil) >= "2023-08-01T00:00:00"^^xsd:dateTime)    

BIND (IF(xsd:boolean(?upFra = ?upTil) = "true"^^xsd:boolean, "internt", "eksternt") AS ?typeKryssloep)
}
ORDER BY ?poFraKode