diff --git a/Gruntfile.local.js b/Gruntfile.local.js index 25e62a59150..d8cf46b8e37 100644 --- a/Gruntfile.local.js +++ b/Gruntfile.local.js @@ -1,5 +1,6 @@ module.exports = function(grunt) { const fs = require("fs"); + const path = require("path"); const os = require("node:os"); grunt.registerTask("finna:scss", function finnaScssFunc() { @@ -20,24 +21,46 @@ module.exports = function(grunt) { }); grunt.registerTask("finna:lessToSass", function finnaLessToSassFunc() { + const exclude = grunt.option('exclude'); + const excludedFiles = exclude ? exclude.split(',') : []; + + const sharedFileOpts = { + expand: true, + src: ['*.less', '**/*.less'], + filter: function(filepath) { + return !excludedFiles.includes(filepath); + }, + ext: '.scss' + }; + + let themeDir = grunt.option('theme-dir'); + if (themeDir) { + themeDir = path.resolve(themeDir); + } + const files = themeDir + ? [ + { + ...sharedFileOpts, + cwd: themeDir + '/less', + dest: themeDir + '/scss' + } + ] + : [ + { + ...sharedFileOpts, + cwd: 'themes/finna2/less', + dest: 'themes/finna2/scss' + }, + { + ...sharedFileOpts, + cwd: 'themes/custom/less', + dest: 'themes/custom/scss' + }, + ]; + console.log(themeDir ? "Converting theme " + themeDir : "Converting Finna default themes"); grunt.config.set('lessToSass', { convert: { - files: [ - { - expand: true, - cwd: 'themes/finna2/less', - src: ['*.less', 'components/**/*.less', 'finna/**/*.less', 'global/**/*.less'], - ext: '.scss', - dest: 'themes/finna2/scss' - }, - { - expand: true, - cwd: 'themes/custom/less', - src: ['*.less', 'components/**/*.less', 'finna/**/*.less', 'global/**/*.less'], - ext: '.scss', - dest: 'themes/custom/scss' - }, - ], + files: files, options: { replacements: [ // Activate SCSS diff --git a/config/vufind/OAuth2Server.yaml b/config/vufind/OAuth2Server.yaml index 87f65837197..67ca2da3009 100644 --- a/config/vufind/OAuth2Server.yaml +++ b/config/vufind/OAuth2Server.yaml @@ -59,6 +59,11 @@ Clients: # Note that a secret can only be used with confidential clients since public # ones have no way of using it securely. secret: "" + # By default a client can request all scopes. This setting can be used to limit + # the allowed scopes to a subset of all available ones. + #allowedScopes: + # - openid + # - email # Scope configuration. Keys are scope identifiers. Each identifier should include a # description (translation key) to be displayed to the user. The ils field should be diff --git a/config/vufind/config.ini b/config/vufind/config.ini index 6dc6cfa4b75..409dd7f7991 100644 --- a/config/vufind/config.ini +++ b/config/vufind/config.ini @@ -312,6 +312,12 @@ driver = Sample ; library_cards setting (see below). allowUserLogin = true +; If true (default), any ILS credentials stored with the user are checked up-front +; when the user logs in to VuFind. If the credentials don't work, they're cleared, +; and the user will be prompted for new credentials when accessing a page that +; needs them. This setting is ignored if allowUserLogin is false. +checkILSCredentialsOnLogin = true + ; loadNoILSOnFailure - Whether or not to load the NoILS driver if the main driver fails loadNoILSOnFailure = false diff --git a/local/config/vufind/rss.ini.sample b/local/config/vufind/rss.ini.sample index 56d078152ee..5c822812816 100644 --- a/local/config/vufind/rss.ini.sample +++ b/local/config/vufind/rss.ini.sample @@ -23,7 +23,7 @@ ; up to items are displayed. In grid visualItems determines the amount of items ; displayed before a "more" button. "More" button displays up to items in the grid. ; title = "[rss|]" Feed title: -; 'rss' = Use element from feed +; 'rss' = Use <title> element from feed [DEPRECATED -- leave title empty to use title from feed, or use a <translation-key>] ; '<translation-key>' = Use local translation ; description = "Feed description|<translation-key>" ; A free-form description text that may include HTML, or a translation key. Also diff --git a/local/languages/finna/classification/en-gb.ini b/local/languages/finna/classification/en-gb.ini index 66baeaf6a40..2ed50f33eef 100644 --- a/local/languages/finna/classification/en-gb.ini +++ b/local/languages/finna/classification/en-gb.ini @@ -4271,3 +4271,158 @@ udk2 930.2 = "Methodology of history. Ancillary historical sciences" udk2 930.25 = "Archivistics. Archives (including public and other records)" udk2 930.85 = "History of civilization. Cultural history" udk2 94 = "General history" + +;classes loovs + +loovs 0 = "Løøv classification" +loovs 1 = "Book and library research" +loovs 01a = "General bibliographies" +loovs 01b = "Bibliographies of individuals" +loovs 01c = "Library catalogues" +loovs 01d = "Books and libraries in general" +loovs 01e = "Book reviews" +loovs 01f = "Archives" +loovs 2 = "Comprehensive works and works of miscellaneous contents" +loovs 02a = "General surveys and handbooks" +loovs 02b = "Non-specialized periodicals" +loovs 02c = "Works of miscellaneous content" +loovs 02d = "Research methodology" +loovs 3 = "Sami institutions and organisations" +loovs 03a = "Institutions and agencies" +loovs 03b = "Organizations and associations" +loovs 03c = "Conferences and meetings" +loovs 4 = "Religion, philosophy, psychology" +loovs 04a = "Pre-Christian religions" +loovs 04b = "Church and mission history" +loovs 04c = "Congregations" +loovs 04d = "Læstadianism" +loovs 04e = "Philosophy and psychology" +loovs 5 = "Religious literature" +loovs 05a = "The Bible and biblical history" +loovs 05b = "Psalms and hymns" +loovs 05c = "Other works" +loovs 6 = "Philology, Language" +loovs 06a = "Language in society" +loovs 06a1 = "Teaching and studying languages" +loovs 06b = "Linguistics" +loovs 06b1 = "Language history and etymology" +loovs 06b1a = "Name research" +loovs 06b2 = "Dialects" +loovs 06b3 = "Other research" +loovs 06c = "Grammar and language revitalisation" +loovs 06d = "Dictionaries" +loovs 06e = "Textbooks and readers" +loovs 7 = "Medicine, physical anthropology" +loovs 07a = "Medical sciences" +loovs 07b = "Physical anthropology" +loovs 8 = "Geography and local history" +loovs 08a = "General surveys" +loovs 08b = "Specific regions" +loovs 08c = "Travelogues" +loovs 9 = "History" +loovs 09a = "General surveys" +loovs 09b = "Archaeology" +loovs 09c = "History" +loovs 10 = "Personal history" +loovs 10a = "Collections of biographies" +loovs 10b = "Family history" +loovs 10c = "Biographies" +loovs 11 = "Cultural history" +loovs 11a = "General surveys" +loovs 11b = "Work and industries" +loovs 11b1 = "Hunting and trapping" +loovs 11b2 = "Fishing" +loovs 11b3 = "Reindeer nomadism" +loovs 11b4 = "Livestock and agriculture" +loovs 11b5 = "Other industries" +loovs 11b6 = "Commerce and markets" +loovs 11b7 = "Diet and cooking" +loovs 11c = "Material culture" +loovs 11c1 = "Housing and buildings" +loovs 11c2 = "Transportation" +loovs 11c3 = "Furniture, utensils, tools" +loovs 11c4 = "Handicraft" +loovs 11c5 = "Clothing" +loovs 11d = "Folk culture. Folklore" +loovs 11d1 = "Folk literature" +loovs 11d1a = "Tales and lore" +loovs 11d1b = "Rhymes and verse, proverbs and phrases, ballads" +loovs 11d2 = "Manners" +loovs 11d3 = "Folklore, folk belief" +loovs 11d4 = "Folk medicine" +loovs 11e = "Social organisation" +loovs 110 = "Family and kinship" +loovs 1100 = "Justice and moral" +loovs 11f = "Museums and exhibitions" +loovs 11f1 = "Preservation" +loovs 12 = "Industry and economy" +loovs 12a = "General surveys" +loovs 12b = "Reindeer husbandry" +loovs 12c = "Agriculture and livestock" +loovs 12d = "Hunting" +loovs 12e = "Fishing" +loovs 12f = "Handicraft" +loovs 12g = "Other trades" +loovs 12h = "Business administration" +loovs 12i = "Home economics" +loovs 13 = "Minorities, Conditions and rights" +loovs 13a = "General surveys" +loovs 13b = "Exploitation within the Sami regions" +loovs 13b1 = "Forestry" +loovs 13b2 = "Mining" +loovs 13b3 = "Watercourse regulations, development of water power" +loovs 13b4 = "Tourism / outdoor life" +loovs 13b5 = "Other exploitation" +loovs 13c = "Minority conditions, minority policy" +loovs 13c1 = "Relations to the Government" +loovs 13c2 = "Relations to other ethnic groups" +loovs 13d = "Legal history and legal position" +loovs 14 = "Social conditions" +loovs 14a = "General surveys" +loovs 14b = "Social conditions, Social policy" +loovs 14c = "Administration and politics" +loovs 14d = "Communications" +loovs 14e = "Demography and statistics" +loovs 14f = "Women’s liberation. Women’s history" +loovs 15 = "SCHOOLS AND EDUCATION" +loovs 15a = "General surveys" +loovs 15b = "School history" +loovs 15c = "Kindergarten" +loovs 15d = "Primary school" +loovs 15e = "Secondary school" +loovs 15f = "Other education" +loovs 15g = "Universities and academies" +loovs 15h = "Adult education" +loovs 15i = "Specific subjects" +loovs 15j = "Pedagogics and upbringing" +loovs 15k = "Textbooks" +loovs 16 = "Art, music, theatre and film" +loovs 16a = "General surveys" +loovs 16b = "Creative arts" +loovs 16b1 = "Visual art" +loovs 16b2 = "Decorative arts" +loovs 16b3 = "Architecture" +loovs 16c = "Joik and music" +loovs 16d = "Theatre" +loovs 16e = "Film and photo" +loovs 17 = "Literary history" +loovs 18 = "Fiction" +loovs 18a = "Sami authors" +loovs 18a1 = "In Sami languages" +loovs 18a2 = "In other languages" +loovs 18b = "Non-Sami authors" +loovs 18b1 = "In Sami languages" +loovs 18b2 = "With Sami main motif" +loovs 18c = "Children’s and youth literature" +loovs 18c1 = "In Sami languages" +loovs 18c2 = "With Sami main motif" +loovs 19 = "Journalism and information operations" +loovs 19a = "General surveys" +loovs 19b = "Press" +loovs 19c = "Radio and TV" +loovs 19d = "Other information operations" +loovs 20 = "Sports, games, play and entertainment" +loovs 21 = "Natural sciences, Technical science" +loovs 21a = "Natural sciences" +loovs 21b = "Technical science" +loovs 21c = "Mathematics" diff --git a/local/languages/finna/classification/fi.ini b/local/languages/finna/classification/fi.ini index e8da55b1e11..9066c8df4f6 100644 --- a/local/languages/finna/classification/fi.ini +++ b/local/languages/finna/classification/fi.ini @@ -4749,3 +4749,158 @@ udk2 930.2 = "Historiantutkimuksen metodi. Historian aputieteet" udk2 930.25 = "Arkistot. Arkistotiede" udk2 930.85 = "Kulttuurihistoria" udk2 94 = "Yleinen historia" + +;classes loovs + +loovs 0 = "Löövin luokitus" +loovs 1 = "KIRJA- JA KIRJASTOALA" +loovs 01a = "Yleiset bibliografiat" +loovs 01b = "Henkilöbibliografiat" +loovs 01c = "Kirjastojen luettelot" +loovs 01d = "Kirjat ja kirjastot yleensä" +loovs 01e = "Kirja-arvostelut" +loovs 01f = "Arkistot" +loovs 2 = "KOKOOMATEOKSET JA ERI ALOJEN JULKAISUT" +loovs 02a = "Yleiskatsaukset ja käsikirjat" +loovs 02b = "Yleiset sarjajulkaisut" +loovs 02c = "Moniaiheiset teokset" +loovs 02d = "Tutkimusmenetelmät" +loovs 3 = "SAAMELAISET LAITOKSET JA ORGANISAATIOT" +loovs 03a = "Laitokset ja virastot" +loovs 03b = "Järjestöt ja yhdistykset" +loovs 03c = "Konferenssit ja kokoukset" +loovs 4 = "USKONTO, FILOSOFIA JA PSYKOLOGIA" +loovs 04a = "Esikristilliset uskonnot" +loovs 04b = "Kirkko- ja lähetyshistoria" +loovs 04c = "Seurakunnat" +loovs 04d = "Lestadiolaisuus" +loovs 04e = "Filosofia ja psykologia" +loovs 5 = "USKONNOLLINEN KIRJALLISUUS" +loovs 05a = "Raamattu ja raamatunhistoria" +loovs 05b = "Virret ja hengelliset laulut" +loovs 05c = "Muut teokset" +loovs 6 = "KIELITIEDE" +loovs 06a = "Kieli yhteiskunnassa" +loovs 06a1 = "Kielten opetus ja opiskelu" +loovs 06b = "Lingvistiikka" +loovs 06b1 = "Kielten historia ja etymologia" +loovs 06b1a = "Nimistöntutkimus" +loovs 06b2 = "Murteet" +loovs 06b3 = "Muu tutkimus" +loovs 06c = "Kielioppi ja kelen elvytys" +loovs 06d = "Sanakirjat" +loovs 06e = "Oppikirjat ja lukemistot" +loovs 7 = "LÄÄKETIEDE, FYYSINEN ANTROPOLOGIA" +loovs 07a = "Lääketiede" +loovs 07b = "Fyysinen antropologia" +loovs 8 = "MAANTIEDE JA PAIKALLISHISTORIA" +loovs 08a = "Yleiskatsaukset" +loovs 08b = "Yksittäiset alueet" +loovs 08c = "Matkakertomukset" +loovs 9 = "HISTORIA" +loovs 09a = "Yleiskatsaukset" +loovs 09b = "Arkeologia" +loovs 09c = "Historia" +loovs 10 = "HENKILÖHISTORIA" +loovs 10a = "Elämäkertakokoomateokset" +loovs 10b = "Sukuhistoriat" +loovs 10c = "Elämäkerrat" +loovs 11 = "KULTTUURIHISTORIA" +loovs 11a = "Yleiskatsaukset" +loovs 11b = "Työ ja elinkeinot" +loovs 11b1 = "Metsästys ja pyynti" +loovs 11b2 = "Kalastus" +loovs 11b3 = "Poropaimentolaisuus" +loovs 11b4 = "Karjatalous ja maanviljely" +loovs 11b5 = "Muut elinkeinot" +loovs 11b6 = "Kauppa ja markkinat" +loovs 11b7 = "Ravinto ja ruoanvalmistus" +loovs 11c = "Aineellinen kulttuuri" +loovs 11c1 = "Asunnot ja rakennukset" +loovs 11c2 = "Kuljetus" +loovs 11c3 = "Huonekalut, käyttöesineet, työkalut" +loovs 11c4 = "Käsityöt" +loovs 11c5 = "Vaatetus" +loovs 11d = "Henkinen kulttuuri, kansanperinne" +loovs 11d1 = "Kansanrunous" +loovs 11d1a = "Kertomukset ja perimätieto" +loovs 11d1b = "Riimit ja säkeet, sananlaskut, sanonnat ja balladit" +loovs 11d2 = "Tavat" +loovs 11d3 = "Kansanperinne, uskomukset" +loovs 11d4 = "Kansanlääkintä" +loovs 11e = "Sosiaaliset suhteet" +loovs 110 = "Perhe ja sukulaisuus" +loovs 1100 = "Oikeus ja moraaliset olot" +loovs 11f = "Museot ja näyttelyt" +loovs 11f1 = "Kulttuurihistorialliset suojelukohteet" +loovs 12 = "ELINKEINOT JA TALOUS" +loovs 12a = "Yleiskatsaukset" +loovs 12b = "Poronhoito" +loovs 12c = "Maanviljely ja karjatalous" +loovs 12d = "Metsästys" +loovs 12e = "Kalastus" +loovs 12f = "Käsityöt" +loovs 12g = "Muut elinkeinot" +loovs 12h = "Liiketalous" +loovs 12i = "Kotitalous" +loovs 13 = "VÄHEMMISTÖT JA OIKEUDET" +loovs 13a = "Yleiskatsaukset" +loovs 13b = "Hyväksikäyttö saamelaisalueella" +loovs 13b1 = "Metsätalous" +loovs 13b2 = "Kaivostoiminta" +loovs 13b3 = "Vesistöjen säännöstely, vesivoiman kehitys" +loovs 13b4 = "Matkailu / retkeily" +loovs 13b5 = "Muu hyväksikäyttö" +loovs 13c = "Vähemmistökansat, olosuhteet ja politiikka" +loovs 13c1 = "Suhteet viranomaisiin" +loovs 13c2 = "Suhteet muihin etnisiin ryhmiin" +loovs 13d = "Oikeushistoria ja oikeudellinen asema" +loovs 14 = "YHTEISKUNNALLISET OLOT" +loovs 14a = "Yleiskatsaukset" +loovs 14b = "Sosiaaliset olot, sosiaalipolitiikka" +loovs 14c = "Hallinto ja politiikka" +loovs 14d = "Viestintä" +loovs 14e = "Väestötiede ja tilastot" +loovs 14f = "Naistutkimus / naisen historia" +loovs 15 = "KOULUT JA KOULUTUS" +loovs 15a = "Yleiskatsaukset" +loovs 15b = "Kouluhistoria" +loovs 15c = "Esikoulu" +loovs 15d = "Peruskoulu" +loovs 15e = "Keskikoulu. Lukio" +loovs 15f = "Muu koulutus" +loovs 15g = "Yliopistot ja korkeakoulut" +loovs 15h = "Aikuiskoulutus" +loovs 15i = "Oppiaineet" +loovs 15j = "Pedagogiikka ja kasvatus" +loovs 15k = "Oppikirjat" +loovs 16 = "TAIDE, MUSIIKKI, TEATTERI, ELOKUVAT" +loovs 16a = "Yleiskatsaukset" +loovs 16b = "Muotoilutaide" +loovs 16b1 = "Kuvataiteet" +loovs 16b2 = "Taidekäsityöt" +loovs 16b3 = "Arkkitehtuuri" +loovs 16c = "Joiut ja musiikki" +loovs 16d = "Teatteri" +loovs 16e = "Elokuvat ja valokuvat" +loovs 17 = "KIRJALLISUUSHISTORIA" +loovs 18 = "KAUNOKIRJALLISUUS" +loovs 18a = "Saamelaiskirjailijat" +loovs 18a1 = "Saamenkieliset" +loovs 18a2 = "Muunkieliset" +loovs 18b = "Ei-saamelaiset kirjailijat" +loovs 18b1 = "Saamenkieliset" +loovs 18b2 = "Saamelaisaiheiset" +loovs 18c = "Lasten ja nuorten kirjallisuus" +loovs 18c1 = "Saamenkieliset" +loovs 18c2 = "Saamelaisaiheiset" +loovs 19 = "JOURNALISMI JA TIEDOTUS" +loovs 19a = "Yleiskatsaukset" +loovs 19b = "Lehdistö" +loovs 19c = "Radio ja TV" +loovs 19d = "Muu viestintätoiminta" +loovs 20 = "URHEILU, PELIT, LEIKIT JA VIIHDE" +loovs 21 = "LUONNONTIETEET. TEKNIIKKA" +loovs 21a = "Luonnontieteet" +loovs 21b = "Tekniset tieteet" +loovs 21c = "Matematiikka" diff --git a/local/languages/finna/classification/se.ini b/local/languages/finna/classification/se.ini index e865d91f272..56482600795 100644 --- a/local/languages/finna/classification/se.ini +++ b/local/languages/finna/classification/se.ini @@ -1,2 +1,154 @@ ;classes +loovs 0 = "Løøva klassifikašuvdna" +loovs 1 = "GIRJE- JA GIRJERÁJUSDOAIMMAT" +loovs 01a = "Oppalaš bibliografiat" +loovs 01b = "Ovttaskasolbmuid bibliografiat" +loovs 01c = "Girjerádjokatalogat" +loovs 01d = "Girjjiid ja girjerádjosiid birra dábáláččat" +loovs 01e = "Girjeárvvoštallamat" +loovs 01f = "Arkiivvat" +loovs 2 = "OPPALAŠGIRJJIT JA IEŠGUĐETLÁGÁN SISDOLOT GIRJJIT" +loovs 02a = "Oppalaš čilgehusat ja giehtagirjjit" +loovs 02b = "Oppalaš áigečallagat" +loovs 02c = "iešguđetlágán sisdolot girjjit" +loovs 02d = "Dutkanmetodihkka" +loovs 3 = "SÁMI ÁSAHUSAT JA ORGANISAŠUVNNAT" +loovs 03a = "Ásahusat" +loovs 03b = "Organisašuvnnat ja searvvit" +loovs 03c = "Konfearánssat ja čoahkkimat" +loovs 4 = "OSKKUT, FILOSOFIIJA, PSYKOLOGIIJA" +loovs 04a = "Ovdakristtalaš osku" +loovs 04b = "Girko- ja mišuvdnahistorjá" +loovs 04c = "Ovttaskas girkoservodagat" +loovs 04d = "Læstadianisma" +loovs 04e = "Filosofiija, psykologiija" +loovs 5 = "VUOIŊŊALAŠ GIRJJÁLAŠVUOHTA" +loovs 05a = "Biibbal ja biibbalhistorjá" +loovs 05b = "Sálmmat ja lávlagat" +loovs 05c = "Eará čállagat" +loovs 6 = "GIELLADIEĐA" +loovs 06a = "Giellagažaldat" +loovs 06a1 = "Giellaoahpaheapmi, giellaoahpahallan" +loovs 06b = "Gielladutkan" +loovs 06b1 = "Giellahistorjá ja etymologiija" +loovs 06b1a = "Nammadutkan" +loovs 06b2 = "Suopmanat" +loovs 06b3 = "Eará dutkamušat" +loovs 06c = "Giellaoahppa ja giellagáhtten" +loovs 06d = "Sátnegirjjit" +loovs 06e = "Oahppogirjjit ja lohkangirjjit" +loovs 7 = "DÁLKKASTUSDIEĐA, FYSÁLAŠ ANTROPOLOGIIJA" +loovs 07a = "Dálkkastusdieđa" +loovs 07b = "Fysálaš antropologiija" +loovs 8 = "EANADIEĐA JA BÁIKKÁLAŠ HISTORJÁ" +loovs 08a = "Oppalaš čilgehusat" +loovs 08b = "Guovllut" +loovs 08c = "Mátkemuitalusat" +loovs 9 = "HISTORJÁ" +loovs 09a = "Oppalaš čilgehusat" +loovs 09b = "Arkeologiija" +loovs 09c = "Historjá" +loovs 10 = "OLBMUID HISTORJÁ" +loovs 10a = "Biografiačoggosat" +loovs 10b = "Ovttaskas sogat" +loovs 10c = "Ovttaskas olbmot" +loovs 11 = "KULTURHISTORJÁ" +loovs 11a = "Oppalaš čilgehusat" +loovs 11b = "Bargu ja ealáhusat" +loovs 11b1 = "Meahccebivdu" +loovs 11b2 = "Guollebivdu" +loovs 11b3 = "Boazodoallu" +loovs 11b4 = "Šibit- ja eanadoallu" +loovs 11b5 = "Eará ealáhusat" +loovs 11b6 = "Gávpi ja márkanat" +loovs 11b7 = "Biebmodilli ja biebmodoallu" +loovs 11c = "Ávnnaskultuvra" +loovs 11c1 = "Ásodagat ja visttit" +loovs 11c2 = "Fievrrut" +loovs 11c3 = "Viessogálvvut, viessoneavvut, bargoneavvut" +loovs 11c4 = "Duodji" +loovs 11c5 = "Biktasat" +loovs 11d = "Vuoiŋŋalaš kultuvra ja álbmotmuitu" +loovs 11d1 = "Álbmotdikten" +loovs 11d1a = "Máidnasat ja cukcasat" +loovs 11d1b = "Hoahkamat ja sátnegárgadasat, sátnevádjasat ja dajaldagat, lávlagaččat" +loovs 11d2 = "Olmmošvierut" +loovs 11d3 = "Álbmotgáttut" +loovs 11d4 = "Álbmotdálkasat" +loovs 11e = "Sosiála oktavuođat" +loovs 110 = "Bearaš ja fuolkevuohta" +loovs 1100 = "Riekte- ja moráladilálašvuohta" +loovs 11f = "Dávvirvuorkkát ja čájáhusat" +loovs 11f1 = "Kulturmuitogáhtten" +loovs 12 = "EALÁHUSAT JA EKONOMIIJA" +loovs 12a = "Oppalaš čilgehusat" +loovs 12b = "Boazodoallu" +loovs 12c = "Eana- ja šibitdoallu" +loovs 12d = "Meahccebivdu" +loovs 12e = "Guollebivdu" +loovs 12f = "Duodji" +loovs 12g = "Eará ealáhusat" +loovs 12h = "Fitnodatekonomiija" +loovs 12i = "Dállodoallu ja dan ekonomiija" +loovs 13 = "UNNITLOHKOÁLBMOGAT. DILLI JA VUOIGATVUOĐAT" +loovs 13a = "Oppalaš čilgehusat" +loovs 13b = "Ávkkástallan sámi guovlluin" +loovs 13b1 = "Vuovdedoallu" +loovs 13b2 = "Ruvkedoaibma" +loovs 13b3 = "Čázádatásahallan / fápmorusttegiid huksen" +loovs 13b4 = "Turisma ja olgoáibmodoaibma" +loovs 13b5 = "Eará ávkkástallan" +loovs 13c = "Unnitlohkoálbmogat / dilli ja politihkka" +loovs 13c1 = "Gaskavuođat eiseváldiiguin" +loovs 13c2 = "Gaskavuođat eará olmmoščearddaidde" +loovs 13d = "Vuoigatvuođat / vuoigatvuođahistorjá" +loovs 14 = "SERVVODATDILLI" +loovs 14a = "Oppalaš čilgehusat" +loovs 14b = "Sosiálalaš dilit / sosiála politihkka" +loovs 14c = "Hálddáhus ja politihkka" +loovs 14d = "Johtolagat" +loovs 14e = "Ássiidválddahallan ja statistihkka" +loovs 14f = "Nissonáššit / nissonhistorjá" +loovs 15 = "SKUVLA JA OAHPPU" +loovs 15a = "Oppalaš čilgehusat" +loovs 15b = "Skuvlahistorjá" +loovs 15c = "Ovdaskuvla" +loovs 15d = "Vuođđoskuvla" +loovs 15e = "Joatkaskuvla" +loovs 15f = "Eará oahppu" +loovs 15g = "Universiteahta ja allaskuvla" +loovs 15h = "Rávesolbmuidoahpahus" +loovs 15i = "Ovttaskas oahpposuorggit" +loovs 15j = "Pedagogihkka ja bajásgeassin" +loovs 15k = "Oahppogirjjit" +loovs 16 = "DÁIDDA, MUSIHKKA, TEAHTER, FILBMA" +loovs 16a = "Oppalaš čilgehusat" +loovs 16b = "Hábmendáidda" +loovs 16b1 = "Govvadáidda" +loovs 16b2 = "Dáiddaduodji" +loovs 16b3 = "Arkitektuvra" +loovs 16c = "Juoigan/musihkka" +loovs 16d = "Teahter" +loovs 16e = "Filbma ja govat" +loovs 17 = "GIRJJÁLAŠVUOĐAHISTORJÁ" +loovs 18 = "ČÁPPAGIRJJÁLAŠVUOHTA" +loovs 18a = "Sámi čállit" +loovs 18a1 = "Sámegillii" +loovs 18a2 = "Eará gielaide" +loovs 18b = "Eai-sámi čállit" +loovs 18b1 = "Sámegillii" +loovs 18b2 = "Sámi váldofáttain" +loovs 18c = "Mánáid- ja nuoraidgirjjit" +loovs 18c1 = "Sámegillii" +loovs 18c2 = "Sámi váldofáttain" +loovs 19 = "JOURNALISTIHKKA JA DIEĐÁHUSDOAIMMAT" +loovs 19a = "Oppalaš čilgehusat" +loovs 19b = "Preantamediat" +loovs 19c = "Radio ja TV" +loovs 19d = "Eará dieđáhusdoaimmat" +loovs 20 = "VALAŠTALLAN, DUHKORADDAMAT, SPEALUT, ÁIGEGOLUT" +loovs 21 = "LUONDDUDIEĐA. TEKNIHKKA" +loovs 21a = "Luonddudieđa" +loovs 21b = "Teknihkka" +loovs 21c = "Matematihkka" diff --git a/local/languages/finna/classification/sv.ini b/local/languages/finna/classification/sv.ini index 2b5807e5242..f69b71651b7 100644 --- a/local/languages/finna/classification/sv.ini +++ b/local/languages/finna/classification/sv.ini @@ -4271,3 +4271,158 @@ udk2 930.2 = "Historiens metodlära. Historiens hjälpvetenskaper" udk2 930.25 = "Arkivlära. Arkiv (inklusive offentliga och andra register)" udk2 930.85 = "Civilisationens historia. Kulturhistoria" udk2 94 = "Allmän historia" + +;classes loovs + +loovs 0 = "Løøv-klassifikation" +loovs 1 = "BOK OCH BIBLIOTEKSVÄSEN" +loovs 01a = "Generella bibliografier" +loovs 01b = "Bibliografier över särskilda personer" +loovs 01c = "Bibliotekskataloger" +loovs 01d = "Allmänt om böcker och bibliotek" +loovs 01e = "Recensioner" +loovs 01f = "Arkiv" +loovs 2 = "ÖVERSIKTSVERK OCH SKRIFTER AV BLANDAT INNEHÅLL" +loovs 02a = "Allmänna översiktsverk och handböcker" +loovs 02b = "Allmänna periodiska publikationer" +loovs 02c = "Skrifter av blandat innehåll" +loovs 02d = "Forskningsmetodik" +loovs 3 = "SAMISKA INSTITUTIONER OCH ORGANISATIONER" +loovs 03a = "Institutioner och organ" +loovs 03b = "Organisationer och föreningar" +loovs 03c = "Konferenser och möten" +loovs 4 = "RELIGION, FILOSOFI OCH PSYKOLOGI" +loovs 04a = "Förkristen tro" +loovs 04b = "Kyrko- och missionshistoria" +loovs 04c = "Särskilda kyrkoförsamlingar" +loovs 04d = "Laestadianism" +loovs 04e = "Filosofi, psykologi" +loovs 5 = "RELIGIÖS LITTERATUR" +loovs 05a = "Bibeln / bibelhistoria" +loovs 05b = "Psalmer och sånger" +loovs 05c = "Andra skrifter" +loovs 6 = "SPRÅKVETENSKAP" +loovs 06a = "Språkfrågor" +loovs 06a1 = "Språkundervisning, språkstudier" +loovs 06b = "Språkforskning" +loovs 06b1 = "Språkhistoria och etymologi" +loovs 06b1a = "Namnforskning" +loovs 06b2 = "Dialekter" +loovs 06b3 = "Annan forskning" +loovs 06c = "Grammatik och språkvård" +loovs 06d = "Ordböcker" +loovs 06e = "Läroböcker och läseböcker" +loovs 7 = "MEDICIN, FYSISK ANTROPOLOGI" +loovs 07a = "Medicin" +loovs 07b = "Fysisk antropologi" +loovs 8 = "GEOGRAFI OCH LOKALHISTORIA" +loovs 08a = "Allmänna översiktsverk" +loovs 08b = "Särskilda områden" +loovs 08c = "Reseskildringar" +loovs 9 = "HISTORIA" +loovs 09a = "Allmänna översiktsverk" +loovs 09b = "Arkeologi" +loovs 09c = "Historia" +loovs 10 = "PERSONHISTORIA" +loovs 10a = "Samlingsbiografier" +loovs 10b = "Särskilda släkter" +loovs 10c = "Särskilda personer" +loovs 11 = "KULTURHISTORIA" +loovs 11a = "Allmänna översiktsverk" +loovs 11b = "Arbets- och näringsliv" +loovs 11b1 = "Jakt och fångst" +loovs 11b2 = "Fiske" +loovs 11b3 = "Rennomadism" +loovs 11b4 = "Jordbruk och kreatursskötsel" +loovs 11b5 = "Andra näringar" +loovs 11b6 = "Handel och marknader" +loovs 11b7 = "Kost och matlagning" +loovs 11c = "Materiell kultur" +loovs 11c1 = "Bostäder / byggnader" +loovs 11c2 = "Transportmedel" +loovs 11c3 = "Möbler, husgeråd, redskap etc." +loovs 11c4 = "Hemslöjd och hantverk" +loovs 11c5 = "Kläder" +loovs 11d = "Andlig kultur / folkminnen" +loovs 11d1 = "Folkdiktning" +loovs 11d1a = "Sagor och sägner" +loovs 11d1b = "Rim, ramsor, ordspråk, talesätt, visor etc." +loovs 11d2 = "Seder och bruk" +loovs 11d3 = "Folktro" +loovs 11d4 = "Folkmedicin" +loovs 11e = "Social organisation" +loovs 110 = "Familje- och släktskap" +loovs 1100 = "Rätts- och moralförhållanden" +loovs 11f = "Museer och utställningar" +loovs 11f1 = "Kulturminnesvård" +loovs 12 = "NÄRINGSLIV OCH EKONOMI" +loovs 12a = "Allmänna översiktsverk" +loovs 12b = "Renskötsel / rennäring" +loovs 12c = "Jordbruk och kreatursskötsel" +loovs 12d = "Jakt" +loovs 12e = "Fiske" +loovs 12f = "Hemslöjd och hantverk" +loovs 12g = "Andra näringar" +loovs 12h = "Företagsekonomi" +loovs 12i = "Privatekonomi / hushåll" +loovs 13 = "MINORITETS- OCH RÄTTSFÖRHÅLLANDEN" +loovs 13a = "Allmänna översiktsverk" +loovs 13b = "Exploatering inom det samiska området" +loovs 13b1 = "Skogsbruk" +loovs 13b2 = "Gruvdrift" +loovs 13b3 = "Vattenkraftsutbyggnad / vattenreglering" +loovs 13b4 = "Turism / friluftsliv" +loovs 13b5 = "Andra ingrepp" +loovs 13c = "Minoritetsförhållanden / minoritetspolitik" +loovs 13c1 = "Förhållandet till myndigheter" +loovs 13c2 = "Förhållandet till andra folkgrupper" +loovs 13d = "Rättsförhållanden / rättshistoria" +loovs 14 = "SAMHÄLLSFÖRHÅLLANDEN" +loovs 14a = "Allmänna översiktsverk" +loovs 14b = "Sociala förhållanden / socialpolitik" +loovs 14c = "Förvaltning och politik" +loovs 14d = "Kommunikationer" +loovs 14e = "Demografi och statistik" +loovs 14f = "Kvinnofrågor / kvinnohistoria" +loovs 15 = "SKOLA OCH UTBILDNING" +loovs 15a = "Allmänna översiktsverk" +loovs 15b = "Skolhistoria" +loovs 15c = "Förskola" +loovs 15d = "Grundskola" +loovs 15e = "Gymnasieskola" +loovs 15f = "Annan utbildning" +loovs 15g = "Universitet och högskola" +loovs 15h = "Vuxenutbildning" +loovs 15i = "Särskilda ämnen" +loovs 15j = "Pedagogik / uppfostran" +loovs 15k = "Skolböcker" +loovs 16 = "KONST, MUSIK, TEATER OCH FILM" +loovs 16a = "Allmänna översiktsverk" +loovs 16b = "Formkonst" +loovs 16b1 = "Bildkonst" +loovs 16b2 = "Konsthantverk" +loovs 16b3 = "Arkitektur" +loovs 16c = "Jojk/musik" +loovs 16d = "Teater" +loovs 16e = "Film/foto" +loovs 17 = "LITTERATURHISTORIA" +loovs 18 = "SKÖNLITTERATUR" +loovs 18a = "Samiska författare" +loovs 18a1 = "På samiska" +loovs 18a2 = "På andra språk" +loovs 18b = "Icke-samiska författare" +loovs 18b1 = "På samiska" +loovs 18b2 = "Med samiskt huvudmotiv" +loovs 18c = "Barn- och ungdomslitteratur" +loovs 18c1 = "På samiska" +loovs 18c2 = "Med samiskt huvudmotiv" +loovs 19 = "JOURNALISTIK OCH INFORMATIONSVERKSAMHET" +loovs 19a = "Allmänna översiktsverk" +loovs 19b = "Tidningar och tidskrifter" +loovs 19c = "Radio och television" +loovs 19d = "Annan informationsverksamhet" +loovs 20 = "IDROTT, SPEL, LEK OCH UNDERHÅLLNING" +loovs 21 = "NATURVETENSKAP OCH TEKNIK" +loovs 21a = "Naturvetenskap" +loovs 21b = "Teknik" +loovs 21c = "Matematik" diff --git a/module/Finna/src/Finna/AjaxHandler/FeedTrait.php b/module/Finna/src/Finna/AjaxHandler/FeedTrait.php index 5af568b2e97..d82fecd0477 100644 --- a/module/Finna/src/Finna/AjaxHandler/FeedTrait.php +++ b/module/Finna/src/Finna/AjaxHandler/FeedTrait.php @@ -102,12 +102,13 @@ protected function formatFeed( 'modal' => $modal, ]; - if (isset($config->title)) { - if ($config->title == 'rss') { - $feed['title'] = $channel->getTitle(); - } else { - $feed['translateTitle'] = $config->title; - } + $title = $config->title ?? 'rss'; + if ('rss' === $title) { + $feed['title'] = $channel->getTitle(); + $feed['translateTitle'] = false; + } else { + $feed['title'] = $title; + $feed['translateTitle'] = true; } if (isset($config->description)) { diff --git a/module/Finna/src/Finna/Auth/ILSFinna.php b/module/Finna/src/Finna/Auth/ILSFinna.php index 673f250a5f1..9c4c2bbe79a 100644 --- a/module/Finna/src/Finna/Auth/ILSFinna.php +++ b/module/Finna/src/Finna/Auth/ILSFinna.php @@ -128,7 +128,7 @@ protected function processILSUser($info) // Set home library if not already set if (!empty($info['home_library']) && empty($user->getHomeLibrary())) { - $user->setHomeLibrary($info['home_library']); + $this->authenticator->updateUserHomeLibrary($user, $info['home_library']); } return $user; diff --git a/module/Finna/src/Finna/Controller/RecordController.php b/module/Finna/src/Finna/Controller/RecordController.php index 168adb3f01e..a1aea0cf403 100644 --- a/module/Finna/src/Finna/Controller/RecordController.php +++ b/module/Finna/src/Finna/Controller/RecordController.php @@ -495,7 +495,7 @@ public function holdAction() $config = $this->getConfig(); $homeLibrary = ($config->Account->set_home_library ?? true) - ? $this->getUser()->home_library : ''; + ? $this->getUser()->getHomeLibrary() : ''; $helpText = $checkHolds['helpText'] ?? null; // acceptTermsText kept for backward-compatibility: $acceptTermsText = $acceptTermsTextHtml @@ -666,7 +666,7 @@ public function storageRetrievalRequestAction() 'gatheredDetails' => $gatheredDetails, 'pickup' => $pickup, 'defaultPickup' => $defaultPickup, - 'homeLibrary' => $this->getUser()->home_library, + 'homeLibrary' => $this->getUser()->getHomeLibrary(), 'extraFields' => $extraFields, 'defaultRequiredDate' => $defaultRequired, 'helpText' => $checkRequests['helpText'] ?? null, @@ -805,7 +805,7 @@ public function illRequestAction() 'gatheredDetails' => $gatheredDetails, 'pickupLibraries' => $pickupLibraries, 'pickupLocations' => $pickupLocations, - 'homeLibrary' => $this->getUser()->home_library, + 'homeLibrary' => $this->getUser()->getHomeLibrary(), 'extraFields' => $extraFields, 'defaultRequiredDate' => $defaultRequired, 'helpText' => $checkRequests['helpText'] ?? null, diff --git a/module/Finna/src/Finna/Db/Row/Fee.php b/module/Finna/src/Finna/Db/Row/Fee.php index 4e4e02c295d..0bfbb4d20d8 100644 --- a/module/Finna/src/Finna/Db/Row/Fee.php +++ b/module/Finna/src/Finna/Db/Row/Fee.php @@ -134,7 +134,7 @@ public function getUser(): UserEntityInterface */ public function setTitle(string $title): FinnaFeeEntityInterface { - $this->title = $title; + $this->title = mb_substr($title, 0, 255, 'UTF-8'); return $this; } @@ -157,7 +157,7 @@ public function getTitle(): string */ public function setType(string $type): FinnaFeeEntityInterface { - $this->type = $type; + $this->type = mb_substr($type, 0, 255, 'UTF-8'); return $this; } @@ -180,7 +180,7 @@ public function getType(): string */ public function setDescription(string $description): FinnaFeeEntityInterface { - $this->description = $description; + $this->description = mb_substr($description, 0, 255, 'UTF-8'); return $this; } @@ -272,7 +272,7 @@ public function getFineId(): string */ public function setOrganization(string $organization): FinnaFeeEntityInterface { - $this->organization = $organization; + $this->organization = mb_substr($organization, 0, 255, 'UTF-8'); return $this; } diff --git a/module/Finna/src/Finna/Db/Row/Transaction.php b/module/Finna/src/Finna/Db/Row/Transaction.php index 6fae8f2bb97..52cae45dd6b 100644 --- a/module/Finna/src/Finna/Db/Row/Transaction.php +++ b/module/Finna/src/Finna/Db/Row/Transaction.php @@ -365,7 +365,7 @@ public function getStatus(): FinnaTransactionStatus */ public function setStatusMessage(string $description): FinnaTransactionEntityInterface { - $this->status = $description; + $this->status = mb_substr($description, 0, 255, 'UTF-8'); return $this; } diff --git a/module/Finna/src/Finna/ILS/Driver/Quria.php b/module/Finna/src/Finna/ILS/Driver/Quria.php index 5ff742920df..6e4d3c0702e 100644 --- a/module/Finna/src/Finna/ILS/Driver/Quria.php +++ b/module/Finna/src/Finna/ILS/Driver/Quria.php @@ -767,6 +767,8 @@ public function getMyTransactions($user) 'title' => $title, 'duedate' => $loan->loanDueDate, 'dueStatus' => $dueStatus, + 'checkoutDate' => $this->formatDate($loan->loanDate), + 'borrowingLocation' => $loan->branch ?? '', 'renewable' => (string)($loan->loanStatus->isRenewable ?? '') === 'yes', 'message' => $message, 'renew' => $renew, diff --git a/module/Finna/src/Finna/Record/Loader.php b/module/Finna/src/Finna/Record/Loader.php index 26d2b0910f5..29980738ef7 100644 --- a/module/Finna/src/Finna/Record/Loader.php +++ b/module/Finna/src/Finna/Record/Loader.php @@ -181,6 +181,41 @@ public function load( return $result; } + /** + * Given an array of associative arrays with id and source keys (or pipe- + * separated source|id strings), load all of the requested records in the + * requested order. + * + * Finna: Ignores 'sources' setting in search configuration. + * + * @param array $ids Array of associative arrays with + * id/source keys or strings in source|id format. In associative array formats, + * there is also an optional "extra_fields" key which can be used to pass in data + * formatted as if it belongs to the Solr schema; this is used to create + * a mock driver object if the real data source is unavailable. + * @param bool $tolerateBackendExceptions Whether to tolerate backend + * exceptions that may be caused by e.g. connection issues or changes in + * subscriptions + * @param ParamBag[] $params Associative array of search + * backend parameters keyed with source key + * + * @throws \Exception + * @return array Array of record drivers + */ + public function loadBatchIgnoringSourceFilter( + $ids, + $tolerateBackendExceptions = false, + $params = [] + ) { + foreach (array_unique(array_column($ids, 'source')) as $source) { + if (!isset($params[$source])) { + $params[$source] = new ParamBag(); + } + $params[$source]->set('finna.ignore_source_filter', 1); + } + return $this->loadBatch($ids, $tolerateBackendExceptions, $params); + } + /** * Given an array of IDs and a record source, load a batch of records for * that source. @@ -215,7 +250,8 @@ public function loadBatchForSource( $records = parent::loadBatchForSource( $ids, $source, - $tolerateBackendExceptions + $tolerateBackendExceptions, + $params ); // Check the results for missing records and try to load them with their old IDs: diff --git a/module/Finna/src/Finna/RecordDriver/Feature/ContainerFormatTrait.php b/module/Finna/src/Finna/RecordDriver/Feature/ContainerFormatTrait.php index caeb29a3476..f1d36f4a403 100644 --- a/module/Finna/src/Finna/RecordDriver/Feature/ContainerFormatTrait.php +++ b/module/Finna/src/Finna/RecordDriver/Feature/ContainerFormatTrait.php @@ -32,6 +32,7 @@ use Finna\Record\Loader; use Finna\RecordDriver\PluginManager; use VuFind\RecordDriver\AbstractBase; +use VuFindSearch\ParamBag; use VuFindSearch\Response\RecordInterface; use function count; @@ -142,9 +143,13 @@ public function getEncapsulatedRecord(string $id): ?RecordInterface $driver instanceof EncapsulatedRecordInterface && $needed = $driver->needsRecordLoaded() ) { - $driver->setLoadedRecord( - $this->recordLoader->load($needed['id'], $needed['source'], true) + $loadedRecord = $this->recordLoader->load( + $needed['id'], + $needed['source'], + true, + new ParamBag(['finna.ignore_source_filter' => 1]) ); + $driver->setLoadedRecord($loadedRecord); } return $driver; } @@ -347,12 +352,13 @@ protected function loadNeededRecords(array $records): void $record instanceof EncapsulatedRecordInterface && $needed = $record->needsRecordLoaded() ) { - $neededMap[$needed['source']][$needed['id']] = $i; + $source = $needed['source']; + $neededMap[$source][$needed['id']] = $i; $ids[] = $needed; } } if (!empty($ids)) { - $loadedRecords = $this->recordLoader->loadBatch($ids); + $loadedRecords = $this->recordLoader->loadBatchIgnoringSourceFilter($ids); foreach ($loadedRecords as $loadedRecord) { $loadedSource = $loadedRecord->getSourceIdentifier(); $loadedId = $loadedRecord->getUniqueID(); diff --git a/module/Finna/src/Finna/RecordDriver/Feature/LrmiDriverTrait.php b/module/Finna/src/Finna/RecordDriver/Feature/LrmiDriverTrait.php new file mode 100644 index 00000000000..895aa31ac27 --- /dev/null +++ b/module/Finna/src/Finna/RecordDriver/Feature/LrmiDriverTrait.php @@ -0,0 +1,58 @@ +<?php + +/** + * Common functionality for LRMI record drivers. + * + * PHP version 8 + * + * Copyright (C) The National Library of Finland 2024. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * @category VuFind + * @package RecordDrivers + * @author Aleksi Peebles <aleksi.peebles@helsinki.fi> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:record_drivers Wiki + */ + +namespace Finna\RecordDriver\Feature; + +/** + * Common functionality for LRMI record drivers. + * + * @category VuFind + * @package RecordDrivers + * @author Aleksi Peebles <aleksi.peebles@helsinki.fi> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:record_drivers Wiki + */ +trait LrmiDriverTrait +{ + /** + * Return root educational levels + * + * @return array + */ + public function getRootEducationalLevels() + { + $rootLevels = []; + foreach ($this->fields['educational_level_str_mv'] ?? [] as $level) { + if (substr($level, 0, 1) === '0') { + $rootLevels[] = trim((string)$level); + } + } + return $rootLevels; + } +} diff --git a/module/Finna/src/Finna/RecordDriver/SolrAipa.php b/module/Finna/src/Finna/RecordDriver/SolrAipa.php index 89bac239216..123d74f59cd 100644 --- a/module/Finna/src/Finna/RecordDriver/SolrAipa.php +++ b/module/Finna/src/Finna/RecordDriver/SolrAipa.php @@ -31,6 +31,7 @@ use Finna\RecordDriver\Feature\ContainerFormatInterface; use Finna\RecordDriver\Feature\ContainerFormatTrait; +use Finna\RecordDriver\Feature\LrmiDriverTrait; use function in_array; @@ -46,6 +47,7 @@ class SolrAipa extends SolrQdc implements ContainerFormatInterface { use ContainerFormatTrait; + use LrmiDriverTrait; /** * Encapsulated content type records. @@ -189,6 +191,28 @@ public function getGeneralNotes() return []; } + /** + * Return record type. + * + * @return string + */ + public function getType(): string + { + return (string)($this->getXmlRecord()->type ?? ''); + } + + /** + * Get topics + * + * @param string $type defaults to /onto/yso/ + * + * @return array + */ + public function getTopics(string $type = '/onto/yso/'): array + { + return $this->getAllSubjectHeadings(); + } + /** * Return encapsulated content type records. * diff --git a/module/Finna/src/Finna/RecordDriver/SolrLido.php b/module/Finna/src/Finna/RecordDriver/SolrLido.php index 0674b031bb1..b8febe0cc2a 100644 --- a/module/Finna/src/Finna/RecordDriver/SolrLido.php +++ b/module/Finna/src/Finna/RecordDriver/SolrLido.php @@ -143,7 +143,10 @@ class SolrLido extends \VuFind\RecordDriver\SolrDefault implements \Laminas\Log\ */ protected $supportedVideoFormats = [ 'mp4' => 'video/mp4', + 'video/mp4' => 'video/mp4', 'mov' => 'video/quicktime', + 'video/quicktime' => 'video/quicktime', + 'text/html' => 'text/html', ]; /** @@ -939,19 +942,26 @@ protected function getVideo( string $format, string $description ): array { - if ($codec = $this->supportedVideoFormats[$format] ?? false) { - return [ + $mediaType = $this->supportedVideoFormats[$format] ?? false; + return match ($mediaType) { + 'text/html' => [ + 'desc' => $description ?: false, + 'url' => $url, + 'embed' => 'iframe', + 'format' => $format, + ], + false => [], + default => [ 'desc' => $description ?: false, 'url' => $url, 'embed' => 'video', 'format' => $format, 'videoSources' => [ 'src' => $url, - 'type' => $codec, + 'type' => $mediaType, ], - ]; - } - return []; + ], + }; } /** @@ -1770,7 +1780,7 @@ public function getNonPresenterAuthors() if (!($event = $set->event ?? '')) { continue; } - $eventType = (string)($event->eventType->term ?? ''); + $eventType = mb_strtolower((string)($event->eventType->term ?? ''), 'UTF-8'); $priority = $this->authorEvents[$eventType] ?? null; if (null === $priority) { continue; diff --git a/module/Finna/src/Finna/RecordDriver/SolrLrmi.php b/module/Finna/src/Finna/RecordDriver/SolrLrmi.php index bad638844da..6e617dfb03e 100644 --- a/module/Finna/src/Finna/RecordDriver/SolrLrmi.php +++ b/module/Finna/src/Finna/RecordDriver/SolrLrmi.php @@ -33,6 +33,8 @@ namespace Finna\RecordDriver; +use Finna\RecordDriver\Feature\LrmiDriverTrait; + use function in_array; /** @@ -50,6 +52,8 @@ */ class SolrLrmi extends SolrQdc { + use LrmiDriverTrait; + /** * File formats that are downloadable * @@ -212,22 +216,6 @@ public function getEducationalLevels() return $this->fields['educational_level_str_mv'] ?? []; } - /** - * Return root educational levels - * - * @return array - */ - public function getRootEducationalLevels() - { - $rootLevels = []; - foreach ($this->fields['educational_level_str_mv'] ?? [] as $level) { - if (substr($level, 0, 1) === '0') { - $rootLevels[] = trim((string)$level); - } - } - return $rootLevels; - } - /** * Return url to external LRMI record page based on the record ID * or false if an external link template is not provided. diff --git a/module/Finna/src/Finna/RecordDriver/SolrMarc.php b/module/Finna/src/Finna/RecordDriver/SolrMarc.php index 01bed6ee9ed..925dfb97f8d 100644 --- a/module/Finna/src/Finna/RecordDriver/SolrMarc.php +++ b/module/Finna/src/Finna/RecordDriver/SolrMarc.php @@ -1109,6 +1109,26 @@ public function getProducers() return $result; } + /** + * Get all authors and primary presenters + * + * @return array + */ + public function getPrimaryAuthors(): array + { + return array_column($this->getPrimaryAuthorsExtended(), 'name'); + } + + /** + * Return extended author information + * + * @return array + */ + public function getPrimaryAuthorsExtended(): array + { + return $this->getAuthorFields(true); + } + /** * Get all authors apart from presenters * @@ -1116,8 +1136,19 @@ public function getProducers() */ public function getNonPresenterAuthors() { - $result = []; + return $this->getAuthorFields(); + } + /** + * Gets all author fields + * + * @param bool $getPrimaryPresenters Whether the function returns primary presenters alongside authors (optional) + * + * @return array + */ + private function getAuthorFields(bool $getPrimaryPresenters = false): array + { + $result = []; foreach (['100', '110', '700', '710'] as $fieldCode) { $fields = $this->getMarcReader()->getFields($fieldCode); if (is_array($fields)) { @@ -1127,6 +1158,8 @@ public function getNonPresenterAuthors() if ($fieldCode == '700' && $this->getSubfield($field, 't')) { continue; } + $checkPresenterRole = + ($getPrimaryPresenters && in_array($fieldCode, ['100', '110'])) ? false : true; $roles = $this->getSubfields($field, '4'); if (empty($roles)) { @@ -1136,7 +1169,8 @@ public function getNonPresenterAuthors() $role = implode(', ', $roles); $role = mb_strtolower($role, 'UTF-8'); if ( - $role + $checkPresenterRole + && $role && isset($this->mainConfig->Record->presenter_roles) && in_array( trim($role, ' .'), @@ -1207,9 +1241,11 @@ public function getOtherLinks() /** * Get presenters * + * @param bool $getSecondaryPresentersOnly Whether returns only secondary presenters + * * @return array */ - public function getPresenters() + public function getPresenters($getSecondaryPresentersOnly = false): array { $result = ['presenters' => [], 'details' => []]; @@ -1217,6 +1253,9 @@ public function getPresenters() $fields = $this->getMarcReader()->getFields($fieldCode); if (is_array($fields)) { foreach ($fields as $field) { + if ($getSecondaryPresentersOnly && in_array($fieldCode, ['100', '110'])) { + continue; + } // Leave out 700 fields containing subfield 't' (these go to the // contents list) if ($fieldCode == '700' && $this->getSubfield($field, 't')) { @@ -1239,18 +1278,19 @@ public function getPresenters() ) { continue; } - $id = $this->getSubfield($field, '0'); $subfields = $this->getSubfieldArray($field, ['a', 'b', 'c']); - $dates = $this->getSubfieldArray($field, ['d']); - if (!empty($subfields)) { - $result['presenters'][] = [ - 'name' => $this->stripTrailingPunctuation($subfields[0]), - 'date' => $dates - ? $this->stripTrailingPunctuation($dates[0]) : '', - 'role' => $role, - 'id' => $id ?: null, - ]; + if (empty($subfields)) { + continue; } + $dates = $this->getSubfieldArray($field, ['d']); + $id = $this->getSubfield($field, '0'); + $result['presenters'][] = [ + 'name' => $this->stripTrailingPunctuation($subfields[0]), + 'date' => $dates + ? $this->stripTrailingPunctuation($dates[0]) : '', + 'role' => $role, + 'id' => $id ?: null, + ]; } } } @@ -1260,6 +1300,16 @@ public function getPresenters() return $result; } + /** + * Get secondary presenters + * + * @return array + */ + public function getSecondaryPresenters(): array + { + return $this->getPresenters(true); + } + /** * Get the main author of the record (without year and role). * diff --git a/module/Finna/src/Finna/Search/Solr/SolrExtensionsListener.php b/module/Finna/src/Finna/Search/Solr/SolrExtensionsListener.php index 1e593929b19..0e091fd1053 100644 --- a/module/Finna/src/Finna/Search/Solr/SolrExtensionsListener.php +++ b/module/Finna/src/Finna/Search/Solr/SolrExtensionsListener.php @@ -192,8 +192,15 @@ public function onSearchPost(EventInterface $event) */ protected function addDataSourceFilter(EventInterface $event) { + $command = $event->getParam('command'); + $params = $command->getSearchParameters(); + // Don't add the filter if requested so (e.g. AIPA encapsulated records) or we're fetching a single record (to + // be able to link to encapsulated records): + if ($params->get('finna.ignore_source_filter') || $command->getContext() === 'retrieve') { + $params->remove('finna.ignore_source_filter'); + return; + } if ($recordSources = $this->getActiveSources($event)) { - $params = $event->getParam('command')->getSearchParameters(); $params->add('fq', static::TERMS_FILTER_PREFIX_SOURCE . implode("\u{001f}", $recordSources)); } } diff --git a/module/Finna/src/Finna/View/Helper/Root/Aipa.php b/module/Finna/src/Finna/View/Helper/Root/Aipa.php index 8a61f37b8dd..039b26875c1 100644 --- a/module/Finna/src/Finna/View/Helper/Root/Aipa.php +++ b/module/Finna/src/Finna/View/Helper/Root/Aipa.php @@ -31,11 +31,13 @@ use Finna\RecordDriver\AipaLrmi; use Finna\RecordDriver\SolrAipa; +use Finna\RecordDriver\SolrQdc; use Laminas\View\Helper\AbstractHelper; use NatLibFi\FinnaCodeSets\FinnaCodeSets; use NatLibFi\FinnaCodeSets\Model\EducationalLevel\EducationalLevelInterface; use NatLibFi\FinnaCodeSets\Model\HierarchicalProxyDataObjectInterface as HPDOInterface; use NatLibFi\FinnaCodeSets\Utility\EducationalData; +use VuFind\View\Helper\Root\ClassBasedTemplateRendererTrait; use function count; @@ -50,6 +52,8 @@ */ class Aipa extends AbstractHelper { + use ClassBasedTemplateRendererTrait; + // Sort order for educational levels. protected const EDUCATIONAL_LEVEL_SORT_ORDER = [ EducationalData::PRIMARY_SCHOOL, @@ -103,6 +107,36 @@ public function __invoke(SolrAipa|AipaLrmi $driver): Aipa return $this; } + /** + * Render an AIPA template. + * + * @param string $name Template name to render + * @param ?array $context Variables needed for rendering template; these will + * be temporarily added to the global view context, then reverted after the + * template is rendered (default = record driver only). + * @param bool $throw If true (default), an exception is thrown if the + * template is not found. Otherwise an empty string is returned. + * + * @return string + */ + public function renderTemplate( + string $name, + ?array $context = null, + bool $throw = true + ) { + $template = 'RecordDriver/%s/' . $name; + $className = match ($this->driver->getType()) { + 'aipa:education' => AipaLrmi::class, + default => SolrQdc::class, + }; + return $this->renderClassTemplate( + $template, + $className, + $context ?? ['driver' => $this->driver], + $throw + ); + } + /** * Render all subject headings. * diff --git a/module/Finna/src/Finna/View/Helper/Root/Feed.php b/module/Finna/src/Finna/View/Helper/Root/Feed.php index 888e253285e..b5af57d8081 100644 --- a/module/Finna/src/Finna/View/Helper/Root/Feed.php +++ b/module/Finna/src/Finna/View/Helper/Root/Feed.php @@ -67,8 +67,14 @@ public function __construct(\Laminas\Config\Config $config) */ public function __invoke($id) { - if (isset($this->config[$id]) && $this->config[$id]['active']) { - return $this->getView()->render('Helpers/feed.phtml', ['id' => $id]); + $feedConfig = $this->config[$id] ?? ''; + if ($feedConfig['active'] ?? false) { + $title = ($feedConfig['title'] ?? 'rss') !== 'rss' ? $feedConfig['title'] : ''; + $type = $feedConfig['type']; + return $this->getView()->render( + 'Helpers/feed.phtml', + ['id' => $id, 'title' => $title, 'type' => $type] + ); } } } diff --git a/module/Finna/src/Finna/View/Helper/Root/RecordDataFormatter.php b/module/Finna/src/Finna/View/Helper/Root/RecordDataFormatter.php index 019bd8e2bbc..1d2cc0fe9a1 100644 --- a/module/Finna/src/Finna/View/Helper/Root/RecordDataFormatter.php +++ b/module/Finna/src/Finna/View/Helper/Root/RecordDataFormatter.php @@ -112,7 +112,7 @@ public function filterMarcFields($coreFields) 'Physical Description', 'Place of Origin', 'Playing Time', - 'Presenters', + 'Presenters Marc', 'Previous Title', 'Production', 'Production Credits', diff --git a/module/Finna/src/Finna/View/Helper/Root/RecordDataFormatterFactory.php b/module/Finna/src/Finna/View/Helper/Root/RecordDataFormatterFactory.php index 9cfbe7b4364..9993f10f91e 100644 --- a/module/Finna/src/Finna/View/Helper/Root/RecordDataFormatterFactory.php +++ b/module/Finna/src/Finna/View/Helper/Root/RecordDataFormatterFactory.php @@ -307,6 +307,17 @@ protected function getDefaultCoreFields() 'context' => ['class' => 'recordPresenters'], ] ); + $setTemplateLine( + 'Presenters Marc', + 'getSecondaryPresenters', + 'data-presenters.phtml', + [ + 'context' => [ + 'class' => 'recordPresenters', + 'title' => 'Presenters', + ], + ] + ); $setTemplateLine( 'Other Titles', 'getAlternativeTitles', diff --git a/module/Finna/src/Finna/View/Helper/Root/RecordFactory.php b/module/Finna/src/Finna/View/Helper/Root/RecordFactory.php index db5aaea667c..c3a87b18607 100644 --- a/module/Finna/src/Finna/View/Helper/Root/RecordFactory.php +++ b/module/Finna/src/Finna/View/Helper/Root/RecordFactory.php @@ -82,9 +82,9 @@ public function __invoke( $container->get(\VuFind\Form\Form::class), $container->get(\Finna\Service\UserPreferenceService::class), function ($options) use ($container) { - $result = clone $container + $result = $container ->get(\VuFind\Search\Results\PluginManager::class) - ->get('EncapsulatedRecords'); + ->get(\Finna\Search\EncapsulatedRecords\Results::class); $result->getParams()->initFromRequest(new Parameters($options)); return $result; } diff --git a/module/VuFind/src/VuFind/Auth/Manager.php b/module/VuFind/src/VuFind/Auth/Manager.php index 309b47abdf9..55d44e82cc5 100644 --- a/module/VuFind/src/VuFind/Auth/Manager.php +++ b/module/VuFind/src/VuFind/Auth/Manager.php @@ -92,6 +92,13 @@ class Manager implements */ protected $hideLogin = null; + /** + * ILS Authenticator + * + * @var ?ILSAuthenticator + */ + protected $ilsAuthenticator = null; + /** * Constructor * @@ -123,6 +130,18 @@ public function __construct( $this->setAuthMethod($method); // load it } + /** + * Set ILS Authenticator + * + * @param ILSAuthenticator $ilsAuthenticator ILS authenticator + * + * @return void + */ + public function setILSAuthenticator(ILSAuthenticator $ilsAuthenticator): void + { + $this->ilsAuthenticator = $ilsAuthenticator; + } + /** * Get the authentication handler. * @@ -754,6 +773,34 @@ public function login($request) throw new AuthException('authentication_error_technical', 0, $e); } + // Attempt catalog login so that any bad credentials are cleared before further processing + // (avoids e.g. multiple login attempts by account AJAX checks). + if ( + ($this->config->Catalog->checkILSCredentialsOnLogin ?? true) + && $this->ilsAuthenticator + && $this->allowsUserIlsLogin() + && ($catUsername = $user->getCatUsername()) + // If ILS authentication was used, catalog username must not be the same as the username just used for + // authentication: + && (!in_array($user->getAuthMethod(), ['ils', 'multiils']) || $catUsername !== $user->getUsername()) + && !$this->ils->getOfflineMode() + ) { + try { + $patron = $this->ils->patronLogin( + $catUsername, + $this->ilsAuthenticator->getCatPasswordForUser($user) + ); + if (empty($patron)) { + // Problem logging in -- clear user credentials so they can be + // prompted again; perhaps their password has changed in the + // system! + $user->setCatUsername(null)->setRawCatPassword(null)->setCatPassEnc(null); + } + } catch (\Exception $e) { + // Ignore exceptions here so that the login can continue + } + } + // Update user object $this->updateUser($user, $mainAuthMethod); @@ -765,8 +812,11 @@ public function login($request) throw new AuthException('authentication_error_technical', 0, $e); } } - // Store the user in the session and send it back to the caller: + + // Store the user in the session: $this->updateSession($user); + + // Send user back to caller: return $user; } catch (\Exception $e) { $this->getAuth()->resetState(); diff --git a/module/VuFind/src/VuFind/Auth/ManagerFactory.php b/module/VuFind/src/VuFind/Auth/ManagerFactory.php index 835910fd35b..18c3440f3bf 100644 --- a/module/VuFind/src/VuFind/Auth/ManagerFactory.php +++ b/module/VuFind/src/VuFind/Auth/ManagerFactory.php @@ -91,6 +91,7 @@ public function __invoke( $loginTokenManager, $ils ); + $manager->setIlsAuthenticator($container->get(\VuFind\Auth\ILSAuthenticator::class)); $manager->checkForExpiredCredentials(); return $manager; } diff --git a/module/VuFind/src/VuFind/Controller/OAuth2Controller.php b/module/VuFind/src/VuFind/Controller/OAuth2Controller.php index c0cfd9568e4..a7ddc302957 100644 --- a/module/VuFind/src/VuFind/Controller/OAuth2Controller.php +++ b/module/VuFind/src/VuFind/Controller/OAuth2Controller.php @@ -172,6 +172,20 @@ public function authorizeAction() return $this->handleException('Authorization request', $e); } + // Hide any scopes not allowed by a client-specific filter (see also ScopeRepository for the actual filtering): + if ($allowedScopes = $clientConfig['allowedScopes'] ?? null) { + $scopes = $authRequest->getScopes(); + array_map( + function ($scope) use ($allowedScopes) { + if (!in_array($scope->getIdentifier(), $allowedScopes)) { + $scope->setHidden(true); + } + }, + $scopes + ); + $authRequest->setScopes($scopes); + } + if ($this->formWasSubmitted('allow') || $this->formWasSubmitted('deny')) { // Check CSRF and session: if (!$this->csrf->isValid($this->getRequest()->getPost()->get('csrf'))) { @@ -278,8 +292,9 @@ public function userInfoAction() OAuthServerException::accessDenied('User does not exist anymore') ); } - $result = $this->claimExtractor - ->extract($scopes, $userEntity->getClaims()); + $result = $this->claimExtractor->extract($scopes, $userEntity->getClaims()); + // The sub claim must always be returned: + $result['sub'] = $userId; return $this->getJsonResponse($result); } catch (OAuthServerException $e) { return $this->handleOAuth2Exception('User info request', $e); diff --git a/module/VuFind/src/VuFind/OAuth2/Repository/ScopeRepository.php b/module/VuFind/src/VuFind/OAuth2/Repository/ScopeRepository.php index 333f74b8e2a..0217e6f0a5f 100644 --- a/module/VuFind/src/VuFind/OAuth2/Repository/ScopeRepository.php +++ b/module/VuFind/src/VuFind/OAuth2/Repository/ScopeRepository.php @@ -33,6 +33,8 @@ use League\OAuth2\Server\Repositories\ScopeRepositoryInterface; use VuFind\OAuth2\Entity\ScopeEntity; +use function in_array; + /** * OAuth2 scope repository implementation. * @@ -98,6 +100,18 @@ public function finalizeScopes( ClientEntityInterface $clientEntity, $userIdentifier = null ) { + $clientId = $clientEntity->getIdentifier(); + // Apply any client-specific filter to scopes: + if ($allowedScopes = $this->oauth2Config['Clients'][$clientId]['allowedScopes'] ?? null) { + $scopes = array_values( + array_filter( + $scopes, + function ($scope) use ($allowedScopes) { + return in_array($scope->getIdentifier(), $allowedScopes); + } + ) + ); + } return $scopes; } } diff --git a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/OAuth2Test.php b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/OAuth2Test.php index d0ba141af13..c160190ba9b 100644 --- a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/OAuth2Test.php +++ b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/OAuth2Test.php @@ -31,6 +31,8 @@ namespace VuFindTest\Mink; +use function count; + /** * OAuth2/OIDC test class. * @@ -115,6 +117,16 @@ protected function getOauth2ConfigOverrides(string $redirectUri): array 'isConfidential' => true, 'secret' => password_hash('mysecret', PASSWORD_DEFAULT), ], + 'test_limited' => [ + 'name' => 'Integration Test', + 'redirectUri' => $redirectUri, + 'isConfidential' => true, + 'secret' => password_hash('mysecret', PASSWORD_DEFAULT), + 'allowedScopes' => [ + 'openid', + 'profile', + ], + ], ], ]; } @@ -140,24 +152,61 @@ protected function setUpTest(string $redirectUri): void ); } + /** + * Data provider for testOAuth2Authorization + * + * @return array + */ + public static function oauth2AuthorizationProvider(): array + { + return [ + 'test client' => [ + 'test', + [ + 'Read your user identifier', + 'Read your basic profile information (name, language, birthdate)', + 'Read a unique hash based on your library user identifier', + 'Read your age', + ], + false, + ], + 'limited test client' => [ + 'test_limited', + [ + 'Read your user identifier', + 'Read your basic profile information (name, language, birthdate)', + ], + true, + ], + ]; + } + /** * Test OAuth2 authorization. * + * @param string $clientId Client ID + * @param array $expectedPermissions Expected permissions in the request + * @param bool $limited Whether the permission set has been limited by the server + * * @return void + * + * @dataProvider oauth2AuthorizationProvider */ - public function testOAuth2Authorization(): void + public function testOAuth2Authorization(string $clientId, array $expectedPermissions, bool $limited): void { // Bogus redirect URI, but it doesn't matter since the page won't handle the // authorization response: $redirectUri = $this->getVuFindUrl() . '/Content/faq'; $this->setUpTest($redirectUri); + static::removeUsers(['username1']); + $nonce = time(); $state = md5((string)$nonce); // Go to OAuth2 authorization screen: $params = [ - 'client_id' => 'test', + 'client_id' => $clientId, 'scope' => 'openid profile library_user_id age', 'response_type' => 'code', 'redirect_uri' => $redirectUri, @@ -184,18 +233,14 @@ public function testOAuth2Authorization(): void 'catuser' . $oauth2ConfigOverrides['Server']['hashSalt'] ); - $expectedPermissions = [ - 'Read your user identifier', - 'Read your basic profile information (name, language, birthdate)', - 'Read a unique hash based on your library user identifier', - 'Read your age', - ]; foreach ($expectedPermissions as $index => $permission) { $this->assertEquals( $permission, $this->findCssAndGetText($page, 'div.oauth2-prompt li', null, $index) ); } + // Ensure that there are no more permissions: + $this->unFindCss($page, 'div.oauth2-prompt li', null, count($expectedPermissions)); $this->clickCss($page, '.form-oauth2-authorize button.btn.btn-primary'); @@ -213,7 +258,7 @@ public function testOAuth2Authorization(): void 'code' => $queryParams['code'], 'grant_type' => 'authorization_code', 'redirect_uri' => $redirectUri, - 'client_id' => 'test', + 'client_id' => $clientId, 'client_secret' => 'mysecret', ]; $response = $this->httpPost( @@ -243,21 +288,26 @@ public function testOAuth2Authorization(): void ); $this->assertInstanceOf(\stdClass::class, $idToken); - $this->assertEquals('test', $idToken->aud); + $this->assertEquals($clientId, $idToken->aud); $this->assertEquals($nonce, $idToken->nonce); $this->assertEquals('Tester McTestenson', $idToken->name); $this->assertEquals('Tester', $idToken->given_name); $this->assertEquals('McTestenson', $idToken->family_name); - $this->assertEquals($catIdHash, $idToken->library_user_id); $this->assertMatchesRegularExpression( '/^\d{4}-\d{2}-\d{2}$/', $idToken->birthdate ); - $this->assertEquals( - \DateTime::createFromFormat('Y-m-d', $idToken->birthdate) - ->diff(new \DateTimeImmutable())->format('%y'), - $idToken->age - ); + if ($limited) { + $this->assertObjectNotHasProperty('library_user_id', $idToken); + $this->assertObjectNotHasProperty('age', $idToken); + } else { + $this->assertEquals($catIdHash, $idToken->library_user_id); + $this->assertEquals( + \DateTime::createFromFormat('Y-m-d', $idToken->birthdate) + ->diff(new \DateTimeImmutable())->format('%y'), + $idToken->age + ); + } // Test the userinfo endpoint: $response = $this->httpGet( @@ -276,15 +326,26 @@ public function testOAuth2Authorization(): void ); $userInfo = json_decode($response->getBody(), true); + $this->assertEquals($idToken->sub, $userInfo['sub']); $this->assertEquals($nonce, $userInfo['nonce']); $this->assertEquals('Tester McTestenson', $userInfo['name']); $this->assertEquals('Tester', $userInfo['given_name']); $this->assertEquals('McTestenson', $userInfo['family_name']); - $this->assertEquals($catIdHash, $userInfo['library_user_id']); $this->assertMatchesRegularExpression( '/^\d{4}-\d{2}-\d{2}$/', $userInfo['birthdate'] ); + if ($limited) { + $this->assertObjectNotHasProperty('library_user_id', $idToken); + $this->assertObjectNotHasProperty('age', $idToken); + } else { + $this->assertEquals($catIdHash, $userInfo['library_user_id']); + $this->assertEquals( + \DateTime::createFromFormat('Y-m-d', $userInfo['birthdate']) + ->diff(new \DateTimeImmutable())->format('%y'), + $userInfo['age'] + ); + } // Test token request with bad credentials: $tokenParams['client_secret'] = 'badsecret'; diff --git a/module/VuFindTheme/src/VuFindTheme/View/Helper/HeadLink.php b/module/VuFindTheme/src/VuFindTheme/View/Helper/HeadLink.php index 37210e0a247..4745d2552ec 100644 --- a/module/VuFindTheme/src/VuFindTheme/View/Helper/HeadLink.php +++ b/module/VuFindTheme/src/VuFindTheme/View/Helper/HeadLink.php @@ -114,7 +114,7 @@ protected function getFileType() public function itemToString(stdClass $item) { // Normalize href to account for themes (if appropriate), then call the parent class: - if ($this->isRelativePath($item->href)) { + if (isset($item->href) && $this->isRelativePath($item->href)) { $relPath = 'css/' . $item->href; $details = $this->themeInfo ->findContainingTheme($relPath, ThemeInfo::RETURN_ALL_DETAILS); diff --git a/themes/bootstrap3/js/record.js b/themes/bootstrap3/js/record.js index e69d50d7647..e68bf2b2bdd 100644 --- a/themes/bootstrap3/js/record.js +++ b/themes/bootstrap3/js/record.js @@ -168,7 +168,7 @@ function handleAjaxTabLinks(_context) { $a.off("click").on("click", function linkClick() { var tabid = $('.record-tabs .nav-tabs li.active').data('tab'); var $tab = $('.' + tabid + '-tab'); - $tab.html('<div class="tab-pane ' + tabid + '-tab">' + VuFind.loading() + '</div>'); + $tab.html('<div role="tabpanel" class="tab-pane ' + tabid + '-tab">' + VuFind.loading() + '</div>'); ajaxLoadTab($tab, '', false, href); return false; }); @@ -293,7 +293,7 @@ function ajaxTagUpdate(_link, tag, _remove) { } function getNewRecordTab(tabid) { - return $('<div class="tab-pane ' + escapeHtmlAttr(tabid) + '-tab" aria-labelledby="record-tab-' + escapeHtmlAttr(tabid) + '">' + VuFind.loading() + '</div>'); + return $('<div role="tabpanel" class="tab-pane ' + escapeHtmlAttr(tabid) + '-tab" aria-labelledby="record-tab-' + escapeHtmlAttr(tabid) + '">' + VuFind.loading() + '</div>'); } function backgroundLoadTab(tabid) { @@ -340,12 +340,12 @@ function removeCheckRouteParam() { function recordDocReady() { removeCheckRouteParam(); - $('.record-tabs .nav-tabs li').attr('aria-selected', 'false'); - $('.record-tabs .nav-tabs .initiallyActive').attr('aria-selected', 'true'); + $('.record-tabs .nav-tabs a').attr('aria-selected', 'false'); + $('.record-tabs .nav-tabs .initiallyActive a').attr('aria-selected', 'true'); // update aria-selected attributes after a tab has been shown $('.record-tabs .nav-tabs a').on('shown.bs.tab', function shownTab(e) { - $('.record-tabs .nav-tabs li').attr('aria-selected', 'false'); - $(e.target).parent().attr('aria-selected', 'true'); + $('.record-tabs .nav-tabs a').attr('aria-selected', 'false'); + $(e.target).attr('aria-selected', 'true'); }); $('.record-tabs .nav-tabs a').on('click', function recordTabsClick() { var $li = $(this).parent(); diff --git a/themes/bootstrap3/templates/collection/view.phtml b/themes/bootstrap3/templates/collection/view.phtml index 880649a2a84..d353184fe5d 100644 --- a/themes/bootstrap3/templates/collection/view.phtml +++ b/themes/bootstrap3/templates/collection/view.phtml @@ -77,8 +77,8 @@ $tabClasses[] = 'noajax'; } ?> - <li class="<?=implode(' ', $tabClasses)?>" role="tab" data-tab="<?=$this->escapeHtmlAttr($tabName)?>"<?php if ($obj->supportsAjax() && in_array($tab, $this->backgroundTabs)):?> data-background<?php endif ?>> - <a href="<?=$this->escapeHtmlAttr($this->recordLinker()->getTabUrl($this->driver, $tab))?>#tabnav" data-lightbox-ignore> + <li role="presentation" id="record-tab-<?=$this->escapeHtmlAttr($tabName)?>" class="<?=implode(' ', $tabClasses)?>" data-tab="<?=$this->escapeHtmlAttr($tabName)?>"<?php if ($obj->supportsAjax() && in_array($tab, $this->backgroundTabs)):?> data-background<?php endif ?>> + <a role="tab" href="<?=$this->escapeHtmlAttr($this->recordLinker()->getTabUrl($this->driver, $tab))?>#tabnav" data-lightbox-ignore> <?=$this->transEsc($desc)?> </a> </li> @@ -87,7 +87,7 @@ <div class="tab-content collectionDetails<?=$tree ? 'Tree' : ''?>"> <?php if (!$this->loadInitialTabWithAjax || !isset($activeTabObj) || !$activeTabObj->supportsAjax()): ?> - <div class="tab-pane active <?=$this->escapeHtmlAttr($this->activeTab) ?>-tab"> + <div role="tabpanel" class="tab-pane active <?=$this->escapeHtmlAttr($this->activeTab) ?>-tab"> <?=isset($activeTabObj) ? $this->record($this->driver)->getTab($activeTabObj) : '' ?> </div> <?php endif; ?> diff --git a/themes/bootstrap3/templates/record/view.phtml b/themes/bootstrap3/templates/record/view.phtml index db8b030a767..a0e2d897e64 100644 --- a/themes/bootstrap3/templates/record/view.phtml +++ b/themes/bootstrap3/templates/record/view.phtml @@ -69,8 +69,8 @@ $tabClasses[] = 'noajax'; } ?> - <li id="record-tab-<?=$this->escapeHtmlAttr($tabName)?>" class="<?=implode(' ', $tabClasses)?>" role="tab" data-tab="<?=$this->escapeHtmlAttr($tabName)?>"<?php if ($obj->supportsAjax() && in_array($tab, $this->backgroundTabs)):?> data-background<?php endif ?>> - <a href="<?=$this->escapeHtmlAttr($this->recordLinker()->getTabUrl($this->driver, $tab))?>#tabnav" data-lightbox-ignore> + <li role="presentation" id="record-tab-<?=$this->escapeHtmlAttr($tabName)?>" class="<?=implode(' ', $tabClasses)?>" data-tab="<?=$this->escapeHtmlAttr($tabName)?>"<?php if ($obj->supportsAjax() && in_array($tab, $this->backgroundTabs)):?> data-background<?php endif ?>> + <a role="tab" href="<?=$this->escapeHtmlAttr($this->recordLinker()->getTabUrl($this->driver, $tab))?>#tabnav" data-lightbox-ignore> <?=$this->transEsc($desc)?> </a> </li> @@ -79,7 +79,7 @@ <div class="tab-content"> <?php if (!$this->loadInitialTabWithAjax || !isset($activeTabObj) || !$activeTabObj->supportsAjax()): ?> - <div class="tab-pane active <?=$this->escapeHtmlAttr($this->activeTab) ?>-tab" aria-labelledby="record-tab-<?=$this->escapeHtmlAttr($activeTabName ?? '')?>"> + <div role="tabpanel" class="tab-pane active <?=$this->escapeHtmlAttr($this->activeTab) ?>-tab" aria-labelledby="record-tab-<?=$this->escapeHtmlAttr($activeTabName ?? '')?>"> <?=isset($activeTabObj) ? $this->record($this->driver)->getTab($activeTabObj) : '' ?> </div> <?php endif; ?> diff --git a/themes/bootstrap5/js/bs3-compat.js b/themes/bootstrap5/js/bs3-compat.js index bfe25828bd1..e7db85b5a66 100644 --- a/themes/bootstrap5/js/bs3-compat.js +++ b/themes/bootstrap5/js/bs3-compat.js @@ -55,16 +55,6 @@ VuFind.register('bootstrap3CompatibilityLayer', function bootstrap3Compatibility // Use a timeout to allow the transition to complete before restoring the state: setTimeout(() => { VuFind.restoreTransitions(aEl, oldStateA); }, 0); } - // Move tab role from li to a: - if (aEl && liEl.parentElement.classList.contains('nav-tabs')) { - liEl.setAttribute('role', 'presentation'); - liEl.classList.add('nav-item'); - aEl.classList.add('nav-link'); - aEl.setAttribute('role', 'tab'); - if (aEl.classList.contains('active')) { - aEl.setAttribute('aria-selected', 'true'); - } - } }); }); diff --git a/themes/bootstrap5/js/record.js b/themes/bootstrap5/js/record.js index e69d50d7647..e68bf2b2bdd 100644 --- a/themes/bootstrap5/js/record.js +++ b/themes/bootstrap5/js/record.js @@ -168,7 +168,7 @@ function handleAjaxTabLinks(_context) { $a.off("click").on("click", function linkClick() { var tabid = $('.record-tabs .nav-tabs li.active').data('tab'); var $tab = $('.' + tabid + '-tab'); - $tab.html('<div class="tab-pane ' + tabid + '-tab">' + VuFind.loading() + '</div>'); + $tab.html('<div role="tabpanel" class="tab-pane ' + tabid + '-tab">' + VuFind.loading() + '</div>'); ajaxLoadTab($tab, '', false, href); return false; }); @@ -293,7 +293,7 @@ function ajaxTagUpdate(_link, tag, _remove) { } function getNewRecordTab(tabid) { - return $('<div class="tab-pane ' + escapeHtmlAttr(tabid) + '-tab" aria-labelledby="record-tab-' + escapeHtmlAttr(tabid) + '">' + VuFind.loading() + '</div>'); + return $('<div role="tabpanel" class="tab-pane ' + escapeHtmlAttr(tabid) + '-tab" aria-labelledby="record-tab-' + escapeHtmlAttr(tabid) + '">' + VuFind.loading() + '</div>'); } function backgroundLoadTab(tabid) { @@ -340,12 +340,12 @@ function removeCheckRouteParam() { function recordDocReady() { removeCheckRouteParam(); - $('.record-tabs .nav-tabs li').attr('aria-selected', 'false'); - $('.record-tabs .nav-tabs .initiallyActive').attr('aria-selected', 'true'); + $('.record-tabs .nav-tabs a').attr('aria-selected', 'false'); + $('.record-tabs .nav-tabs .initiallyActive a').attr('aria-selected', 'true'); // update aria-selected attributes after a tab has been shown $('.record-tabs .nav-tabs a').on('shown.bs.tab', function shownTab(e) { - $('.record-tabs .nav-tabs li').attr('aria-selected', 'false'); - $(e.target).parent().attr('aria-selected', 'true'); + $('.record-tabs .nav-tabs a').attr('aria-selected', 'false'); + $(e.target).attr('aria-selected', 'true'); }); $('.record-tabs .nav-tabs a').on('click', function recordTabsClick() { var $li = $(this).parent(); diff --git a/themes/bootstrap5/templates/collection/view.phtml b/themes/bootstrap5/templates/collection/view.phtml index 880649a2a84..d353184fe5d 100644 --- a/themes/bootstrap5/templates/collection/view.phtml +++ b/themes/bootstrap5/templates/collection/view.phtml @@ -77,8 +77,8 @@ $tabClasses[] = 'noajax'; } ?> - <li class="<?=implode(' ', $tabClasses)?>" role="tab" data-tab="<?=$this->escapeHtmlAttr($tabName)?>"<?php if ($obj->supportsAjax() && in_array($tab, $this->backgroundTabs)):?> data-background<?php endif ?>> - <a href="<?=$this->escapeHtmlAttr($this->recordLinker()->getTabUrl($this->driver, $tab))?>#tabnav" data-lightbox-ignore> + <li role="presentation" id="record-tab-<?=$this->escapeHtmlAttr($tabName)?>" class="<?=implode(' ', $tabClasses)?>" data-tab="<?=$this->escapeHtmlAttr($tabName)?>"<?php if ($obj->supportsAjax() && in_array($tab, $this->backgroundTabs)):?> data-background<?php endif ?>> + <a role="tab" href="<?=$this->escapeHtmlAttr($this->recordLinker()->getTabUrl($this->driver, $tab))?>#tabnav" data-lightbox-ignore> <?=$this->transEsc($desc)?> </a> </li> @@ -87,7 +87,7 @@ <div class="tab-content collectionDetails<?=$tree ? 'Tree' : ''?>"> <?php if (!$this->loadInitialTabWithAjax || !isset($activeTabObj) || !$activeTabObj->supportsAjax()): ?> - <div class="tab-pane active <?=$this->escapeHtmlAttr($this->activeTab) ?>-tab"> + <div role="tabpanel" class="tab-pane active <?=$this->escapeHtmlAttr($this->activeTab) ?>-tab"> <?=isset($activeTabObj) ? $this->record($this->driver)->getTab($activeTabObj) : '' ?> </div> <?php endif; ?> diff --git a/themes/bootstrap5/templates/record/view.phtml b/themes/bootstrap5/templates/record/view.phtml index db8b030a767..a0e2d897e64 100644 --- a/themes/bootstrap5/templates/record/view.phtml +++ b/themes/bootstrap5/templates/record/view.phtml @@ -69,8 +69,8 @@ $tabClasses[] = 'noajax'; } ?> - <li id="record-tab-<?=$this->escapeHtmlAttr($tabName)?>" class="<?=implode(' ', $tabClasses)?>" role="tab" data-tab="<?=$this->escapeHtmlAttr($tabName)?>"<?php if ($obj->supportsAjax() && in_array($tab, $this->backgroundTabs)):?> data-background<?php endif ?>> - <a href="<?=$this->escapeHtmlAttr($this->recordLinker()->getTabUrl($this->driver, $tab))?>#tabnav" data-lightbox-ignore> + <li role="presentation" id="record-tab-<?=$this->escapeHtmlAttr($tabName)?>" class="<?=implode(' ', $tabClasses)?>" data-tab="<?=$this->escapeHtmlAttr($tabName)?>"<?php if ($obj->supportsAjax() && in_array($tab, $this->backgroundTabs)):?> data-background<?php endif ?>> + <a role="tab" href="<?=$this->escapeHtmlAttr($this->recordLinker()->getTabUrl($this->driver, $tab))?>#tabnav" data-lightbox-ignore> <?=$this->transEsc($desc)?> </a> </li> @@ -79,7 +79,7 @@ <div class="tab-content"> <?php if (!$this->loadInitialTabWithAjax || !isset($activeTabObj) || !$activeTabObj->supportsAjax()): ?> - <div class="tab-pane active <?=$this->escapeHtmlAttr($this->activeTab) ?>-tab" aria-labelledby="record-tab-<?=$this->escapeHtmlAttr($activeTabName ?? '')?>"> + <div role="tabpanel" class="tab-pane active <?=$this->escapeHtmlAttr($this->activeTab) ?>-tab" aria-labelledby="record-tab-<?=$this->escapeHtmlAttr($activeTabName ?? '')?>"> <?=isset($activeTabObj) ? $this->record($this->driver)->getTab($activeTabObj) : '' ?> </div> <?php endif; ?> diff --git a/themes/finna2/js/finna-feed-element.js b/themes/finna2/js/finna-feed-element.js index e5392622142..7d7ed90d906 100644 --- a/themes/finna2/js/finna-feed-element.js +++ b/themes/finna2/js/finna-feed-element.js @@ -134,10 +134,14 @@ class FinnaFeedElement extends HTMLElement { var settings = Object.assign({}, jsonResponse.data.settings); settings.height = settings.height || 300; const type = settings.type; + const titleElement = this.parentElement.querySelector('.carousel-header'); const carousel = ['carousel', 'carousel-vertical', 'slider'].includes(type); const hasContent = this.querySelector('.list-feed > ul > li, .carousel-feed > li, .feed-grid > div'); if (!hasContent) { this.classList.add('hidden'); + if (titleElement) { + titleElement.classList.add('hidden'); + } this.innerHTML = `<!-- No content received -->`; return; } @@ -360,6 +364,10 @@ class FinnaFeedElement extends HTMLElement { // The catch will catch all the js errors in buildFeedDom, so display a warning // if something happens console.error(responseJSON); + const titleElement = holder.parentElement.querySelector('.carousel-header'); + if (titleElement) { + titleElement.classList.add('hidden'); + } holder.innerHTML = `<!-- Feed could not be loaded: ${responseJSON.data || ''} -->`; }); diff --git a/themes/finna2/less/finna/feed.less b/themes/finna2/less/finna/feed.less index 1560f3f55a3..65989b4b599 100644 --- a/themes/finna2/less/finna/feed.less +++ b/themes/finna2/less/finna/feed.less @@ -10,6 +10,15 @@ } } } +.feed-container .list-feed .feed-header { + color: @list-feed-header-color; +} +.feed-header { + display: inline-block; + color: @list-feed-header-color; + padding: @list-feed-header-padding; + background: @list-feed-header-background; +} .feed-container .list-feed { background: @list-feed-background; padding: @list-feed-padding; @@ -23,12 +32,6 @@ .title { font-weight: normal; } - .feed-header { - display: inline-block; - color: @list-feed-header-color; - padding: @list-feed-header-padding; - background: @list-feed-header-background; - } .more-link { margin-top: 15px; } @@ -571,6 +574,12 @@ } } +.carousel-header { + background: @carousel-header-background; + display: inline-block; + padding: @carousel-header-padding; + color: @carousel-header-color; +} finna-feed { display: block; .feed-content-container { @@ -579,12 +588,6 @@ finna-feed { align-items: center; position: relative; } - .carousel-header { - background: @carousel-header-background; - display: inline-block; - padding: @carousel-header-padding; - color: @carousel-header-color; - } .carousel-hover-title { font-size: 14px; font-weight: 900; @@ -920,9 +923,6 @@ finna-feed.carousel-slider.splide--ltr { position: relative; } } - .carousel-header { - display: none; - } .slider-image, .slider-text-part { height: 100%; padding: 10px 15px; diff --git a/themes/finna2/scss/finna/feed.scss b/themes/finna2/scss/finna/feed.scss index d3c4e04b597..8f3d6faf4cb 100644 --- a/themes/finna2/scss/finna/feed.scss +++ b/themes/finna2/scss/finna/feed.scss @@ -10,6 +10,15 @@ } } } +.feed-container .list-feed .feed-header { + color: $list-feed-header-color; +} +.feed-header { + display: inline-block; + color: $list-feed-header-color; + padding: $list-feed-header-padding; + background: $list-feed-header-background; +} .feed-container .list-feed { background: $list-feed-background; padding: $list-feed-padding; @@ -23,12 +32,6 @@ .title { font-weight: normal; } - .feed-header { - display: inline-block; - color: $list-feed-header-color; - padding: $list-feed-header-padding; - background: $list-feed-header-background; - } .more-link { margin-top: 15px; } @@ -571,6 +574,12 @@ } } +.carousel-header { + background: $carousel-header-background; + display: inline-block; + padding: $carousel-header-padding; + color: $carousel-header-color; +} finna-feed { display: block; .feed-content-container { @@ -579,12 +588,6 @@ finna-feed { align-items: center; position: relative; } - .carousel-header { - background: $carousel-header-background; - display: inline-block; - padding: $carousel-header-padding; - color: $carousel-header-color; - } .carousel-hover-title { font-size: 14px; font-weight: 900; @@ -920,9 +923,6 @@ finna-feed.carousel-slider.splide--ltr { position: relative; } } - .carousel-header { - display: none; - } .slider-image, .slider-text-part { height: 100%; padding: 10px 15px; diff --git a/themes/finna2/scss/functions.scss b/themes/finna2/scss/functions.scss deleted file mode 100644 index 6c275f04117..00000000000 --- a/themes/finna2/scss/functions.scss +++ /dev/null @@ -1,45 +0,0 @@ -// Support functions - -/* #SCSS> */ -$use "sass:math"; - -$function de-gamma($n) { - $if $n <= 0.03928 { - $return math.div($n, 12.92); - } - $else { - $return math.pow((math.div(($n + 0.055), 1.055)), 2.4); - } -} - -$function re-gamma($n) { - $if $n <= 0.0031308 { - $return $n * 12.92; - } $else { - $return 1.055 * math.pow($n, math.div(1, 2.4)) - 0.055; - } - } - -// sRGB BT-709 BRIGHTNESS -$function brightness($c) { - $rlin: de-gamma(math.div(red($c), 255)); - $glin: de-gamma(math.div(green($c), 255)); - $blin: de-gamma(math.div(blue($c), 255)); - $return re-gamma(0.2126 * $rlin + 0.7152 * $glin + 0.0722 * $blin) * 100; -} - -// Compares contrast of a given color to the light/dark arguments and returns whichever is most "contrasty" -$function contrast($color, $dark: #000, $light: #fff) { - $if $color == null { - $return null; - } - - $else { - $color-brightness: brightness($color); - $light-text-brightness: brightness($light); - $dark-text-brightness: brightness($dark); - - $return if(abs($color-brightness - $light-text-brightness) > abs($color-brightness - $dark-text-brightness), $light, $dark); - } -} -/* <#SCSS */ diff --git a/themes/finna2/templates/Helpers/feed.phtml b/themes/finna2/templates/Helpers/feed.phtml index 0699248d617..bba74a4931d 100644 --- a/themes/finna2/templates/Helpers/feed.phtml +++ b/themes/finna2/templates/Helpers/feed.phtml @@ -1,3 +1,8 @@ <!-- START of: finna - Helpers/feed.phtml --> -<finna-feed class="feed-container" feed-id="<?= $this->escapeHtmlAttr($this->id); ?>"></finna-feed> +<div> + <?php if ($this->title !== ''): ?> + <h2 class="carousel-header feed-header<?php if ($this->type === 'slider'):?> sr-only<?php endif;?>"><?=$this->transEsc($this->title); ?></h2> + <?php endif; ?> + <finna-feed class="feed-container" feed-id="<?= $this->escapeHtmlAttr($this->id); ?>" ></finna-feed> +</div> <!-- END of: finna - Helpers/feed.phtml --> diff --git a/themes/finna2/templates/RecordDriver/AipaLrmi/result-list.phtml b/themes/finna2/templates/RecordDriver/AipaLrmi/result-list.phtml new file mode 100644 index 00000000000..0b8f169db60 --- /dev/null +++ b/themes/finna2/templates/RecordDriver/AipaLrmi/result-list.phtml @@ -0,0 +1,180 @@ +<!-- START of: finna - RecordDriver/AipaLrmi/result-list.phtml --> +<?php + $combinedView = !$this->layout()->templateDir || $this->layout()->templateDir === 'combined'; + $img = $this->recordImage($this->record($this->driver)); + $thumbnail = false; + $thumbnailAlignment = $this->record($this->driver)->getThumbnailAlignment('result'); + $inL1Results = $this->layout()->templateDir === 'l1'; + $recordLinker = $this->recordLinker($this->results); + $coverDetails = $this->record($this->driver)->getCoverDetails('result-list', 'medium', $recordLinker->getUrl($this->driver)); + if ($img) { + $thumbnail = $img->render('list', ['small' => ['w' => 158, 'h' => 228], 'medium' => ['w' => 158, 'h' => 228]], 'L1', ['imageClick' => 'open']); + } + $recordType = $this->driver->tryMethod('getRecordType'); +?> +<div class="record-container<?=$recordType ? " record-type-$recordType" : ''?> list-view <?=$this->record($this->driver)->getContainerJsClasses()?>"> + <div class="media"> + <?php if ($thumbnail && $thumbnailAlignment == 'left'): ?> + <div class="media-<?=$thumbnailAlignment ?> <?=$this->escapeHtmlAttr($coverDetails['size'])?>"> + <input type="hidden" value="<?=$this->escapeHtmlAttr($this->driver->getUniqueID())?>" class="hiddenId"> + <input type="hidden" value="<?=$this->escapeHtmlAttr($this->driver->getSourceIdentifier())?>" class="hiddenSource"> + <?=$thumbnail ?> + </div> + <?php endif; ?> + <div class="media-body"> + <div class="result-body"> + <div class="title-container"> + <h2 class="search-title"> + <a class="title" href="<?=$this->escapeHtmlAttr($recordLinker->getUrl($this->driver))?>" data-view="<?=isset($this->params) ? $this->params->getOptions()->getListViewOption() : 'list' ?>" title="<?=$this->escapeHtmlAttr($this->driver->getTitle())?>"> + <?=$this->record($this->driver)->getTitleHtml()?><span class="sr-only"> <?=$this->transEsc('To the record')?></span> + </a> + </h2> + <?php if ($uniformTitles = $this->driver->tryMethod('getUniformTitles')): ?> + <div class="result-uniform-titles"> + <?php foreach ($uniformTitles as $uniformTitle): ?> + <?=$this->escapeHtml($uniformTitle) ?><br> + <?php endforeach; ?> + </div> + <?php endif; ?> + </div> + <?php foreach ($this->driver->tryMethod('getFullTitlesAltScript', [], []) as $altTitle): ?> + <div class="title-alt-script"> + <?=$this->escapeHtml($altTitle)?> + </div> + <?php endforeach; ?> + + <div class="dateyeartype"> + <div class="resultItemFormat"> + <?=$this->record($this->driver)->getFormatList() ?> + <?=$this->record($this->driver)->getLabelList() ?> + </div> + + <?php if ($this->driver->isCollection()): ?> + <?=implode('<br>', $this->driver->getSummary()); ?> + <?php else: ?> + <?php $authors = $this->driver->tryMethod('getAuthorsWithRoles'); + if (!empty($authors)): ?> + <div class="truncate-field" data-rows="1"> + <?php foreach($authors as $index => $author): ?> + <?=($index > 0 ? '; ' : '')?><?=$this->record($this->driver)->getLinkedFieldElement('author', $author['name'], $author, ['date' => true, 'role' => true, 'authorityType' => $author['type'] ?? null])?> + <?php endforeach; ?> + </div> + <?php endif; ?> + <?php $containerTitle = $this->driver->getContainerTitle(); + $summDate = $this->driver->getPublicationDates(); ?> + <?php if (!empty($containerTitle)): ?> + <div class="truncate-field" data-rows="1"> + <?=$this->transEsc('Published in')?> + <?php + $containerSource = $this->driver->getSourceIdentifier(); + $containerID = $this->driver->getContainerRecordID(); + $hostRecords = $this->driver->tryMethod('getHostRecords'); + ?> + <?php if ($hostRecords): ?> + <?php foreach ($hostRecords as $i => $hostRecord): ?> + <?=$i > 0 ? '<br>' : ''?> + <?php if ($hostRecord['id'] || $hostRecord['title']): ?> + <?php $link = $hostRecord['id'] ? $recordLinker->getUrl($hostRecord['sourceId'] . '|' . $hostRecord['id']) : $this->record($this->driver)->getLink('title', $hostRecord['title']); ?> + <a href="<?=$this->escapeHtmlAttr($link)?>"><?=$hostRecord['title'] !== '' ? $hostRecord['title'] : $hostRecord['mainHeading']?></a> + <?php endif; ?> + <?=$this->escapeHtml($hostRecord['reference']); ?> + <?php endforeach; ?> + <?php else: ?> + <?php $link = $containerID ? $recordLinker->getUrl($containerSource . '|' . $containerID) : $this->record($this->driver)->getLink('title', $containerTitle); ?> + <a href="<?=$this->escapeHtmlAttr($link)?>"><?=$this->escapeHtml($containerTitle)?></a> + <?php $ref = $this->driver->getContainerReference(); ?> + <?php if (!empty($ref)): ?> + <?=$this->escapeHtml($ref); ?> + <?php else: ?> + <?=!empty($summDate) ? ' (' . $this->escapeHtml($summDate[0]) . ')' : ''?> + <?php endif; ?> + <?php endif; ?> + </div> + <?php elseif (!empty($summDate)): ?> + <?=!empty($summAuthor) ? '' : ' '?> + <span class="publish-year"><?=$this->escapeHtml($summDate[0])?></span> + <?php endif; ?> + <?php if ($classifications = $this->driver->tryMethod('getClassifications')): ?> + <div class="resultClassification"> + <?php foreach ($classifications as $class => $field): ?> + <?php if (!empty($field[0])): ?> + <?php + $result = $class . ' ' . $field[0]; + $classForTranslation = 'classification::' . str_replace('::', ' ', $result); + if (!$this->translationEmpty($classForTranslation)): + ?> + <a title="<?=$this->escapeHtmlAttr($result)?>" href="<?=$this->record($this->driver)->getLink('classification', $result)?>"><?=$this->transEsc($classForTranslation) . '<span class="classification-detail"> (' . $this->escapeHtml($result) . ')</span>';?></a><br> + <?php endif; ?> + <?php endif; ?> + <?php endforeach; ?> + </div> + <?php endif; ?> + <?php if ($inL1Results): ?> + <div class="targeted-information truncate-field outer" data-rows="2"> + <?php if ($levels = $this->driver->getRootEducationalLevels()): ?> + <?php foreach($levels as $level): ?> + <span class="lrmi-topic"><?=$this->transEsc($level)?></span> + <?php endforeach; ?> + <?php endif; ?> + <?php if ($topics = $this->driver->getTopics()): ?> + <?php foreach($topics as $topic): ?> + <span class="lrmi-topic"><?=$this->transEsc($topic)?></span> + <?php endforeach; ?> + <?php endif; ?> + </div> + <?php if ($rights = $this->driver->getAccessRestrictionsType($this->layout()->userLang)): ?> + <div class="material-rights"> + <span class="rights-header"><?=$this->transEsc('usage_rights_ext')?>:</span> + <?= $this->partial('Helpers/copyright-field-value.phtml', ['data' => $rights]) ?> + </div> + <?php endif; ?> + <?php endif; ?> + <?php endif; ?> + </div> + <?php if ($this->driver->tryMethod('getWorkKeys') && $this->searchOptions($this->driver->getSourceIdentifier())->getVersionsAction()): ?> + <div class="record-versions ajax"></div> + <?php endif; ?> + + <?php $recordHelper = $this->record($this->driver); ?> + <?=$recordHelper->getPreviews();?> + <?=$this->render('list/list-notes.phtml')?> + + <div class="clearfix"></div> + <?php if ($this->userlist()->getMode() !== 'disabled'): ?> + <?php /* Saved lists */ ?> + <div class="savedLists"> + <strong><?=$this->transEsc('Saved in')?>:</strong> + </div> + <?php endif; ?> + + <div class="hidden-print add-to-favorite-col"> + <?php + // Display qrcode if appropriate: + echo $this->record($this->driver)->renderTemplate('result-qrcode.phtml', ['driver' => $this->driver]); + ?> + + <?php if ($this->userlist()->getMode() !== 'disabled'): ?> + <?php /* Add to favorites */ ?> + <div> + <a href="<?=$this->escapeHtmlAttr($recordLinker->getActionUrl($this->driver, 'Save'))?>" class="save-record" data-lightbox data-id="<?=$this->escapeHtmlAttr($this->driver->getUniqueId()) ?>" title="<?=$this->transEsc('Add to favorites')?>" rel="nofollow"><?=$this->icon('favorite') ?><span class="hidden-sm hidden-md hidden-lg"> <?=$this->transEsc('Add to favorites')?></span><span class="sr-only hidden-xs"> <?=$this->transEsc('Add to favorites')?></span></a> + </div> + <?php endif; ?> + + <?php if (isset($this->params) && $this->cart()->isActiveInSearch() && $this->params->getOptions()->supportsCart() && $this->cart()->isActive()): ?> + <?=$this->render('record/cart-buttons.phtml', ['id' => $this->driver->getUniqueId(), 'source' => $this->driver->getSourceIdentifier()]); ?><br> + <?php endif; ?> + + <?=$this->driver->supportsCoinsOpenUrl() ? '<span class="Z3988" aria-hidden="true" title="' . $this->escapeHtmlAttr($this->driver->getCoinsOpenUrl()) . '"></span>' : ''?> + </div> + </div> + </div> + <?php if ($thumbnail && $thumbnailAlignment == 'right'): ?> + <div class="media-<?=$thumbnailAlignment ?> <?=$this->escapeHtmlAttr($coverDetails['size'])?>"> + <input type="hidden" value="<?=$this->escapeHtmlAttr($this->driver->getUniqueID())?>" class="hiddenId"> + <input type="hidden" value="<?=$this->escapeHtmlAttr($this->driver->getSourceIdentifier())?>" class="hiddenSource"> + <?=$thumbnail ?> + </div> + <?php endif; ?> + </div> +</div> +<!-- END of: finna - RecordDriver/AipaLrmi/result-list.phtml --> diff --git a/themes/finna2/templates/RecordDriver/DefaultRecord/core.phtml b/themes/finna2/templates/RecordDriver/DefaultRecord/core.phtml index bfe0836f445..315d1c82233 100644 --- a/themes/finna2/templates/RecordDriver/DefaultRecord/core.phtml +++ b/themes/finna2/templates/RecordDriver/DefaultRecord/core.phtml @@ -207,7 +207,7 @@ </div> <?php endif; ?> - <?php $results = $this->driver->getNonPresenterAuthors();?> + <?php $results = $this->driver->getPrimaryAuthorsExtended();?> <?php if ($results): ?> <div class="recordAuthors"> <div class="truncate-field"> diff --git a/themes/finna2/templates/RecordDriver/DefaultRecord/export-endnote.phtml b/themes/finna2/templates/RecordDriver/DefaultRecord/export-endnote.phtml index 110ce2403af..cec7d2f797a 100644 --- a/themes/finna2/templates/RecordDriver/DefaultRecord/export-endnote.phtml +++ b/themes/finna2/templates/RecordDriver/DefaultRecord/export-endnote.phtml @@ -1,8 +1,9 @@ <?php echo $this->record($this->driver)->getExportFormat('endnote'); +$format = $this->driver->getRecordFormat(); if ($this->driver->getHierarchyType()) { - if ($this->driver->getRecordFormat() === 'ead3') { + if ($format === 'ead3') { if ($archiveAuthors = $this->driver->tryMethod('getAuthorsWithoutRoleHeadings')) { foreach ($archiveAuthors as $archiveAuthor) { if ($archiveAuthor['name']) { @@ -18,6 +19,10 @@ if ($this->driver->getHierarchyType()) { } echo '%A ' . implode(', ', $archiveLocation) . "\n"; } +} elseif ($format === 'marc') { + foreach ($this->driver->tryMethod('getPrimaryAuthors', [], []) as $author) { + echo "%A {$author}\n"; + } } else { foreach ($this->driver->tryMethod('getNonPresenterAuthors', [], []) as $author) { echo "%A {$author['name']}\n"; diff --git a/themes/finna2/templates/RecordDriver/DefaultRecord/export-refworks.phtml b/themes/finna2/templates/RecordDriver/DefaultRecord/export-refworks.phtml index 0747b20fbc0..bf124266c48 100644 --- a/themes/finna2/templates/RecordDriver/DefaultRecord/export-refworks.phtml +++ b/themes/finna2/templates/RecordDriver/DefaultRecord/export-refworks.phtml @@ -61,8 +61,9 @@ if (!empty($containerTitle)) { } } +$recordFormat = $this->driver->getRecordFormat(); if ($this->driver->getHierarchyType()) { - if ($this->driver->getRecordFormat() === 'ead3') { + if ($recordFormat === 'ead3') { if ($archiveAuthors = $this->driver->tryMethod('getAuthorsWithoutRoleHeadings')) { foreach ($archiveAuthors as $archiveAuthor) { if ($archiveAuthor['name']) { @@ -78,6 +79,10 @@ if ($this->driver->getHierarchyType()) { } echo 'A1 ' . implode(', ', $archiveLocation) . "\n"; } +} elseif ($recordFormat === 'marc') { + foreach ($this->driver->tryMethod('getPrimaryAuthors', [], []) as $author) { + echo "A1 {$author}\n"; + } } else { foreach ($this->driver->tryMethod('getNonPresenterAuthors', [], []) as $author) { echo "A1 {$author['name']}\n"; diff --git a/themes/finna2/templates/RecordDriver/DefaultRecord/export-ris.phtml b/themes/finna2/templates/RecordDriver/DefaultRecord/export-ris.phtml index 4f21f480d75..c808d730877 100644 --- a/themes/finna2/templates/RecordDriver/DefaultRecord/export-ris.phtml +++ b/themes/finna2/templates/RecordDriver/DefaultRecord/export-ris.phtml @@ -60,9 +60,13 @@ if (is_array($series)) { } } -$authors = $this->driver->tryMethod('getNonPresenterAuthors') - ?? $this->driver->tryMethod('getPrimaryAuthors') - ?? []; +if ($marc) { + $authors = $this->driver->tryMethod('getPrimaryAuthors', [], []); +} else { + $authors = $this->driver->tryMethod('getNonPresenterAuthors') + ?? $this->driver->tryMethod('getPrimaryAuthors') + ?? []; +} foreach ($authors as $author) { $name = $author['name'] ?? $author; diff --git a/themes/finna2/templates/RecordDriver/DefaultRecord/result-condensed.phtml b/themes/finna2/templates/RecordDriver/DefaultRecord/result-condensed.phtml index 48e5fb7db43..95024a0b516 100644 --- a/themes/finna2/templates/RecordDriver/DefaultRecord/result-condensed.phtml +++ b/themes/finna2/templates/RecordDriver/DefaultRecord/result-condensed.phtml @@ -57,7 +57,7 @@ <?php if ($this->driver->isCollection()): ?> <?=implode('<br>', $this->driver->getSummary()); ?> <?php else: ?> - <?php $authors = $this->driver->tryMethod('getAuthorsWithRoles'); + <?php $authors = $this->driver->tryMethod('getPrimaryAuthorsExtended'); if (!empty($authors)): ?> <?php $author = $authors[0]; ?> <?=$this->record($this->driver)->getLinkedFieldElement('author', $author['name'], $author, ['authorityType' => $author['type'] ?? null])?> diff --git a/themes/finna2/templates/RecordDriver/DefaultRecord/result-list.phtml b/themes/finna2/templates/RecordDriver/DefaultRecord/result-list.phtml index e356d26aea0..a2dd2ced3a7 100644 --- a/themes/finna2/templates/RecordDriver/DefaultRecord/result-list.phtml +++ b/themes/finna2/templates/RecordDriver/DefaultRecord/result-list.phtml @@ -72,7 +72,7 @@ <?php if ($this->driver->isCollection()): ?> <?=implode('<br>', $this->driver->getSummary()); ?> <?php else: ?> - <?php $authors = $this->driver->tryMethod('getAuthorsWithRoles'); + <?php $authors = $this->driver->tryMethod('getPrimaryAuthorsExtended'); if (!empty($authors)): ?> <div class="truncate-field" data-rows="1" tabindex="-1"> <?php foreach($authors as $index => $author): ?> diff --git a/themes/finna2/templates/RecordDriver/SolrAipa/result-list.phtml b/themes/finna2/templates/RecordDriver/SolrAipa/result-list.phtml index fe07f2e73a1..cfccab9e807 100644 --- a/themes/finna2/templates/RecordDriver/SolrAipa/result-list.phtml +++ b/themes/finna2/templates/RecordDriver/SolrAipa/result-list.phtml @@ -1,217 +1 @@ -<!-- START of: finna - RecordDriver/SolrAipa/result-list.phtml --> -<?php - $combinedView = !$this->layout()->templateDir || $this->layout()->templateDir === 'combined'; - $img = $this->recordImage($this->record($this->driver)); - $thumbnail = false; - $thumbnailAlignment = $this->record($this->driver)->getThumbnailAlignment('result'); - $recordLinker = $this->recordLinker($this->results); - $coverDetails = $this->record($this->driver)->getCoverDetails('result-list', 'medium', $recordLinker->getUrl($this->driver)); - if ($img) { - $thumbnail = $img->render('list', ['small' => ['w' => 100, 'h' => 100], 'medium' => ['w' => 250, 'h' => 250]], 'Solr', ['imageClick' => 'open']); - } -?> -<div class="record-container list-view <?=$this->record($this->driver)->getContainerJsClasses()?>"> - <div class="media"> - <?php if ($thumbnail && $thumbnailAlignment == 'left'): ?> - <div class="media-<?=$thumbnailAlignment ?> <?=$this->escapeHtmlAttr($coverDetails['size'])?>"> - <input type="hidden" value="<?=$this->escapeHtmlAttr($this->driver->getUniqueID())?>" class="hiddenId"> - <input type="hidden" value="<?=$this->escapeHtmlAttr($this->driver->getSourceIdentifier())?>" class="hiddenSource"> - <?=$thumbnail ?> - </div> - <?php endif; ?> - <div class="media-body"> - <div class="result-body"> - <div class="title-container"> - <h2 class="search-title"> - <a class="title" href="<?=$this->escapeHtmlAttr($recordLinker->getUrl($this->driver))?>" data-view="<?=isset($this->params) ? $this->params->getOptions()->getListViewOption() : 'list' ?>" title="<?=$this->escapeHtmlAttr($this->driver->getTitle())?>"> - <?=$this->record($this->driver)->getTitleHtml()?><span class="sr-only"> <?=$this->transEsc('To the record')?></span> - </a> - </h2> - <?php if ($altTitles = $this->driver->tryMethod('getAlternativeTitles')): ?> - <div class="result-alt-titles truncate-field" data-rows="5"> - <?=implode('<br>', array_map([$this, 'escapeHtml'], $altTitles))?> - </div> - <?php endif; ?> - <?php if ($uniformTitles = $this->driver->tryMethod('getUniformTitles')): ?> - <div class="result-uniform-titles"> - <?php foreach ($uniformTitles as $uniformTitle): ?> - <?=$this->escapeHtml($uniformTitle) ?><br> - <?php endforeach; ?> - </div> - <?php endif; ?> - </div> - <?php foreach ($this->driver->tryMethod('getFullTitlesAltScript', [], []) as $altTitle): ?> - <div class="title-alt-script"> - <?=$this->escapeHtml($altTitle)?> - </div> - <?php endforeach; ?> - - <div class="dateyeartype"> - <div class="resultItemFormat"> - <?=$this->record($this->driver)->getFormatList() ?> - <?=$this->record($this->driver)->getLabelList() ?> - </div> - - <?php if ($results = $this->driver->tryMethod('getOtherLinks')): ?> - <div class="resultOtherLinks"> - <?php $prevOtherLinkHeading = ''; ?> - <?php foreach ($results as $result): ?> - <?=$prevOtherLinkHeading != $result['heading'] ? $this->transEsc('default::link_' . $result['heading'], null, $result['heading']) : ' ' ?> - <?php if (!empty($result['isn'])): ?> - <?php $otherLinkTitle = !empty($result['title']) ? $result['title'] : $result['isn']; ?> - <a title="<?=$this->escapeHtmlAttr($otherLinkTitle) ?>" href="<?=$this->record($this->driver)->getLink('isn', $result['isn']) ?>"> - <?=$this->escapeHtml($otherLinkTitle); ?> - </a> - <?php elseif (!empty($result['title'])): ?> - <a title="<?=$this->escapeHtmlAttr($result['title']) ?>" href="<?=$this->record($this->driver)->getLink('title', $result['title']) ?>"> - <?=$this->escapeHtml($result['title']); ?> - </a> - <?php endif; ?> - <?php if (!empty($result['author'])): ?> - (<?=$this->escapeHtml($result['author']); ?>) - <?php endif; ?> - <?php endforeach; ?> - </div> - <?php endif; ?> - - <?php if ($this->driver->isCollection()): ?> - <?=implode('<br>', $this->driver->getSummary()); ?> - <?php else: ?> - <?php $authors = $this->driver->tryMethod('getAuthorsWithRoles'); - if (!empty($authors)): ?> - <div class="truncate-field" data-rows="1"> - <?php foreach($authors as $index => $author): ?> - <?=($index > 0 ? '; ' : '')?><?=$this->record($this->driver)->getLinkedFieldElement('author', $author['name'], $author, ['date' => true, 'role' => true, 'authorityType' => $author['type'] ?? null])?> - <?php endforeach; ?> - </div> - <?php endif; ?> - <?php $containerTitle = $this->driver->getContainerTitle(); - $summDate = $this->driver->getPublicationDateRange(); ?> - <?php if (!empty($containerTitle)): ?> - <div class="truncate-field" data-rows="1"> - <?=$this->transEsc('Published in')?> - <?php - $containerSource = $this->driver->getSourceIdentifier(); - $containerID = $this->driver->getContainerRecordID(); - $hostRecords = $this->driver->tryMethod('getHostRecords'); - ?> - <?php if ($hostRecords): ?> - <?php foreach ($hostRecords as $i => $hostRecord): ?> - <?=$i > 0 ? '<br>' : ''?> - <?php if ($hostRecord['id'] || $hostRecord['title']): ?> - <?php $link = $hostRecord['id'] ? $recordLinker->getUrl($hostRecord['sourceId'] . '|' . $hostRecord['id']) : $this->record($this->driver)->getLink('title', $hostRecord['title']); ?> - <a href="<?=$this->escapeHtmlAttr($link)?>"><?=$hostRecord['title'] !== '' ? $hostRecord['title'] : $hostRecord['mainHeading']?></a> - <?php endif; ?> - <?=$this->escapeHtml($hostRecord['reference']); ?> - <?php endforeach; ?> - <?php else: ?> - <?php $link = $containerID ? $recordLinker->getUrl($containerSource . '|' . $containerID) : $this->record($this->driver)->getLink('title', $containerTitle); ?> - <a href="<?=$this->escapeHtmlAttr($link)?>"><?=$this->escapeHtml($containerTitle)?></a> - <?php $ref = $this->driver->getContainerReference(); ?> - <?php if (!empty($ref)): ?> - <?=$this->escapeHtml($ref); ?> - <?php else: ?> - <?=!empty($summDate) ? ' (' . implode('–', array_map($this->escapeHtml, $summDate)) . ')' : ''?> - <?php endif; ?> - <?php endif; ?> - </div> - <?php endif; ?> - <?php if ($classifications = $this->driver->tryMethod('getClassifications')): ?> - <div class="resultClassification"> - <?php foreach ($classifications as $class => $field): ?> - <?php if (!empty($field[0])): ?> - <?php - $result = $class . ' ' . $field[0]; - $classForTranslation = 'classification::' . str_replace('::', ' ', $result); - if (!$this->translationEmpty($classForTranslation)): - ?> - <a title="<?=$this->escapeHtmlAttr($result)?>" href="<?=$this->record($this->driver)->getLink('classification', $result)?>"><?=$this->transEsc($classForTranslation) . '<span class="classification-detail"> (' . $this->escapeHtml($result) . ')</span>';?></a><br> - <?php endif; ?> - <?php endif; ?> - <?php endforeach; ?> - </div> - <?php endif; ?> - - <?php $summInCollection = $this->driver->getContainingCollections(); - if (!empty($summInCollection)): ?> - <?php foreach ($summInCollection as $collId => $collText): ?> - <div> - <b><?=$this->transEsc('in_collection_label')?></b> - <a class="collectionLinkText" href="<?=$this->url('collection', ['id' => $collId])?>?recordID=<?=urlencode($this->driver->getUniqueID())?>"> - <?=$this->escapeHtml($collText)?> - </a> - </div> - <?php endforeach; ?> - <?php endif; ?> - <?php endif; ?> - </div> - <?php if (!$this->driver->isCollection()): ?> - <?php if ($snippet = $this->driver->getHighlightedSnippet()): ?> - <?php if (!empty($snippet['snippet'])): ?> - <?=$this->translate('highlight_snippet_html', ['%%snippet%%' => $this->highlight($snippet['snippet'])]) ?><br> - <?php endif; ?> - <?php endif; ?> - <?php endif; ?> - - <?php if ($this->driver->tryMethod('getWorkKeys') && $this->searchOptions($this->driver->getSourceIdentifier())->getVersionsAction()): ?> - <div class="record-versions ajax"></div> - <?php endif; ?> - - <?php $recordHelper = $this->record($this->driver); ?> - <?=$recordHelper->getOnlineUrls('results');?> - <?=$recordHelper->getPreviews();?> - <?=$this->render('list/list-notes.phtml')?> - - <div class="clearfix"></div> - <?php if ($this->userlist()->getMode() !== 'disabled'): ?> - <?php /* Saved lists */ ?> - <div class="savedLists"> - <strong><?=$this->transEsc('Saved in')?>:</strong> - </div> - <?php endif; ?> - - <?php /* Hierarchy tree link */ ?> - <?php $trees = $this->driver->tryMethod('getHierarchyTrees'); - if (!empty($trees)): ?> - <?php foreach ($trees as $hierarchyID => $hierarchyTitle): ?> - <div class="hierarchyTreeLink"> - <input type="hidden" value="<?=$this->escapeHtmlAttr($hierarchyID)?>" class="hiddenHierarchyId"> - <?=$this->icon('hierarchy-tree') ?> - <a class="hierarchyTreeLinkText" data-lightbox href="<?=$this->escapeHtmlAttr($recordLinker->getTabUrl($this->driver, 'HierarchyTree', ['hierarchy' => $hierarchyID]))?>#tabnav" title="<?=$this->transEsc('hierarchy_tree')?>"> - <?=$this->transEsc('hierarchy_view_context')?><?php if (count($trees) > 1): ?>: <?=$this->escapeHtml($hierarchyTitle)?><?php endif; ?> - </a> - </div> - <?php endforeach; ?> - <?php endif; ?> - - <div class="hidden-print add-to-favorite-col"> - <?php - // Display qrcode if appropriate: - echo $this->record($this->driver)->renderTemplate('result-qrcode.phtml', ['driver' => $this->driver]); - ?> - - <?php if ($this->userlist()->getMode() !== 'disabled'): ?> - <?php /* Add to favorites */ ?> - <div> - <a href="<?=$this->escapeHtmlAttr($recordLinker->getActionUrl($this->driver, 'Save'))?>" class="save-record" data-lightbox data-id="<?=$this->escapeHtmlAttr($this->driver->getUniqueId()) ?>" title="<?=$this->transEsc('Add to favorites')?>" rel="nofollow"><?=$this->icon('favorite') ?><span class="hidden-sm hidden-md hidden-lg"> <?=$this->transEsc('Add to favorites')?></span><span class="sr-only hidden-xs"> <?=$this->transEsc('Add to favorites')?></span></a> - </div> - <?php endif; ?> - - <?php if (isset($this->params) && $this->cart()->isActiveInSearch() && $this->params->getOptions()->supportsCart() && $this->cart()->isActive()): ?> - <?=$this->render('record/cart-buttons.phtml', ['id' => $this->driver->getUniqueId(), 'source' => $this->driver->getSourceIdentifier()]); ?><br> - <?php endif; ?> - - <?=$this->driver->supportsCoinsOpenUrl() ? '<span class="Z3988" aria-hidden="true" title="' . $this->escapeHtmlAttr($this->driver->getCoinsOpenUrl()) . '"></span>' : ''?> - </div> - </div> - </div> - <?php if ($thumbnail && $thumbnailAlignment == 'right'): ?> - <div class="media-<?=$thumbnailAlignment ?> <?=$this->escapeHtmlAttr($coverDetails['size'])?>"> - <input type="hidden" value="<?=$this->escapeHtmlAttr($this->driver->getUniqueID())?>" class="hiddenId"> - <input type="hidden" value="<?=$this->escapeHtmlAttr($this->driver->getSourceIdentifier())?>" class="hiddenSource"> - <?=$thumbnail ?> - </div> - <?php endif; ?> - </div> -</div> -<!-- END of: finna - RecordDriver/SolrAipa/result-list.phtml --> +<?= $this->aipa($this->driver)->renderTemplate('result-list.phtml'); diff --git a/themes/finna2/templates/RecordDriver/SolrForward/result-condensed.phtml b/themes/finna2/templates/RecordDriver/SolrForward/result-condensed.phtml new file mode 100644 index 00000000000..b54bfd9af12 --- /dev/null +++ b/themes/finna2/templates/RecordDriver/SolrForward/result-condensed.phtml @@ -0,0 +1,195 @@ +<!-- START of: finna - RecordDriver/SolrForward/result-condensed.phtml --> +<?php + $recordLinker = $this->recordLinker($this->results); + $formats = $this->driver->tryMethod('getFormats'); + $database = $this->record($this->driver)->getFormatClass(end($formats)) === '0database'; + $ejournal = $this->record($this->driver)->getFormatClass(end($formats)) === '1journaleserial'; + // Thumbnail + $img = $this->recordImage($this->record($this->driver)); + $thumbnail = false; + $thumbnailAlignment = $this->record($this->driver)->getThumbnailAlignment('result'); + if ($img): + ob_start(); ?> + <?=$img->render('list', ['small' => ['w' => 100, 'h' => 100], 'medium' => ['w' => 250]]) ?> + <?php $thumbnail = ob_get_contents(); ?> + <?php ob_end_clean(); ?> +<?php endif;?> +<div class="record-container list-view <?=$this->record($this->driver)->getContainerJsClasses()?>"> + <div class="condensed-collapse-toggle"> + <div class="condensed-body"> + <?=$this->icon('condensed-expand', 'condensed-expand') ?><?=$this->icon('condensed-collapse', 'condensed-collapse') ?> + <h2 class="title"> + <a href="<?=$this->escapeHtmlAttr($recordLinker->getUrl($this->driver))?>" class="title" title="<?=$this->escapeHtmlAttr($this->driver->getTitle())?>"> + <?=$this->record($this->driver)->getTitleHtml()?><span class="sr-only"> <?=$this->transEsc('To the record')?></span> + </a> + </h2> + <?php if ($uniformTitles = $this->driver->tryMethod('getUniformTitles')): ?> + <div class="result-uniform-titles"> + <?=implode(' – ', array_map($this->escapeHtml, $uniformTitles)) ?> + </div> + <?php endif; ?> + + <div class="resultOtherLinks"> + <div class="resultItemFormat"> + <?=$this->record($this->driver)->getFormatList() ?> + <?=$this->record($this->driver)->getLabelList() ?> + </div> + <?php if ($results = $this->driver->tryMethod('getOtherLinks')): ?> + <?php $prevOtherLinkHeading = ''; ?> + <?php foreach ($results as $result): ?> + <?=$prevOtherLinkHeading != $result['heading'] ? $this->transEsc('default::link_' . $result['heading'], null, $result['heading']) : ' ' ?> + <?php if (!empty($result['isn'])): ?> + <?php $otherLinkTitle = !empty($result['title']) ? $result['title'] : $result['isn']; ?> + <a title="<?=$this->escapeHtmlAttr($otherLinkTitle) ?>" href="<?=$this->record($this->driver)->getLink('isn', $result['isn']) ?>"> + <?=$this->escapeHtml($otherLinkTitle); ?> + </a> + <?php elseif (!empty($result['title'])): ?> + <a title="<?=$this->escapeHtmlAttr($result['title']) ?>" href="<?=$this->record($this->driver)->getLink('title', $result['title']) ?>"> + <?=$this->escapeHtml($result['title']); ?> + </a> + <?php endif; ?> + <?php if (!empty($result['author'])): ?> + (<?=$this->escapeHtml($result['author']); ?>) + <?php endif; ?> + <?php endforeach; ?> + <?php endif; ?> + + <?php if ($this->driver->isCollection()): ?> + <?=implode('<br>', $this->driver->getSummary()); ?> + <?php else: ?> + <?php $authors = $this->driver->tryMethod('getAuthorsWithRoles'); + if (!empty($authors)): ?> + <?php $author = $authors[0]; ?> + <?=$this->record($this->driver)->getLinkedFieldElement('author', $author['name'], $author, ['authorityType' => $author['type'] ?? null])?> + <?php if (count($authors) > 1): ?> <?=$this->transEsc('more_authors_abbrev')?><?php endif; ?> + <?php endif; ?> + <?php $journalTitle = $this->driver->getContainerTitle(); + $summDate = $this->driver->getPublicationDates(); ?> + <?php if (!empty($journalTitle)): ?> + <?=!empty($summAuthor) ? '' : ' '?> + <?php + $parentSource = $this->driver->getSourceIdentifier(); + $parentId = $this->driver->tryMethod('getHierarchyParentId'); + $parentLink = false; + if ($parentId) { + try { + $parentLink = $this->escapeHtmlAttr($recordLinker->getUrl("$parentSource|{$parentId[0]}")); + } catch (Exception $e) { + // Probably parent record doesn't exist, fall back to linking by title + } + } + if ($parentLink === false) { + $parentLink = $this->record($this->driver)->getLink('journaltitle', str_replace(['{{{{START_HILITE}}}}', '{{{{END_HILITE}}}}'], '', $journalTitle)); + } + ?> + <?=/* TODO: handle highlighting more elegantly here */ $this->transEsc('Published in') . ' <a href="' . $parentLink . '">' . $this->highlight($journalTitle) . '</a>';?> + <?php $ref = $this->driver->getContainerReference(); + if (!empty($ref)): ?> + <?=$this->escapeHtml($ref); ?> + <?php else: ?> + <?=!empty($summDate) ? ' (' . $this->escapeHtml($summDate[0]) . ')' : ''?> + <?php endif; ?> + <?php elseif (!empty($summDate)): ?> + <?=!empty($summAuthor) ? '' : ' '?> + <span class="publish-year"><?=$this->escapeHtml($summDate[0])?></span> + <?php endif; ?> + </div> + </div> + <div class="condensed-links"> + <?=$this->record($this->driver)->getOnlineUrls('results')?> + <?php $formats = $this->driver->getFormats(); ?> + <?=$this->record($this->driver)->getPreviews()?> + </div> + <div class="condensed-notes"> + <?=$this->render('list/list-notes.phtml')?> + </div> + </div> + <div class="clearfix"></div> + <div class="media condensed-collapse-data"> + <?php if (!$database && !$ejournal): ?> + <?php if ($thumbnail && $thumbnailAlignment == 'left'): ?> + <div class="media-left resultThumb"> + <input type="hidden" value="<?=$this->escapeHtmlAttr($this->driver->getUniqueID())?>" class="hiddenId"> + <input type="hidden" value="<?=$this->escapeHtmlAttr($this->driver->getSourceIdentifier())?>" class="hiddenSource"> + <?=$thumbnail ?> + </div> + <?php endif; ?> + <div class="media-body"> + <div class="result-body"> + <?php $classifications = $this->driver->tryMethod('getClassifications'); + if (!empty($classifications)): ?> + <div class="resultClassification"> + <?php + $result = ''; + $i = 0; + foreach ($classifications as $class => $field) { + foreach ($field as $j => $subfield) { + $result = $this->escapeHtml($class) . ' ' . $this->escapeHtml($subfield); + ?> + <a title="<?=$this->escapeHtmlAttr($result)?>" href="<?=$this->record($this->driver)->getLink('classification', $result)?>"><?php if (!$this->translationEmpty('classification::' . $result)): ?><?=$this->transEsc('classification::' . $result) . '<span class="classification-detail"> (' . $result . ')</span>';?><?php else: ?><?= $result; ?><?php endif; ?></a> + <?php + } + } + ?> + </div> + <?php endif; ?> + + <?php $summInCollection = $this->driver->getContainingCollections(); + if (!empty($summInCollection)): ?> + <?php foreach ($summInCollection as $collId => $collText): ?> + <div> + <b><?=$this->transEsc('in_collection_label')?></b> + <a class="collectionLinkText" href="<?=$this->url('collection', ['id' => $collId])?>?recordID=<?=urlencode($this->driver->getUniqueID())?>"> + <?=$this->escapeHtml($collText)?> + </a> + </div> + <?php endforeach; ?> + <?php endif; ?> + + <?= $this->record($this->driver)->renderTemplate('holdings.phtml'); ?> + <div class="hidden-print add-to-favorite-col"> + <?php + // Display qrcode if appropriate: + echo $this->record($this->driver)->renderTemplate('result-qrcode.phtml', ['driver' => $this->driver]); + ?> + + <?php if ($this->userlist()->getMode() !== 'disabled'): ?> + <?php /* Add to favorites */ ?> + <a href="<?=$this->escapeHtmlAttr($recordLinker->getActionUrl($this->driver, 'Save'))?>" class="save-record" data-lightbox data-id="<?=$this->escapeHtmlAttr($this->driver->getUniqueId()) ?>" title="<?=$this->transEsc('Add to favorites')?>" rel="nofollow"><?=$this->icon('favorite') ?> <span class="hidden-sm hidden-md hidden-lg"><?=$this->transEsc('Add to favorites')?></span> <span class="sr-only hidden-xs"><?=$this->transEsc('Add to favorites')?></span></a> + <?php endif; ?> + + <?=$this->driver->supportsCoinsOpenUrl() ? '<span class="Z3988" aria-hidden="true" title="' . $this->escapeHtmlAttr($this->driver->getCoinsOpenUrl()) . '"></span>' : ''?> + </div> + <?php /* Hierarchy tree link */ ?> + <?php $trees = $this->driver->tryMethod('getHierarchyTrees'); + if (!empty($trees)): ?> + <?php foreach ($trees as $hierarchyID => $hierarchyTitle): ?> + <div class="hierarchyTreeLink"> + <input type="hidden" value="<?=$this->escapeHtmlAttr($hierarchyID)?>" class="hiddenHierarchyId"> + <?=$this->icon('hierarchy-tree') ?> + <a class="hierarchyTreeLinkText" data-lightbox href="<?=$this->escapeHtmlAttr($recordLinker->getTabUrl($this->driver, 'HierarchyTree', ['hierarchy' => $hierarchyID]))?>#tabnav" title="<?=$this->transEsc('hierarchy_tree')?>" data-lightbox-href="<?=$this->escapeHtmlAttr($recordLinker->getTabUrl($this->driver, 'AjaxTab', ['hierarchy' => $hierarchyID]))?>" data-lightbox-post="tab=hierarchytree"> + <?=$this->transEsc('hierarchy_view_context')?><?php if (count($trees) > 1): ?>: <?=$this->escapeHtml($hierarchyTitle)?><?php endif; ?> + </a> + </div> + <?php endforeach; ?> + <?php endif; ?> + </div> + </div> + <?php if ($thumbnail && $thumbnailAlignment === 'right'): ?> + <div class="media-right resultThumb"> + <input type="hidden" value="<?=$this->escapeHtmlAttr($this->driver->getUniqueID())?>" class="hiddenId"> + <input type="hidden" value="<?=$this->escapeHtmlAttr($this->driver->getSourceIdentifier())?>" class="hiddenSource"> + <?=$thumbnail ?> + </div> + <?php endif; ?> + <?php endif; ?><?php /* this will end layout for format other than database or e-magazines */ ?> + <?php if ($database): ?> + <?=$this->record($this->driver)->renderTemplate('result-condensed-database.phtml'); ?> + <?php elseif ($ejournal): ?> + <?=$this->record($this->driver)->renderTemplate('result-condensed-ejournal.phtml'); ?> + <?php endif; ?> + <?php endif; ?> + <div class="clearfix visible-xs"></div> + </div> +</div> +<!-- END of: finna - RecordDriver/SolrForward/result-condensed.phtml --> diff --git a/themes/finna2/templates/ajax/feed-carousel.phtml b/themes/finna2/templates/ajax/feed-carousel.phtml index 055da859068..ce5e0ebc6a7 100644 --- a/themes/finna2/templates/ajax/feed-carousel.phtml +++ b/themes/finna2/templates/ajax/feed-carousel.phtml @@ -2,10 +2,9 @@ <?php $hasLinkText = isset($this->linkText); $linkTextEsc = $this->transEsc($this->linkText ? $this->linkText : 'To the record'); - $feedTitleEsc = !empty($this->translateTitle) ? $this->transEsc($this->translateTitle) : $this->escapeHtml($this->title ?? ''); - if ($feedTitleEsc): + if (!$this->translateTitle): // Translated title is displayed in the container, so ignore it here ?> - <h2 class="carousel-header"><?=$feedTitleEsc?></h2> + <h2 class="carousel-header <?php if ($this->type === 'slider'):?> sr-only<?php endif;?>"><?=$this->escapeHtml($this->title)?></h2> <?php endif; ?> <?php if (isset($this->description)): ?> <p class="additional-description"><?=$this->translate($this->description) // HTML, intentionally left unescaped ?></p> @@ -19,7 +18,7 @@ <?=$this->icon('chevron-right')?> </button> </div> - <div class="splide__track carousel"> + <div class="splide__track carousel-inner"> <ul class="splide__list carousel-feed"> <?php $items = isset($this->visualItems) ? array_slice($this->items, 0, $this->visualItems) : $this->items; ?> <?php foreach ($items as $item): ?> @@ -49,7 +48,7 @@ } ?> <a class="feed-link" <?=$this->htmlElement()->getAttributes($linkElement)?>> - <?=$this->partial("ajax/feed-$this->type-item.phtml", compact('item', 'linkTextEsc', 'hasLinkText', 'feedTitleEsc'))?> + <?=$this->context($this)->renderInContext("ajax/feed-$this->type-item.phtml", compact('item', 'linkTextEsc', 'hasLinkText'))?> </a> <?php else: ?> <?=$this->partial("ajax/feed-$this->type-item.phtml", compact('item', 'linkTextEsc', 'hasLinkText'))?> diff --git a/themes/finna2/templates/ajax/feed-list.phtml b/themes/finna2/templates/ajax/feed-list.phtml index 66061d247e6..b8447733a3f 100644 --- a/themes/finna2/templates/ajax/feed-list.phtml +++ b/themes/finna2/templates/ajax/feed-list.phtml @@ -1,6 +1,8 @@ <!-- START of: finna - ajax/feed-list.phtml --> <div class="list-feed<?= $this->images ? ' with-image' : ''; ?>"> -<?php if ($this->title || $this->translateTitle): ?><h2 class="feed-header"><?= $this->translateTitle ? $this->transEsc($this->translateTitle) : $this->escapeHtml($this->title) ?></h2><?php endif; ?> +<?php if (!$this->translateTitle): // Translated title is displayed in the container, so ignore it here ?> + <h2 class="feed-header"><?= $this->escapeHtml($this->title) ?></h2> +<?php endif; ?> <?php if (isset($this->description)): ?><p class="additional-description"><?= $this->translate($this->description) // HTML, intentionally left unescaped ?></p><?php endif; ?> <ul> <?php $items = isset($this->visualItems) ? array_slice($this->items, 0, $this->visualItems) : $this->items; ?> diff --git a/themes/finna2/templates/ajax/feed-slider-item.phtml b/themes/finna2/templates/ajax/feed-slider-item.phtml index 137cb837d77..1835ca1d2b3 100644 --- a/themes/finna2/templates/ajax/feed-slider-item.phtml +++ b/themes/finna2/templates/ajax/feed-slider-item.phtml @@ -2,6 +2,7 @@ <?php $hasIcon = !empty($item['icon']['name']); $hasImg = !empty($item['image']['url']); + $feedTitleEsc = $this->translateTitle ? $this->transEsc($this->title) : $this->escapeHtml($this->title ?? ''); ?> <?php if ($hasImg): ?> <div class="slider-image" aria-hidden="true"> @@ -12,7 +13,7 @@ <div class="slider-text-container"> <div class="carousel-text"> <?php if (!empty($feedTitleEsc)): ?> - <h2 class="slider-header"><?=$feedTitleEsc;?></h2> + <h2 class="slider-header" aria-hidden="true"><?=$feedTitleEsc;?></h2> <?php endif; ?> <?php if (!empty($item['title'])): ?> <h3 class="slider-title"><?=$this->escapeHtml($this->truncate($item['title'], 70))?></h3> diff --git a/themes/finna2/templates/collection/view.phtml b/themes/finna2/templates/collection/view.phtml index 6a2a8111070..5284930fc62 100644 --- a/themes/finna2/templates/collection/view.phtml +++ b/themes/finna2/templates/collection/view.phtml @@ -147,11 +147,11 @@ <?php // N.B. The element/class structure here is important for the ajax tabs to work properly ?> <div class="record-tabs record-accordions"> <?php if (count($this->tabs) > 0): ?> - <a id="tabnav"></a> + <a id="tabnav"><h2 class="sr-only"><?=$this->transEsc('Additional Information')?></h2></a> <?php if (count($this->tabs) > 3): ?> <div class="tabs-responsive"> <?php endif; ?> - <ul class="recordTabs nav nav-tabs visible-md visible-lg"> + <ul class="recordTabs nav nav-tabs visible-md visible-lg" role="tablist"> <?php foreach ($this->tabs as $tab => $obj): ?> <?php $desc = $obj->getDescription(); @@ -174,8 +174,8 @@ $tabClasses[] = 'hidden-xs tab-right'; } ?> - <li class="<?=implode(' ', $tabClasses)?>" data-tab="<?=$tabName?>"> - <a class="<?=$this->escapeHtmlAttr(strtolower($tab)) ?>" aria-label="<?=$this->escapeHtmlAttr($this->transEsc($desc))?>" + <li role="presentation" id="record-tab-<?=$this->escapeHtmlAttr($tabName)?>" class="<?=implode(' ', $tabClasses)?>" data-tab="<?=$tabName?>"> + <a role="tab" class="<?=$this->escapeHtmlAttr(strtolower($tab)) ?>" aria-label="<?=$this->escapeHtmlAttr($this->transEsc($desc))?>" href="<?=$this->escapeHtmlAttr($this->recordLinker()->getTabUrl($this->driver, $tab))?>#tabnav" data-lightbox-ignore<?php if ($obj->supportsAjax() && in_array($tab, $this->backgroundTabs)):?> data-background<?php endif ?>> <?php if (strtolower($tab) == 'details'): ?><?=$this->icon('staff-view', 'staff-view-icon') ?><?php endif; ?> @@ -189,7 +189,7 @@ <?php endif; ?> <?php endif; ?> - <div class="tab-content collectionDetails<?=$tree ? 'Tree' : ''?>"> + <div class="tab-content collectionDetails<?=$tree ? 'Tree' : ''?>" tabindex="0"> <?php if (!$this->loadInitialTabWithAjax || !isset($activeTabObj) || !$activeTabObj->supportsAjax()): ?> <div class="tab-pane active <?=$this->escapeHtmlAttr($this->activeTab) ?>-tab"> <?=isset($activeTabObj) ? $this->record($this->driver)->getTab($activeTabObj) : '' ?> diff --git a/themes/finna2/templates/record/view.phtml b/themes/finna2/templates/record/view.phtml index 4493a0ace77..e7bbe2a141f 100644 --- a/themes/finna2/templates/record/view.phtml +++ b/themes/finna2/templates/record/view.phtml @@ -129,10 +129,9 @@ <?=$this->driver->supportsCoinsOpenURL() ? '<span class="Z3988" aria-hidden="true" title="' . $this->escapeHtmlAttr($this->driver->getCoinsOpenURL()) . '"></span>' : ''?> <div class="clearfix hidden-lg hidden-md"></div> - <h2 class="sr-only"><?=$this->transEsc('Additional Information')?></h2> <div class="record-tabs record-accordions"> <?php if (count($this->tabs) > 0): ?> - <a id="tabnav"></a> + <a id="tabnav"><h2 class="sr-only"><?=$this->transEsc('Additional Information')?></h2></a> <?php if (count($this->tabs) > 3): ?> <div class="tabs-responsive"> <?php endif; ?> @@ -160,8 +159,8 @@ $tabClasses[] = 'hidden-xs tab-right'; } ?> - <li id="record-tab-<?=$this->escapeHtmlAttr($tabName)?>" class="<?=implode(' ', $tabClasses)?>" role="tab" data-tab="<?=$this->escapeHtmlAttr($tabName)?>"<?php if ($obj->supportsAjax() && in_array($tab, $this->backgroundTabs)):?> data-background<?php endif ?>> - <a class="<?=$this->escapeHtmlAttr(strtolower($tab)) ?>" aria-label="<?=$this->transEscAttr($desc)?>" + <li role="presentation" id="record-tab-<?=$this->escapeHtmlAttr($tabName)?>" class="<?=implode(' ', $tabClasses)?>" data-tab="<?=$this->escapeHtmlAttr($tabName)?>"<?php if ($obj->supportsAjax() && in_array($tab, $this->backgroundTabs)):?> data-background<?php endif ?>> + <a role="tab" class="<?=$this->escapeHtmlAttr(strtolower($tab)) ?>" aria-label="<?=$this->transEscAttr($desc)?>" href="<?=$this->escapeHtmlAttr($this->recordLinker()->getTabUrl($this->driver, $tab))?>#tabnav" data-lightbox-ignore> <?php if (strtolower($tab) == 'details'): ?><?=$this->icon('staff-view', 'staff-view-icon')?><?php endif; ?> <span class="tab-name"><?=$this->transEsc($desc)?></span> @@ -208,7 +207,7 @@ </div> </div> <?php endforeach; ?> - <div class="tab-content"> + <div class="tab-content" tabindex="0"> <?php if (!$this->loadInitialTabWithAjax || !isset($activeTabObj) || !$activeTabObj->supportsAjax()): ?> <div class="tab-pane active <?=$this->escapeHtmlAttr($this->activeTab) ?>-tab" aria-labelledby="record-tab-<?=$this->escapeHtmlAttr($activeTabName ?? '')?>"> <?=isset($activeTabObj) ? $this->record($this->driver)->getTab($activeTabObj) : '' ?>