Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add /documents/otd API endpoint and rework the on-this-day widget on the landing page #487

Merged
merged 13 commits into from
Feb 26, 2025
Merged
2 changes: 2 additions & 0 deletions api/v1/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ paths:
$ref: "./openapi/paths/documents-findByMention.yaml"
/documents/findByAuthor/{authorID}:
$ref: "./openapi/paths/documents-findByAuthor.yaml"
/documents/otd:
$ref: "./openapi/paths/documents-otd.yaml"
/code/findByElement/{element}:
$ref: "./openapi/paths/code-findByElement.yaml"
/application/status:
Expand Down
41 changes: 41 additions & 0 deletions api/v1/openapi/paths/documents-otd.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# /documents/otd path
get:
tags:
- Documents
summary: Returns documents which feature events on this day
description: |
This endpoint returns a list of documents which feature events on this day.
The events are extracted from the metadata (e.g. birth of a person, writing date of a letter)
and do not take mentioned events (from the text) into account.
parameters:
- name: date
in: query
description: |
The day and month to search for in years before (or equal) the given year.
If no date is provided, the current server date will be used.
required: false
schema:
type: string
format: date
default: '2024-12-18'
- $ref: '../parameters/docType.yaml'
- $ref: '../parameters/offset.yaml'
- $ref: '../parameters/limit.yaml'
responses:
'200':
description: An array of documents
headers:
totalrecordcount:
description: The total size of the result set
schema:
type: integer
content:
application/json:
schema:
type: array
items:
allOf:
- $ref: '../schemas/document.yaml'
- $ref: '../schemas/document-otd.yaml'
default:
$ref: '../responses/unexpectedError.yaml'
32 changes: 32 additions & 0 deletions api/v1/openapi/schemas/document-otd.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Document "on this day" schema
# as addition to the standard document schema
type: object
properties:
otdEvent:
type: string
description: The type of event occurring in the document
enum:
- letter
- birth
- baptism
- death
- funeral
- performance
- rehearsal
- production
otdJubilee:
type: integer
format: int32
description: The years between the event and the provided on-this-day date
otdRelations:
description: |
related entities or documents, e.g. addressees of a letter or works being performed or rehearsed.
Only available for docTypes "diaries" and "letters".
type: array
items:
$ref: 'document.yaml'
otdTeaser:
description: |
Some quote or summary of the text (in German).
Only available for docTypes "diaries" and "letters".
type: string
12 changes: 8 additions & 4 deletions catalogues/dictionary_de.xml
Original file line number Diff line number Diff line change
Expand Up @@ -216,12 +216,16 @@
<entry xml:id="goToArchive">Zum Archiv …</entry>
<entry xml:id="more">Mehr</entry>
<entry xml:id="readOnLetter">Zum Brief</entry>
<entry xml:id="readOnDiaries">Zum Tagebuch</entry>
<entry xml:id="whatHappenedOn">Was geschah am %1?</entry>
<entry xml:id="roundYearsAgo">Heute vor %1 Jahren</entry>
<entry xml:id="dies">stirbt.</entry>
<entry xml:id="wasBuried">wird beerdigt.</entry>
<entry xml:id="isBorn">wird geboren.</entry>
<entry xml:id="isBaptised">wird getauft.</entry>
<entry xml:id="otd-death">stirbt.</entry>
<entry xml:id="otd-funeral">wird beerdigt.</entry>
<entry xml:id="otd-birth">wird geboren.</entry>
<entry xml:id="otd-baptism">wird getauft.</entry>
<entry xml:id="otd-performance">Aufführung von</entry>
<entry xml:id="otd-rehearsal">Probe von</entry>
<entry xml:id="otd-production">Arbeit an</entry>
<entry xml:id="writesTo">schreibt an</entry>
<entry xml:id="editorialGuidelines">Editionsrichtlinien</entry>
<entry xml:id="guidelines">Richtlinien</entry>
Expand Down
12 changes: 8 additions & 4 deletions catalogues/dictionary_en.xml
Original file line number Diff line number Diff line change
Expand Up @@ -214,12 +214,16 @@
<entry xml:id="goToArchive">The News Archive …</entry>
<entry xml:id="more">More</entry>
<entry xml:id="readOnLetter">View letter</entry>
<entry xml:id="readOnDiaries">View diary entry</entry>
<entry xml:id="whatHappenedOn">What happened on %1?</entry>
<entry xml:id="roundYearsAgo">Today, %1 years ago</entry>
<entry xml:id="dies">died.</entry>
<entry xml:id="wasBuried">was buried.</entry>
<entry xml:id="isBorn">was born.</entry>
<entry xml:id="isBaptised">was baptised.</entry>
<entry xml:id="otd-death">died.</entry>
<entry xml:id="otd-funeral">was buried.</entry>
<entry xml:id="otd-birth">was born.</entry>
<entry xml:id="otd-baptism">was baptised.</entry>
<entry xml:id="otd-performance">Performance of</entry>
<entry xml:id="otd-rehearsal">Rehearsal of</entry>
<entry xml:id="otd-production">Work on</entry>
<entry xml:id="writesTo">writes to</entry>
<entry xml:id="editorialGuidelines">Editorial Guidelines</entry>
<entry xml:id="guidelines">Guidelines</entry>
Expand Down
139 changes: 134 additions & 5 deletions modules/api.xqm
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,53 @@ declare function api:documents-findByAuthor($model as map(*)) as map(*) {
)
};

declare function api:documents-otd($model as map(*)) as map(*) {
let $otdDate :=
if($model?otdDate castable as xs:date)
then xs:date($model?otdDate)
else current-date()
let $dateWithoutYear :=
(: strip of the year part so only month and day are left, e.g. "12-03" :)
substring($otdDate, 6, 5)
let $model :=
(: update model with date information :)
map:put($model, 'otdDate', string($otdDate)) => map:put('dateWithoutYear', $dateWithoutYear)
let $eventElements :=
for $docType in api:resolve-docTypes($model)
return
switch($docType)
case 'diaries' return
core:getOrCreateColl(
$docType, 'indices', true()
)//tei:ab[ft:query(., 'date:' || $dateWithoutYear)]/self::tei:ab (: use self axis here for performance reasons :)
[xs:date(@n) le $otdDate]//tei:seg[@type = ('rehearsal', 'performance', 'production')]
[.//tei:workName[not(ancestor::tei:note)]/@key or .//tei:rs[@type='work'][not(ancestor::tei:note)]/@key]
case 'letters' return
core:getOrCreateColl(
$docType,'indices', true()
)//tei:TEI[ft:query(., 'date:' || $dateWithoutYear)]//tei:correspAction[@type='sent']
[some $cur.date in tei:date satisfies contains($cur.date/@when, $dateWithoutYear) and xs:date($cur.date/@when) le $otdDate][following::tei:text//tei:p]
case 'persons' return
core:getOrCreateColl(
$docType, 'indices', true()
)//tei:person[ft:query(., 'date:' || $dateWithoutYear, map{'facets': map{'docSource': 'WeGA'}})]
//tei:date[contains(@when, $dateWithoutYear)][xs:date(@when) le $otdDate][parent::tei:birth or parent::tei:death]/parent::*
default return ()
let $sortedEvents :=
for $event in $eventElements
let $docID := $event/root()/*/data(@xml:id)
let $docType := config:get-doctype-by-id($docID)
order by api:otd-event-date($event, $docType, $dateWithoutYear)
return $event
return (
map {
'otdDate' : $otdDate,
'totalRecordCount': count($sortedEvents),
'results': api:document-otd(api:subsequence($sortedEvents, $model), $model)
}
)
};

declare function api:code-findByElement($model as map(*)) {
let $documents :=
for $docType in api:resolve-docTypes($model)
Expand Down Expand Up @@ -528,12 +575,85 @@ declare %private function api:document($documents as document-node()*, $model as
let $id := $doc/*/data(@xml:id)
let $docType := config:get-doctype-by-id($id)
return
map {
'uri' : api:document-uri($id, $model),
'docID' : $id,
'docType' : $docType,
'title' : wdt:lookup($docType, $doc)('title')('txt')
api:document-basics($doc, $id, $docType, $model)
}
};

(:~
: Helper function for creating a Document otd object
:
: @param $events a sequence of elements describing an event like e.g., tei:correspAction, tei:birth, or tei:seg[@type='performance']
: @param $model the current model including a mandatory "date" property with the give on-this-day date, and a mandatory "dateWithoutYear" property
~:)
declare %private function api:document-otd($events as element()*, $model as map(*)) as array(*) {
array {
for $event in $events
let $docID := $event/root()/*/data(@xml:id)
let $docType := config:get-doctype-by-id($docID)
let $typeOfEvent :=
switch($docType)
case 'diaries' return $event/data(@type)
case 'letters' return 'letter'
case 'persons' return
if($event/self::tei:birth)
then
if($event/tei:date[@type='baptism'])
then 'baptism'
else 'birth'
else
if($event/tei:date[@type='funeral']) then 'funeral'
else 'death'
default return ()
let $eventDate := api:otd-event-date($event, $docType, $model?dateWithoutYear)
let $jubilee := year-from-date($model?otdDate) - year-from-date($eventDate)
let $relations :=
switch($docType)
case 'diaries' return (
($event//tei:workName[not(ancestor::tei:note)]/@key ! api:document-basics(crud:doc(.), ., 'works', $model)),
(($event//tei:rs[@type=('work', 'works')][not(ancestor::tei:note)]/@key ! tokenize(., '\s+')) ! api:document-basics(crud:doc(.), ., 'works', $model)),
($event//tei:persName[not(ancestor::tei:note)]/@key ! api:document-basics(crud:doc(.), ., 'persons', $model)),
(($event//tei:rs[@type=('person', 'persons')][not(ancestor::tei:note)]/@key ! tokenize(., '\s+')) ! api:document-basics(crud:doc(.), ., 'persons', $model))
)
case 'letters' return (
($event/ancestor::tei:correspDesc//tei:persName[not(ancestor::tei:note)]/@key ! api:document-basics(crud:doc(.), ., 'persons', $model)),
($event/ancestor::tei:correspDesc//tei:orgName[not(ancestor::tei:note)]/@key ! api:document-basics(crud:doc(.), ., 'orgs', $model))
)
default return ()
let $teaser :=
switch($docType)
case 'diaries' return wega-util:txtFromTEI($event) => string-join() => normalize-space()
case 'letters' return $event/preceding::tei:note[@type='summary']/node() => wega-util:txtFromTEI() => string-join() => normalize-space()
default return ()
return map:merge((
api:document-basics($event/root(), $docID, $docType, $model),
map {
'otdEvent': $typeOfEvent,
'otdJubilee': $jubilee,
'otdTeaser': if($teaser) then $teaser else (), (: return NULL instead of empty strings:)
'otdRelations': array { $relations }
}
))
}
};

(:~
: Helper function for api:documents-otd#1 and api:document-otd#2
:)
declare %private function api:otd-event-date($event as element(), $docType as xs:string, $dateWithoutYear as xs:string) as xs:date {
switch($docType)
case 'diaries' return $event/ancestor::tei:ab/@n cast as xs:date
default return ($event/tei:date/@when[contains(., $dateWithoutYear)])[1] cast as xs:date
};

(:~
: Helper function for api:document and api:document-otd
:)
declare %private function api:document-basics($doc as document-node(), $docID as xs:string, $docType as xs:string, $model as map(*)) as map(*) {
map {
'uri' : api:document-uri($docID, $model),
'docID' : $docID,
'docType' : $docType,
'title' : wdt:lookup($docType, $doc)('title')('txt')
}
};

Expand Down Expand Up @@ -637,6 +757,15 @@ declare function api:validate-toDate($model as map(*)) as map(*)? {
else error($api:INVALID_PARAMETER, 'Unsupported date format given: "' || $model('toDate') || '". Should be YYYY-MM-DD.')
};

(:~
: Check parameter date
~:)
declare function api:validate-date($model as map(*)) as map(*)? {
if($model('date') castable as xs:date) then $model
else if($model?date ='') then () (: an empty string is simply dropped :)
else error($api:INVALID_PARAMETER, 'Unsupported date format given: "' || $model('date') || '". Should be YYYY-MM-DD.')
};

(:~
: Check parameter docID
~:)
Expand Down
54 changes: 22 additions & 32 deletions modules/app.xqm
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ declare namespace map="http://www.w3.org/2005/xpath-functions/map";
declare namespace ft="http://exist-db.org/xquery/lucene";
declare namespace xmldb="http://exist-db.org/xquery/xmldb";

import module namespace api="http://xquery.weber-gesamtausgabe.de/modules/api" at "api.xqm";
import module namespace core="http://xquery.weber-gesamtausgabe.de/modules/core" at "core.xqm";
import module namespace crud="http://xquery.weber-gesamtausgabe.de/modules/crud" at "crud.xqm";
import module namespace img="http://xquery.weber-gesamtausgabe.de/modules/img" at "img.xqm";
Expand Down Expand Up @@ -558,57 +559,46 @@ declare

declare
%templates:wrap
%templates:default("otd-date", "")
function app:lookup-todays-events($node as node(), $model as map(*), $otd-date as xs:string) as map(*) {
let $date :=
if($otd-date castable as xs:date) then xs:date($otd-date)
else current-date()
let $events :=
for $i in query:getTodaysEvents($date)
order by $i/xs:date(@when) ascending
return $i
let $length := count($events)
return
map {
'otd-date' : $date,
'events1' : subsequence($events, 1, ceiling($length div 2)),
'events2' : subsequence($events, ceiling($length div 2) + 1)
}
%templates:default("otdDate", "")
function app:lookup-todays-events($node as node(), $model as map(*), $otdDate as xs:string) as map(*) {
api:documents-otd(map:put($model, 'otdDate', $otdDate) => map:put('openapi:config', json-doc($config:openapi-config-path)))
};

declare function app:print-event($node as node(), $model as map(*), $lang as xs:string) as element(xhtml:span)* {
let $date := $model?otd-date
let $teiDate := $model('event')
let $isJubilee := (year-from-date($date) - $teiDate/year-from-date(@when)) mod 25 = 0
let $typeOfEvent :=
if($teiDate/ancestor::tei:correspDesc) then 'letter'
else if($teiDate[@type='baptism']) then 'isBaptised'
else if($teiDate/parent::tei:birth) then 'isBorn'
else if($teiDate[@type='funeral']) then 'wasBuried'
else if($teiDate/parent::tei:death) then 'dies'
else ()
let $event := $model?event
let $isJubilee := $event?otdJubilee mod 25 = 0
let $doc := crud:doc($event?docID)
return (
element xhtml:span {
if($isJubilee) then (
attribute class {'jubilee event-year'},
attribute title {lang:get-language-string('roundYearsAgo',xs:string(year-from-date($date) - $teiDate/year-from-date(@when)), $lang)},
attribute title {lang:get-language-string('roundYearsAgo', $event?otdJubilee, $lang)},
attribute data-toggle {'tooltip'},
attribute data-container {'body'}
)
else attribute class {'event-year'},
date:formatYear($teiDate/year-from-date(@when) cast as xs:int, $lang)
date:formatYear(year-from-date($model?otdDate) - $event?otdJubilee, $lang)
},
element xhtml:span {
attribute class {'event-text'},
if($typeOfEvent eq 'letter') then app:createLetterLink($teiDate, $lang)
(:else (wega:createPersonLink($teiDate/root()/*/string(@xml:id), $lang, 'fs'), ' ', lang:get-language-string($typeOfEvent, $lang)):)
else (app:createDocLink($teiDate/root(), wega-util:print-forename-surname-from-nameLike-element($teiDate/ancestor::tei:person/tei:persName[@type='reg']), $lang, ('class=persons')), ' ', lang:get-language-string($typeOfEvent, $lang))
switch($event?otdEvent)
case 'letter' return app:createLetterLink(($doc//tei:correspAction/tei:date)[1], $lang)
case 'birth' case 'baptism' case 'funeral' case 'death' return (
app:createDocLink($doc, wega-util:print-forename-surname-from-nameLike-element($doc//tei:persName[@type='reg']), $lang, ('class=persons')), ' ',
lang:get-language-string('otd-' || $event?otdEvent, $lang)
)
case 'performance' case 'rehearsal' case 'production' return (
lang:get-language-string('otd-' || $event?otdEvent, $lang), ' ',
app:createDocLink(crud:doc($event?otdRelations?1?docID), $event?otdRelations?1?title, $lang, ('class=works')), '. ',
app:createDocLink($doc, concat('[', lang:get-language-string('readOnDiaries', $lang), ']'), $lang, ('class=readOn'))
)
default return ()
}
)
};

declare function app:print-events-title($node as node(), $model as map(*), $lang as xs:string) as element(xhtml:h2) {
<xhtml:h2>{lang:get-language-string('whatHappenedOn', format-date($model?otd-date, if($lang eq 'de') then '[D]. [MNn]' else '[MNn] [D]', $lang, (), ()), $lang)}</xhtml:h2>
<xhtml:h2>{lang:get-language-string('whatHappenedOn', format-date($model?otdDate, if($lang eq 'de') then '[D]. [MNn]' else '[MNn] [D]', $lang, (), ()), $lang)}</xhtml:h2>
};

(:~
Expand Down
1 change: 1 addition & 0 deletions modules/wega-util.xqm
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ declare function wega-util:txtFromTEI($nodes as node()*) as xs:string* {
else $node/child::node() ! wega-util:txtFromTEI(.)
case element(tei:del) return ()
case element(tei:subst) return $node/child::element() ! wega-util:txtFromTEI(.)
case element(tei:choice) return ($node/tei:abbr | $node/tei:corr | $node/tei:unclear[1]) ! wega-util:txtFromTEI(.)
case element(tei:note) return ()
case element(tei:lb) return
if($node[@type='inWord' or @break='no']) then ()
Expand Down
2 changes: 1 addition & 1 deletion resources/js/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ $.fn.loadPortrait = function () {
/* Load the what-happened-on-this-day div for the start page */
$('#otd').each(function() {
const date = moment(new Date()).format("YYYY-MM-DD"),
url = $(this).attr('data-target') + '?otd-date=' + date;
url = $(this).attr('data-target') + '?otdDate=' + date;
$(this).load(url);
});

Expand Down
Loading