From c82d01b07989b3a6b73675c36031a425375979a0 Mon Sep 17 00:00:00 2001 From: Peter Stadler Date: Tue, 15 Feb 2022 16:36:28 +0100 Subject: [PATCH 01/47] add a crud module to MerMEId --- modules/crud.xqm | 107 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 modules/crud.xqm diff --git a/modules/crud.xqm b/modules/crud.xqm new file mode 100644 index 00000000..2ecba9eb --- /dev/null +++ b/modules/crud.xqm @@ -0,0 +1,107 @@ +xquery version "3.1"; + +(:~ + : Basic CRUD (Create, Read, Update, Delete) functions for the MerMEId data store + : + : All operations assume data is kept at $config:data-root and the file hierarchy is flat, + : i.e. there are no subfolders +~:) +module namespace crud="https://github.com/edirom/mermeid/crud"; + +declare namespace mei="http://www.music-encoding.org/ns/mei"; +declare namespace request="http://exist-db.org/xquery/request"; +declare namespace response="http://exist-db.org/xquery/response"; +declare namespace xmldb="http://exist-db.org/xquery/xmldb"; +declare namespace map="http://www.w3.org/2005/xpath-functions/map"; + +import module namespace config="https://github.com/edirom/mermeid/config" at "config.xqm"; + +(:~ + : Delete files within the data directory + : + : @param $filenames the files to delete + : @return a map object with the $filename as key and some return message as value +~:) +declare function crud:delete($filenames as xs:string*) as map(xs:string,xs:string) { + map:merge( + for $filename in $filenames + return + try {( + xmldb:remove($config:data-root, $filename), + map:entry($filename, 'deleted successfully') + )} + catch * { + map:entry($filename, 'failed to delete: ' || string-join(($err:code, $err:description))) + } + ) +}; + +(:~ + : Create a file within the data directory + : + : @param $node the XML document to store + : @param $filename the filename for the new file + : @return a map object with the $filename as key and some return message as value +~:) +declare function crud:create($node as node(), $filename as xs:string) as map(xs:string,xs:string) { + map:entry( + $filename, + if(xmldb:store($config:data-root, $filename, $node)) + then 'created successfully' + else 'failure' + ) +}; + +(:~ + : Copy a file within the data directory + : + : @param $source-filename the input filename to copy + : @param $target-filename the output filename to copy to + : @param $overwrite whether an existent target file may be overwritten + : @return a map object with the $target-filename as key and some return message as value +~:) +declare function crud:copy($source-filename as xs:string, $target-filename as xs:string, $overwrite as xs:boolean) as map(xs:string,xs:string) { + let $source := + if(doc-available($config:data-root || '/' || $source-filename)) + then doc($config:data-root || '/' || $source-filename) + else () + let $create-target := + if($source and (not(doc-available($config:data-root || '/' || $target-filename)) or $overwrite)) + then xmldb:store($config:data-root, $target-filename, $source) => crud:adjust-mei-title() + else () + return + map:entry( + $target-filename, + if($create-target) + then 'copied successfully from ' || $source-filename + else if(doc-available($config:data-root || '/' || $target-filename) and not($overwrite)) + then 'target already existent and "overwrite" flag was missing' + else if($source) + then 'failed to create target' + else 'source ' || $source-filename || ' does not exist' + ) +}; + +(:~ + : Append "copy" to the MEI title + : Helper function for crud:copy() + : + : @param $filepath the (full) filepath to the resource in the eXist db + : @return the input filepath if successfull, the empty sequence otherwise +~:) +declare %private function crud:adjust-mei-title($filepath as xs:string?) as xs:string? { + if(doc-available($filepath)) + then + let $mei := doc($filepath) + return + try {( + for $title in $mei//mei:workList/mei:work[1]/mei:title[text()][1] + let $new_title_text := concat(normalize-space($title), " (copy)") + return + update value $title with $new_title_text + ), + $filepath + } + catch * {()} + else () +}; From 4c4042b0d352882b035358cc28d4d5487eb5cff2 Mon Sep 17 00:00:00 2001 From: Peter Stadler Date: Tue, 15 Feb 2022 16:36:58 +0100 Subject: [PATCH 02/47] add `/data/copy` endpoint --- data/controller.xql | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/data/controller.xql b/data/controller.xql index 030fe722..ea0261ff 100644 --- a/data/controller.xql +++ b/data/controller.xql @@ -1,11 +1,14 @@ -xquery version "3.0"; +xquery version "3.1"; declare namespace exist="http://exist.sourceforge.net/NS/exist"; declare namespace request="http://exist-db.org/xquery/request"; declare namespace response="http://exist-db.org/xquery/response"; declare namespace transform="http://exist-db.org/xquery/transform"; +declare namespace output="http://www.w3.org/2010/xslt-xquery-serialization"; +import module namespace login="http://exist-db.org/xquery/login" at "resource:org/exist/xquery/modules/persistentlogin/login.xql"; import module namespace config="https://github.com/edirom/mermeid/config" at "../modules/config.xqm"; +import module namespace crud="https://github.com/edirom/mermeid/crud" at "../modules/crud.xqm"; import module namespace xmldb="http://exist-db.org/xquery/xmldb"; import module namespace console="http://exist-db.org/xquery/console"; @@ -63,6 +66,39 @@ if (ends-with($exist:resource, ".xml")) then } default return (response:set-status-code(405), <_/>) ) +(:~ + : copy files endpoint +~:) +else if($exist:path = '/copy' and request:get-method() eq 'POST') then + let $loggedIn := login:set-user("org.exist.login", (), false()) + let $serializationParameters := ('method=text', 'media-type=application/json', 'encoding=utf-8') + let $user := request:get-attribute("org.exist.login.user") + let $isInMermeidGroup := sm:get-group-members('mermedit') = $user + let $source := request:get-parameter('source', '') + let $target := request:get-parameter('target', '') + let $overwriteString := request:get-parameter('overwrite', 'false') + let $overwrite := $overwriteString = ('1', 'yes', 'ja', 'y', 'true', 'true()') (: some string values that are considered boolean "true()" :) + return + if($isInMermeidGroup) then + response:stream( + serialize(crud:copy($source, $target, $overwrite) => map:merge(), + + json + + ), + string-join($serializationParameters, ' ') + ) + else ( + response:set-status-code(401), + response:stream( + serialize(map {'error': 'permissions missing'} => map:merge(), + + json + + ), + string-join($serializationParameters, ' ') + ) + ) else (: everything else is passed through :) (console:log('/data Controller: passthrough'), From 3b3286c0f5cfbe33b4cc0bbeb5942ba3265a3208 Mon Sep 17 00:00:00 2001 From: Peter Stadler Date: Tue, 15 Feb 2022 16:37:33 +0100 Subject: [PATCH 03/47] update form to copy files --- modules/list_files.xq | 7 +++---- modules/list_utils.xqm | 9 ++++----- resources/js/confirm.js | 11 +++++++++++ 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/modules/list_files.xq b/modules/list_files.xq index cad39517..9138e768 100755 --- a/modules/list_files.xq +++ b/modules/list_files.xq @@ -132,10 +132,6 @@ declare function local:format-reference( href="../resources/css/login.css" type="text/css"/> - - @@ -332,6 +328,9 @@ declare function local:format-reference( } {doc('../login.html')/*} {config:replace-properties(config:get-property('footer'))} + diff --git a/modules/list_utils.xqm b/modules/list_utils.xqm index 67c7b438..d1bb7f8c 100755 --- a/modules/list_utils.xqm +++ b/modules/list_utils.xqm @@ -139,12 +139,11 @@ let $options:= { let $form-id := util:document-name($doc) let $uri := concat($config:data-public-root, "/", util:document-name($doc)) - let $form := -
- - + return + + +
- return $form }; diff --git a/resources/js/confirm.js b/resources/js/confirm.js index f6645242..54b060f5 100755 --- a/resources/js/confirm.js +++ b/resources/js/confirm.js @@ -23,3 +23,14 @@ function filename_prompt(formid, text, published) } } +function filecopy(ev) { + ev.preventDefault(); + var source = ev.target[0].value, target, overwrite=false; + target = prompt("Copy " + source + " to:", source.substring(0, source.length -4) + "-copy.xml"); + const xhttp = new XMLHttpRequest(); + xhttp.open("POST", "../data/copy", true); + xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); + xhttp.send("source="+source+"&target="+target+"&overwrite="+overwrite); +} + +document.querySelectorAll(".copyform").forEach(el => {el.addEventListener("submit", filecopy)}); From e3420baa700187aab3b5e05e95011331fcd976a7 Mon Sep 17 00:00:00 2001 From: Peter Stadler Date: Wed, 16 Feb 2022 00:12:38 +0100 Subject: [PATCH 04/47] updates to response objects and add `$new_title` as parameter to copy operation --- data/controller.xql | 3 +- modules/crud.xqm | 102 +++++++++++++++++++++++++++++--------------- 2 files changed, 70 insertions(+), 35 deletions(-) diff --git a/data/controller.xql b/data/controller.xql index ea0261ff..377cfac5 100644 --- a/data/controller.xql +++ b/data/controller.xql @@ -76,12 +76,13 @@ else if($exist:path = '/copy' and request:get-method() eq 'POST') then let $isInMermeidGroup := sm:get-group-members('mermedit') = $user let $source := request:get-parameter('source', '') let $target := request:get-parameter('target', '') + let $title := request:get-parameter('title', ()) let $overwriteString := request:get-parameter('overwrite', 'false') let $overwrite := $overwriteString = ('1', 'yes', 'ja', 'y', 'true', 'true()') (: some string values that are considered boolean "true()" :) return if($isInMermeidGroup) then response:stream( - serialize(crud:copy($source, $target, $overwrite) => map:merge(), + serialize(crud:copy($source, $target, $overwrite, $title), json diff --git a/modules/crud.xqm b/modules/crud.xqm index 2ecba9eb..ea641955 100644 --- a/modules/crud.xqm +++ b/modules/crud.xqm @@ -5,7 +5,7 @@ xquery version "3.1"; : : All operations assume data is kept at $config:data-root and the file hierarchy is flat, : i.e. there are no subfolders -~:) + :) module namespace crud="https://github.com/edirom/mermeid/crud"; declare namespace mei="http://www.music-encoding.org/ns/mei"; @@ -20,20 +20,28 @@ import module namespace config="https://github.com/edirom/mermeid/config" at "co : Delete files within the data directory : : @param $filenames the files to delete - : @return a map object with the $filename as key and some return message as value -~:) -declare function crud:delete($filenames as xs:string*) as map(xs:string,xs:string) { - map:merge( + : @return an array of map object with filename, message and code properties concerning the delete operation + :) +declare function crud:delete($filenames as xs:string*) as array(map(xs:string,xs:string)*) { + array { for $filename in $filenames return try {( xmldb:remove($config:data-root, $filename), - map:entry($filename, 'deleted successfully') + map { + 'filename': $filename, + 'message': 'deleted successfully', + 'code': 200 + } )} - catch * { - map:entry($filename, 'failed to delete: ' || string-join(($err:code, $err:description))) + catch * { + map { + 'filename': $filename, + 'message': 'failed to delete: ' || string-join(($err:code, $err:description)), + 'code': 500 + } } - ) + } }; (:~ @@ -41,15 +49,20 @@ declare function crud:delete($filenames as xs:string*) as map(xs:string,xs:strin : : @param $node the XML document to store : @param $filename the filename for the new file - : @return a map object with the $filename as key and some return message as value -~:) + : @return a map object with filename, message and code properties concerning the create operation + :) declare function crud:create($node as node(), $filename as xs:string) as map(xs:string,xs:string) { - map:entry( - $filename, - if(xmldb:store($config:data-root, $filename, $node)) - then 'created successfully' - else 'failure' - ) + if(xmldb:store($config:data-root, $filename, $node)) + then map { + 'filename': $filename, + 'message': 'created successfully', + 'code': 200 + } + else map { + 'filename': $filename, + 'message': 'failed to create file', + 'code': 500 + } }; (:~ @@ -58,28 +71,46 @@ declare function crud:create($node as node(), $filename as xs:string) as map(xs: : @param $source-filename the input filename to copy : @param $target-filename the output filename to copy to : @param $overwrite whether an existent target file may be overwritten - : @return a map object with the $target-filename as key and some return message as value -~:) -declare function crud:copy($source-filename as xs:string, $target-filename as xs:string, $overwrite as xs:boolean) as map(xs:string,xs:string) { + : @param $new_title an optional new title. If omitted, the string "(copy)" will be appended to the old title + : @return a map object with source, target, message and code properties concerning the copy operation + :) +declare function crud:copy($source-filename as xs:string, $target-filename as xs:string, $overwrite as xs:boolean, $new_title as xs:string?) as map(xs:string,xs:string) { let $source := if(doc-available($config:data-root || '/' || $source-filename)) then doc($config:data-root || '/' || $source-filename) else () let $create-target := if($source and (not(doc-available($config:data-root || '/' || $target-filename)) or $overwrite)) - then xmldb:store($config:data-root, $target-filename, $source) => crud:adjust-mei-title() + then xmldb:store($config:data-root, $target-filename, $source) => crud:adjust-mei-title($new_title) else () return - map:entry( - $target-filename, - if($create-target) - then 'copied successfully from ' || $source-filename - else if(doc-available($config:data-root || '/' || $target-filename) and not($overwrite)) - then 'target already existent and "overwrite" flag was missing' - else if($source) - then 'failed to create target' - else 'source ' || $source-filename || ' does not exist' - ) + if($create-target) + then map { + 'source': $source-filename, + 'target': $target-filename, + 'message': 'copied successfully', + 'code': 200 + } + else if(doc-available($config:data-root || '/' || $target-filename) and not($overwrite)) + then map { + 'source': $source-filename, + 'target': $target-filename, + 'message': 'target already existent and "overwrite" flag was missing', + 'code': 500 + } + else if($source) + then map { + 'source': $source-filename, + 'target': $target-filename, + 'message': 'failed to create target', + 'code': 500 + } + else map { + 'source': $source-filename, + 'target': $target-filename, + 'message': 'source does not exist', + 'code': 200 + } }; (:~ @@ -87,16 +118,19 @@ declare function crud:copy($source-filename as xs:string, $target-filename as xs : Helper function for crud:copy() : : @param $filepath the (full) filepath to the resource in the eXist db + : @param $new_title an optional new title. If omitted, the string "(copy)" will be appended to the old title : @return the input filepath if successfull, the empty sequence otherwise -~:) -declare %private function crud:adjust-mei-title($filepath as xs:string?) as xs:string? { + :) +declare %private function crud:adjust-mei-title($filepath as xs:string?, $new_title as xs:string?) as xs:string? { if(doc-available($filepath)) then let $mei := doc($filepath) return try {( for $title in $mei//mei:workList/mei:work[1]/mei:title[text()][1] - let $new_title_text := concat(normalize-space($title), " (copy)") + let $new_title_text := + if($new_title) then $new_title + else concat(normalize-space($title), " (copy)") return update value $title with $new_title_text ), From 3b9383e477814dab72749f04dc9733bdc0e21ff0 Mon Sep 17 00:00:00 2001 From: Peter Stadler Date: Wed, 16 Feb 2022 00:12:59 +0100 Subject: [PATCH 05/47] remove outdated xquery module --- modules/copy-file.xq | 59 -------------------------------------------- 1 file changed, 59 deletions(-) delete mode 100755 modules/copy-file.xq diff --git a/modules/copy-file.xq b/modules/copy-file.xq deleted file mode 100755 index 63f96640..00000000 --- a/modules/copy-file.xq +++ /dev/null @@ -1,59 +0,0 @@ -xquery version "3.0"; - -import module namespace login="http://kb.dk/this/login" at "./login.xqm"; -import module namespace config="https://github.com/edirom/mermeid/config" at "./config.xqm"; - -declare namespace functx = "http://www.functx.com"; -declare namespace m="http://www.music-encoding.org/ns/mei"; -declare namespace xmldb="http://exist-db.org/xquery/xmldb"; -declare namespace request="http://exist-db.org/xquery/request"; -declare namespace response="http://exist-db.org/xquery/response"; -declare namespace fn="http://www.w3.org/2005/xpath-functions"; -declare namespace util="http://exist-db.org/xquery/util"; - - -declare option exist:serialize "method=xml media-type=text/html"; - -declare variable $dcmroot := $config:app-root; - -declare function functx:copy-attributes - ( $copyTo as element() , - $copyFrom as element() ) as element() { - - element { node-name($copyTo)} - { $copyTo/@*[not(node-name(.) = $copyFrom/@*/node-name(.))], - $copyFrom/@*, - $copyTo/node() } - - } ; - - -let $return_to := config:link-to-app("modules/list_files.xq") - - -let $log-in := login:function() -let $res := response:redirect-to($return_to cast as xs:anyURI) -let $parameters := request:get-parameter-names() - -return - - { - for $parameter in $parameters - let $new_file := concat($dcmroot,util:uuid(),".xml") - let $old_file := concat($dcmroot,$parameter) - where request:get-parameter($parameter,"")="copy" and contains($parameter,"xml") - return - let $odoc := doc($old_file) - let $stored := xmldb:store($dcmroot,$new_file, $odoc ) - let $new_doc := doc($new_file) - for $title in $new_doc//m:workList/m:work[1]/m:title[string()][1] - let $new_title_text := concat($title//string()," (copy) ") - let $new_title := - {$new_title_text} - let $upd := update replace $title[string()] with - functx:copy-attributes($new_title,$title) - return - } -
{$title[string()][1]//string()}{$new_title_text}
- - From df0c3370091efcde96a2a3cb50f0ba4fed2130dc Mon Sep 17 00:00:00 2001 From: Peter Stadler Date: Wed, 16 Feb 2022 21:38:23 +0100 Subject: [PATCH 06/47] add try/catch to crud operations to capture permission issues --- modules/crud.xqm | 73 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 59 insertions(+), 14 deletions(-) diff --git a/modules/crud.xqm b/modules/crud.xqm index ea641955..f8f02835 100644 --- a/modules/crud.xqm +++ b/modules/crud.xqm @@ -13,6 +13,8 @@ declare namespace request="http://exist-db.org/xquery/request"; declare namespace response="http://exist-db.org/xquery/response"; declare namespace xmldb="http://exist-db.org/xquery/xmldb"; declare namespace map="http://www.w3.org/2005/xpath-functions/map"; +declare namespace err="http://www.w3.org/2005/xqt-errors"; +declare namespace jb="http://exist.sourceforge.net/NS/exist/java-binding"; import module namespace config="https://github.com/edirom/mermeid/config" at "config.xqm"; @@ -34,10 +36,17 @@ declare function crud:delete($filenames as xs:string*) as array(map(xs:string,xs 'code': 200 } )} + catch jb:org.xmldb.api.base.XMLDBException { + map { + 'filename': $filename, + 'message': 'failed to delete: ' || $err:description, + 'code': 401 + } + } catch * { map { 'filename': $filename, - 'message': 'failed to delete: ' || string-join(($err:code, $err:description)), + 'message': 'failed to delete: ' || string-join(($err:code, $err:description), '; '), 'code': 500 } } @@ -52,16 +61,32 @@ declare function crud:delete($filenames as xs:string*) as array(map(xs:string,xs : @return a map object with filename, message and code properties concerning the create operation :) declare function crud:create($node as node(), $filename as xs:string) as map(xs:string,xs:string) { - if(xmldb:store($config:data-root, $filename, $node)) - then map { - 'filename': $filename, - 'message': 'created successfully', - 'code': 200 + try { + if(xmldb:store($config:data-root, $filename, $node)) + then map { + 'filename': $filename, + 'message': 'created successfully', + 'code': 200 + } + else map { + 'filename': $filename, + 'message': 'failed to create file', + 'code': 500 + } } - else map { - 'filename': $filename, - 'message': 'failed to create file', - 'code': 500 + catch jb:org.xmldb.api.base.XMLDBException { + map { + 'filename': $filename, + 'message': 'failed to create file: ' || $err:description, + 'code': 401 + } + } + catch * { + map { + 'filename': $filename, + 'message': 'failed to create file: ' || string-join(($err:code, $err:description), '; '), + 'code': 500 + } } }; @@ -80,11 +105,31 @@ declare function crud:copy($source-filename as xs:string, $target-filename as xs then doc($config:data-root || '/' || $source-filename) else () let $create-target := - if($source and (not(doc-available($config:data-root || '/' || $target-filename)) or $overwrite)) - then xmldb:store($config:data-root, $target-filename, $source) => crud:adjust-mei-title($new_title) - else () + try { + if($source and (not(doc-available($config:data-root || '/' || $target-filename)) or $overwrite)) + then xmldb:store($config:data-root, $target-filename, $source) => crud:adjust-mei-title($new_title) + else () + } + catch jb:org.xmldb.api.base.XMLDBException { + map { + 'source': $source-filename, + 'target': $target-filename, + 'message': 'failed to copy file: ' || $err:description, + 'code': 401 + } + } + catch * { + map { + 'source': $source-filename, + 'target': $target-filename, + 'message': 'failed to copy file: ' || string-join(($err:code, $err:description), '; '), + 'code': 500 + } + } return - if($create-target) + if($create-target instance of map(*)) + then $create-target + else if($create-target instance of xs:string) then map { 'source': $source-filename, 'target': $target-filename, From 920bc2e62327c851b369a666d4e3f678bf23343f Mon Sep 17 00:00:00 2001 From: Peter Stadler Date: Wed, 16 Feb 2022 21:39:15 +0100 Subject: [PATCH 07/47] add "delete" endpoint and add some comments --- data/controller.xql | 79 ++++++++++++++++++++++++++++++--------------- 1 file changed, 53 insertions(+), 26 deletions(-) diff --git a/data/controller.xql b/data/controller.xql index 377cfac5..b82da7bf 100644 --- a/data/controller.xql +++ b/data/controller.xql @@ -2,6 +2,7 @@ xquery version "3.1"; declare namespace exist="http://exist.sourceforge.net/NS/exist"; declare namespace request="http://exist-db.org/xquery/request"; +declare namespace session="http://exist-db.org/xquery/session"; declare namespace response="http://exist-db.org/xquery/response"; declare namespace transform="http://exist-db.org/xquery/transform"; declare namespace output="http://www.w3.org/2010/xslt-xquery-serialization"; @@ -19,6 +20,33 @@ declare variable $exist:controller external; declare variable $exist:prefix external; declare variable $exist:root external; +(:~ + : Wrapper function for outputting JSON + : + : @param $response-body the response body + : @param $response-code the response status code + :) +declare function output:stream-json($response-body, $response-code as xs:integer) as empty-sequence() { + response:set-status-code($response-code), + response:stream( + serialize($response-body, + + json + + ), + 'method=text media-type=application/json encoding=utf-8' + ) +}; + +(:~ + : Wrapper function for redirecting to the main page + :) +declare function output:redirect-to-main-page() as element(exist:dispatch) { + + + +}; + (console:log('/data Controller'), if (ends-with($exist:resource, ".xml")) then (console:log('/data Controller: XML data session: '||session:exists()), @@ -68,38 +96,37 @@ if (ends-with($exist:resource, ".xml")) then ) (:~ : copy files endpoint + : For POST requests with Accept header 'application/json' the response + : of the crud operation (a map object) is returned as JSON, for all other + : Accept headers the client is redirected to the main page (after the + : execution of the crud operation) ~:) else if($exist:path = '/copy' and request:get-method() eq 'POST') then - let $loggedIn := login:set-user("org.exist.login", (), false()) - let $serializationParameters := ('method=text', 'media-type=application/json', 'encoding=utf-8') - let $user := request:get-attribute("org.exist.login.user") - let $isInMermeidGroup := sm:get-group-members('mermedit') = $user let $source := request:get-parameter('source', '') - let $target := request:get-parameter('target', '') - let $title := request:get-parameter('title', ()) + let $target := request:get-parameter('target', util:uuid() || '.xml') (: generate a unique filename if none is provided :) + let $title := request:get-parameter('title', ()) (: empty titles will get passed on and filled in later :) let $overwriteString := request:get-parameter('overwrite', 'false') let $overwrite := $overwriteString = ('1', 'yes', 'ja', 'y', 'true', 'true()') (: some string values that are considered boolean "true()" :) + let $backend-response := crud:copy($source, $target, $overwrite, $title) + let $log := util:log-system-out(request:get-header('Accept')) + return + if(request:get-header('Accept') eq 'application/json') + then output:stream-json($backend-response, $backend-response?code) + else output:redirect-to-main-page() +(:~ + : delete files endpoint + : For POST requests with Accept header 'application/json' the response + : of the crud operation (an array) is returned as JSON, for all other + : Accept headers the client is redirected to the main page (after the + : execution of the crud operation) +~:) +else if($exist:path = '/delete' and request:get-method() eq 'POST') then + let $filename := request:get-parameter('filename', '') + let $backend-response := crud:delete($filename) return - if($isInMermeidGroup) then - response:stream( - serialize(crud:copy($source, $target, $overwrite, $title), - - json - - ), - string-join($serializationParameters, ' ') - ) - else ( - response:set-status-code(401), - response:stream( - serialize(map {'error': 'permissions missing'} => map:merge(), - - json - - ), - string-join($serializationParameters, ' ') - ) - ) + if(request:get-header('Accept') eq 'application/json') + then output:stream-json($backend-response, $backend-response(1)?code) + else output:redirect-to-main-page() else (: everything else is passed through :) (console:log('/data Controller: passthrough'), From 388764c8d206d8dfec82e4d998afd4bac9c60af3 Mon Sep 17 00:00:00 2001 From: Peter Stadler Date: Wed, 16 Feb 2022 21:44:14 +0100 Subject: [PATCH 08/47] rework forms for "delete" and "copy" to use the new endpoints `/data/copy` and `/data/delete` --- modules/list_utils.xqm | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/modules/list_utils.xqm b/modules/list_utils.xqm index d1bb7f8c..fd320d94 100755 --- a/modules/list_utils.xqm +++ b/modules/list_utils.xqm @@ -137,11 +137,12 @@ let $options:= declare function app:copy-document-reference($doc as node()) as node() { - let $form-id := util:document-name($doc) + let $doc-name := util:document-name($doc) let $uri := concat($config:data-public-root, "/", util:document-name($doc)) return -
- + + +
}; @@ -166,21 +167,19 @@ let $options:= declare function app:delete-document-reference($doc as node()) as node() { - let $form-id := util:document-name($doc) + let $doc-name := util:document-name($doc) let $uri := concat($config:data-public-root,"/",util:document-name($doc)) - let $form := - if(doc-available($uri)) then + return + if(doc-available($uri)) then - Remove (disabled) + Remove (disabled) else -
- - + + + +
- return $form }; declare function app:list-title() From 3c9279754718156f5559a3e80e9cf86137188c16 Mon Sep 17 00:00:00 2001 From: Peter Stadler Date: Wed, 16 Feb 2022 21:45:09 +0100 Subject: [PATCH 09/47] try to rewrite the submit handler in a generic way This is unfinished work! --- resources/js/confirm.js | 44 +++++++++++++++++++++++++++++++++++------ 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/resources/js/confirm.js b/resources/js/confirm.js index 54b060f5..78ac6120 100755 --- a/resources/js/confirm.js +++ b/resources/js/confirm.js @@ -23,14 +23,46 @@ function filename_prompt(formid, text, published) } } -function filecopy(ev) { + +/* + * callback for the copy task + */ +function copyprompt(params) { + var source = params.get("source"), + target = prompt("Copy " + source + " to:", source.substring(0, source.length -4) + "-copy.xml"); + params.append('target', target); +} + +/* + * main event handler for the forms on the main page + */ +function ajaxform(ev) { ev.preventDefault(); - var source = ev.target[0].value, target, overwrite=false; - target = prompt("Copy " + source + " to:", source.substring(0, source.length -4) + "-copy.xml"); + var source = ev.target[0].value, + endpoint = new URL(ev.target.getAttribute('action')), + method = ev.target.getAttribute('method'), + callback = ev.target.querySelector('[name=callback]').value, + params = new URLSearchParams(endpoint.search); + // append all input name-value pairs (e.g. ) as URL parameters + ev.target.querySelectorAll('input').forEach( + function(a,b) { + params.append(a.getAttribute('name'), a.getAttribute('value')) + } + ) + // check if a callback function is given + if(eval("typeof " + callback) === 'function') { + window[callback](params) + } + //console.log(params.toString()); const xhttp = new XMLHttpRequest(); - xhttp.open("POST", "../data/copy", true); + xhttp.open(method, endpoint, true); xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); - xhttp.send("source="+source+"&target="+target+"&overwrite="+overwrite); + xhttp.setRequestHeader("Accept", "application/json"); + xhttp.send(params.toString()); } -document.querySelectorAll(".copyform").forEach(el => {el.addEventListener("submit", filecopy)}); +/* + * Generic form submit event handler for the main page + * to capture clicks on e.g. "copy", or "rename" buttons + */ +document.querySelectorAll(".ajaxform").forEach(el => {el.addEventListener("submit", ajaxform)}); From 6c4d79797a30e9afbf35f343c48825bf853fded1 Mon Sep 17 00:00:00 2001 From: Peter Stadler Date: Wed, 16 Feb 2022 23:03:32 +0100 Subject: [PATCH 10/47] remove outdated xquery module --- modules/delete-file.xq | 45 ------------------------------------------ 1 file changed, 45 deletions(-) delete mode 100755 modules/delete-file.xq diff --git a/modules/delete-file.xq b/modules/delete-file.xq deleted file mode 100755 index 7c41f9a1..00000000 --- a/modules/delete-file.xq +++ /dev/null @@ -1,45 +0,0 @@ -xquery version "3.1" encoding "UTF-8"; - -import module namespace login="http://kb.dk/this/login" at "./login.xqm"; -import module namespace config="https://github.com/edirom/mermeid/config" at "./config.xqm"; - -declare namespace functx = "http://www.functx.com"; -declare namespace m="http://www.music-encoding.org/ns/mei"; -declare namespace xmldb="http://exist-db.org/xquery/xmldb"; -declare namespace request="http://exist-db.org/xquery/request"; -declare namespace response="http://exist-db.org/xquery/response"; -declare namespace fn="http://www.w3.org/2005/xpath-functions"; - -declare option exist:serialize "method=xml media-type=text/html"; - -declare variable $dcmroot := $config:data-root; - -let $return_to := config:link-to-app("modules/list_files.xq") - - -let $log-in := login:function() -let $res := response:redirect-to($return_to cast as xs:anyURI) -let $parameters := request:get-parameter-names() - -return -

- { - for $resource in $parameters - where request:get-parameter($resource,"")="delete" and contains($resource,"xml") - return xmldb:remove(xs:anyURI($dcmroot), $resource) - } -

- -(: -xquery version "3.1" encoding "UTF-8"; - -declare namespace xmldb="http://exist-db.org/xquery/xmldb"; - -let $dcmroot := "/db/dcm/" -let $resource := "nielsen_cnw0126.xml" - -return xmldb:remove(xs:anyURI($dcmroot), $resource) -:) - - - From 8b9f2b488c26085a567f1c2c154aaa0c5a8ef923 Mon Sep 17 00:00:00 2001 From: Peter Stadler Date: Thu, 17 Feb 2022 00:49:26 +0100 Subject: [PATCH 11/47] add new xquery module "common" and copy over some common functions --- modules/common.xqm | 53 ++++++++++++++++++++++++++++++++++++++++++ modules/list_files.xq | 35 ++++++++++------------------ modules/list_utils.xqm | 26 --------------------- 3 files changed, 65 insertions(+), 49 deletions(-) create mode 100644 modules/common.xqm diff --git a/modules/common.xqm b/modules/common.xqm new file mode 100644 index 00000000..3a40b50c --- /dev/null +++ b/modules/common.xqm @@ -0,0 +1,53 @@ +xquery version "3.1"; + +(:~ + : Common MerMEId XQuery functions + :) +module namespace common="https://github.com/edirom/mermeid/common"; + +declare namespace mei="http://www.music-encoding.org/ns/mei"; +declare namespace map="http://www.w3.org/2005/xpath-functions/map"; + +import module namespace config="https://github.com/edirom/mermeid/config" at "config.xqm"; + +declare function common:display-date($doc as node()?) as xs:string { + if($doc//mei:workList/mei:work/mei:creation/mei:date/(@notbefore|@notafter|@startdate|@enddate)!='') then + concat(substring($doc//mei:workList/mei:work/mei:creation/mei:date/@notbefore,1,4), + substring($doc//mei:workList/mei:work/mei:creation/mei:date/@startdate,1,4), + '-', + substring($doc//mei:workList/mei:work/mei:creation/mei:date/@enddate,1,4), + substring($doc//mei:workList/mei:work/mei:creation/mei:date/@notafter,1,4)) + else if($doc//mei:workList/mei:work/mei:creation/mei:date/@isodate!='') then + substring($doc//mei:workList/mei:work/mei:creation/mei:date[1]/@isodate,1,4) + else if($doc//mei:workList/mei:work/mei:expressionList/mei:expression[mei:creation/mei:date][1]/mei:creation/mei:date/(@notbefore|@notafter|@startdate|@enddate)!='') then + concat(substring($doc//mei:workList/mei:work/mei:expressionList/mei:expression[mei:creation/mei:date][1]/mei:creation/mei:date/@notbefore,1,4), + substring($doc//mei:workList/mei:work/mei:expressionList/mei:expression[mei:creation/mei:date][1]/mei:creation/mei:date/@startdate,1,4), + '-', + substring($doc//mei:workList/mei:work/mei:expressionList/mei:expression[mei:creation/mei:date][1]/mei:creation/mei:date/@enddate,1,4), + substring($doc//mei:workList/mei:work/mei:expressionList/mei:expression[mei:creation/mei:date][1]/mei:creation/mei:date/@notafter,1,4)) + else + substring($doc//mei:workList/mei:work/mei:expressionList/mei:expression[mei:creation/mei:date][1]/mei:creation/mei:date[@isodate][1]/@isodate,1,4) +}; + +declare function common:get-edition-and-number($doc as node()?) as xs:string* { + let $c := ($doc//mei:fileDesc/mei:seriesStmt/mei:identifier[@type="file_collection"])[1] => normalize-space() + let $no := ($doc//mei:meiHead/mei:workList/mei:work/mei:identifier[normalize-space(@label)=$c])[1] => normalize-space() + (: shorten very long identifiers (i.e. lists of numbers) :) + let $part1 := substring($no, 1, 11) + let $part2 := substring($no, 12) + let $delimiter := substring(concat(translate($part2,'0123456789',''),' '),1,1) + let $n := + if (string-length($no)>11) then + concat($part1,substring-before($part2,$delimiter),'...') + else + $no + return ($c, $n) +}; + +declare function common:get-composers($doc as node()?) as xs:string? { + $doc//mei:workList/mei:work/mei:contributor/mei:persName[@role='composer'] => string-join(', ') +}; + +declare function common:get-title($doc as node()?) as xs:string { + ($doc//mei:workList/mei:work/mei:title[text()])[1] => normalize-space() +}; diff --git a/modules/list_files.xq b/modules/list_files.xq index 9138e768..1008138a 100755 --- a/modules/list_files.xq +++ b/modules/list_files.xq @@ -3,6 +3,7 @@ xquery version "3.0" encoding "UTF-8"; import module namespace loop="http://kb.dk/this/getlist" at "./main_loop.xqm"; import module namespace app="http://kb.dk/this/listapp" at "./list_utils.xqm"; import module namespace config="https://github.com/edirom/mermeid/config" at "./config.xqm"; +import module namespace common="https://github.com/edirom/mermeid/common" at "./common.xqm"; declare namespace xl="http://www.w3.org/1999/xlink"; declare namespace request="http://exist-db.org/xquery/request"; @@ -64,39 +65,27 @@ declare function local:format-reference( else "even" - let $date_output := - if($doc//m:workList/m:work/m:creation/m:date/(@notbefore|@notafter|@startdate|@enddate)!='') then - concat(substring($doc//m:workList/m:work/m:creation/m:date/@notbefore,1,4), - substring($doc//m:workList/m:work/m:creation/m:date/@startdate,1,4), - '-', - substring($doc//m:workList/m:work/m:creation/m:date/@enddate,1,4), - substring($doc//m:workList/m:work/m:creation/m:date/@notafter,1,4)) - else if($doc//m:workList/m:work/m:creation/m:date/@isodate!='') then - substring($doc//m:workList/m:work/m:creation/m:date[1]/@isodate,1,4) - else if($doc//m:workList/m:work/m:expressionList/m:expression[m:creation/m:date][1]/m:creation/m:date/(@notbefore|@notafter|@startdate|@enddate)!='') then - concat(substring($doc//m:workList/m:work/m:expressionList/m:expression[m:creation/m:date][1]/m:creation/m:date/@notbefore,1,4), - substring($doc//m:workList/m:work/m:expressionList/m:expression[m:creation/m:date][1]/m:creation/m:date/@startdate,1,4), - '-', - substring($doc//m:workList/m:work/m:expressionList/m:expression[m:creation/m:date][1]/m:creation/m:date/@enddate,1,4), - substring($doc//m:workList/m:work/m:expressionList/m:expression[m:creation/m:date][1]/m:creation/m:date/@notafter,1,4)) - else - substring($doc//m:workList/m:work/m:expressionList/m:expression[m:creation/m:date][1]/m:creation/m:date[@isodate][1]/@isodate,1,4) - (: for some reason the sort-key function must be called outside the actual searching to have correct work number sorting when searching within all collections :) let $dummy := loop:sort-key("dummy_collection", $doc, "null") let $ref := - {$doc//m:workList/m:work/m:contributor/m:persName[@role='composer']} + {common:get-composers($doc)} - {app:view-document-reference($doc)} - {" ",$date_output} - {app:get-edition-and-number($doc)} + + + {common:get-title($doc)} + + + {common:display-date($doc)} + {common:get-edition-and-number($doc)} + href="{config:link-to-app('data/read') || '?filename=' || util:document-name($doc)}"> view source11) then - concat($part1,substring-before($part2,$delimiter),'...') - else - $no - return ($c, $n) - }; - - declare function app:view-document-reference($doc as node()) as node() { - (: it is assumed that we live in the same collection 'modules' :) - let $ref := - - {$doc//m:workList/m:work/m:title[1]/string()} - - return $ref - }; declare function app:view-document-notes($doc as node()) as node() { let $note := $doc//m:fileDesc/m:notesStmt/m:annot[@type='private_notes']/string() From 823d860041b2cc5f0e3afc5ace3b27f1bc7838d6 Mon Sep 17 00:00:00 2001 From: Peter Stadler Date: Thu, 17 Feb 2022 00:50:10 +0100 Subject: [PATCH 12/47] add new crud endpoint "read" --- data/controller.xql | 12 ++++++++++++ modules/crud.xqm | 29 +++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/data/controller.xql b/data/controller.xql index b82da7bf..153e6bb2 100644 --- a/data/controller.xql +++ b/data/controller.xql @@ -127,6 +127,18 @@ else if($exist:path = '/delete' and request:get-method() eq 'POST') then if(request:get-header('Accept') eq 'application/json') then output:stream-json($backend-response, $backend-response(1)?code) else output:redirect-to-main-page() +(:~ + : read files endpoint +~:) +else if($exist:path = '/read' and request:get-method() eq 'GET') then + let $filename := request:get-parameter('filename', '') + let $backend-response := crud:read($filename) + return + if(request:get-header('Accept') eq 'application/json') + then output:stream-json(map:remove($backend-response, 'document-node'), $backend-response?code) + else if(contains(request:get-header('Accept'), 'application/xml')) + then $backend-response?document-node + else () else (: everything else is passed through :) (console:log('/data Controller: passthrough'), diff --git a/modules/crud.xqm b/modules/crud.xqm index f8f02835..9a3a1e50 100644 --- a/modules/crud.xqm +++ b/modules/crud.xqm @@ -17,6 +17,7 @@ declare namespace err="http://www.w3.org/2005/xqt-errors"; declare namespace jb="http://exist.sourceforge.net/NS/exist/java-binding"; import module namespace config="https://github.com/edirom/mermeid/config" at "config.xqm"; +import module namespace common="https://github.com/edirom/mermeid/common" at "common.xqm"; (:~ : Delete files within the data directory @@ -158,6 +159,34 @@ declare function crud:copy($source-filename as xs:string, $target-filename as xs } }; +(:~ + : Read a file from the data directory + : + :) +declare function crud:read($filename as xs:string) as map(*) { + let $doc := + if(doc-available($config:data-root || '/' || $filename)) + then doc($config:data-root || '/' || $filename) + else () + return + if($doc) + then map { + 'filename': $filename, + 'document-node': $doc, + 'composer': common:get-composers($doc), + 'title': common:get-title($doc), + 'year': common:display-date($doc), + 'collection': common:get-edition-and-number($doc), + 'message': 'read successfully', + 'code': 200 + } + else map { + 'filename': $filename, + 'message': 'file not found or permissions missing', + 'code': 404 + } +}; + (:~ : Append "copy" to the MEI title : Helper function for crud:copy() From 0c23b5ebaeef483e750e370fc782cdd695f74ac9 Mon Sep 17 00:00:00 2001 From: Peter Stadler Date: Thu, 17 Feb 2022 22:21:25 +0100 Subject: [PATCH 13/47] remove logging --- data/controller.xql | 1 - 1 file changed, 1 deletion(-) diff --git a/data/controller.xql b/data/controller.xql index 153e6bb2..ef33167d 100644 --- a/data/controller.xql +++ b/data/controller.xql @@ -108,7 +108,6 @@ else if($exist:path = '/copy' and request:get-method() eq 'POST') then let $overwriteString := request:get-parameter('overwrite', 'false') let $overwrite := $overwriteString = ('1', 'yes', 'ja', 'y', 'true', 'true()') (: some string values that are considered boolean "true()" :) let $backend-response := crud:copy($source, $target, $overwrite, $title) - let $log := util:log-system-out(request:get-header('Accept')) return if(request:get-header('Accept') eq 'application/json') then output:stream-json($backend-response, $backend-response?code) From ea307dfa4b8e77fffa56f339cc6a0c1c7b02af26 Mon Sep 17 00:00:00 2001 From: Peter Stadler Date: Thu, 17 Feb 2022 22:21:34 +0100 Subject: [PATCH 14/47] adjust error code --- modules/crud.xqm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/crud.xqm b/modules/crud.xqm index 9a3a1e50..90a78b54 100644 --- a/modules/crud.xqm +++ b/modules/crud.xqm @@ -155,7 +155,7 @@ declare function crud:copy($source-filename as xs:string, $target-filename as xs 'source': $source-filename, 'target': $target-filename, 'message': 'source does not exist', - 'code': 200 + 'code': 404 } }; From 1af0c6e7d2307ce5e1b1c982d844d5181107e180 Mon Sep 17 00:00:00 2001 From: Peter Stadler Date: Fri, 18 Feb 2022 09:11:39 +0100 Subject: [PATCH 15/47] make sure to return only one string --- modules/common.xqm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/common.xqm b/modules/common.xqm index 3a40b50c..86f13bbb 100644 --- a/modules/common.xqm +++ b/modules/common.xqm @@ -29,7 +29,7 @@ declare function common:display-date($doc as node()?) as xs:string { substring($doc//mei:workList/mei:work/mei:expressionList/mei:expression[mei:creation/mei:date][1]/mei:creation/mei:date[@isodate][1]/@isodate,1,4) }; -declare function common:get-edition-and-number($doc as node()?) as xs:string* { +declare function common:get-edition-and-number($doc as node()?) as xs:string { let $c := ($doc//mei:fileDesc/mei:seriesStmt/mei:identifier[@type="file_collection"])[1] => normalize-space() let $no := ($doc//mei:meiHead/mei:workList/mei:work/mei:identifier[normalize-space(@label)=$c])[1] => normalize-space() (: shorten very long identifiers (i.e. lists of numbers) :) @@ -41,7 +41,7 @@ declare function common:get-edition-and-number($doc as node()?) as xs:string* { concat($part1,substring-before($part2,$delimiter),'...') else $no - return ($c, $n) + return concat($c, ' ', $n) }; declare function common:get-composers($doc as node()?) as xs:string? { From b2e1fd32877f6f2934d2c0c13677ee29f3954869 Mon Sep 17 00:00:00 2001 From: Peter Stadler Date: Fri, 18 Feb 2022 11:06:21 +0100 Subject: [PATCH 16/47] make the copy task build upon create rather than its own implementation --- data/controller.xql | 2 +- modules/crud.xqm | 71 ++++++++++++--------------------------------- 2 files changed, 20 insertions(+), 53 deletions(-) diff --git a/data/controller.xql b/data/controller.xql index ef33167d..26c6c064 100644 --- a/data/controller.xql +++ b/data/controller.xql @@ -110,7 +110,7 @@ else if($exist:path = '/copy' and request:get-method() eq 'POST') then let $backend-response := crud:copy($source, $target, $overwrite, $title) return if(request:get-header('Accept') eq 'application/json') - then output:stream-json($backend-response, $backend-response?code) + then output:stream-json(map:remove($backend-response, 'document-node'), $backend-response?code) else output:redirect-to-main-page() (:~ : delete files endpoint diff --git a/modules/crud.xqm b/modules/crud.xqm index 90a78b54..e2fa1f71 100644 --- a/modules/crud.xqm +++ b/modules/crud.xqm @@ -59,21 +59,24 @@ declare function crud:delete($filenames as xs:string*) as array(map(xs:string,xs : : @param $node the XML document to store : @param $filename the filename for the new file + : @param $overwrite whether an existent file may be overwritten : @return a map object with filename, message and code properties concerning the create operation :) -declare function crud:create($node as node(), $filename as xs:string) as map(xs:string,xs:string) { +declare function crud:create($node as node(), $filename as xs:string, $overwrite as xs:boolean) as map(*) { try { - if(xmldb:store($config:data-root, $filename, $node)) - then map { - 'filename': $filename, - 'message': 'created successfully', - 'code': 200 - } + if(not(doc-available($config:data-root || '/' || $filename)) or $overwrite) + then if(xmldb:store($config:data-root, $filename, $node)) + then crud:read($filename) => map:put('message', 'created successfully') else map { 'filename': $filename, 'message': 'failed to create file', 'code': 500 } + else map { + 'filename': $filename, + 'message': 'file already exists and no overwrite flag was set', + 'code': 401 + } } catch jb:org.xmldb.api.base.XMLDBException { map { @@ -100,57 +103,21 @@ declare function crud:create($node as node(), $filename as xs:string) as map(xs: : @param $new_title an optional new title. If omitted, the string "(copy)" will be appended to the old title : @return a map object with source, target, message and code properties concerning the copy operation :) -declare function crud:copy($source-filename as xs:string, $target-filename as xs:string, $overwrite as xs:boolean, $new_title as xs:string?) as map(xs:string,xs:string) { +declare function crud:copy($source-filename as xs:string, $target-filename as xs:string, $overwrite as xs:boolean, $new_title as xs:string?) as map(*) { let $source := if(doc-available($config:data-root || '/' || $source-filename)) then doc($config:data-root || '/' || $source-filename) else () let $create-target := - try { - if($source and (not(doc-available($config:data-root || '/' || $target-filename)) or $overwrite)) - then xmldb:store($config:data-root, $target-filename, $source) => crud:adjust-mei-title($new_title) - else () - } - catch jb:org.xmldb.api.base.XMLDBException { - map { - 'source': $source-filename, - 'target': $target-filename, - 'message': 'failed to copy file: ' || $err:description, - 'code': 401 - } - } - catch * { - map { - 'source': $source-filename, - 'target': $target-filename, - 'message': 'failed to copy file: ' || string-join(($err:code, $err:description), '; '), - 'code': 500 - } - } - return - if($create-target instance of map(*)) + if($source) then crud:create($source, $target-filename, $overwrite) + else () + let $adjust-mei-title := + if($create-target?code = 200) + then crud:adjust-mei-title($target-filename, $new_title) + else () + return + if($source) then $create-target - else if($create-target instance of xs:string) - then map { - 'source': $source-filename, - 'target': $target-filename, - 'message': 'copied successfully', - 'code': 200 - } - else if(doc-available($config:data-root || '/' || $target-filename) and not($overwrite)) - then map { - 'source': $source-filename, - 'target': $target-filename, - 'message': 'target already existent and "overwrite" flag was missing', - 'code': 500 - } - else if($source) - then map { - 'source': $source-filename, - 'target': $target-filename, - 'message': 'failed to create target', - 'code': 500 - } else map { 'source': $source-filename, 'target': $target-filename, From 6147bfaf7f627b2258a4f6003535bc37586728cf Mon Sep 17 00:00:00 2001 From: Peter Stadler Date: Fri, 18 Feb 2022 13:06:55 +0100 Subject: [PATCH 17/47] add another value that gets sent by javascript --- data/controller.xql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/controller.xql b/data/controller.xql index 26c6c064..02734124 100644 --- a/data/controller.xql +++ b/data/controller.xql @@ -106,7 +106,7 @@ else if($exist:path = '/copy' and request:get-method() eq 'POST') then let $target := request:get-parameter('target', util:uuid() || '.xml') (: generate a unique filename if none is provided :) let $title := request:get-parameter('title', ()) (: empty titles will get passed on and filled in later :) let $overwriteString := request:get-parameter('overwrite', 'false') - let $overwrite := $overwriteString = ('1', 'yes', 'ja', 'y', 'true', 'true()') (: some string values that are considered boolean "true()" :) + let $overwrite := $overwriteString = ('1', 'yes', 'ja', 'y', 'on', 'true', 'true()') (: some string values that are considered boolean "true()" :) let $backend-response := crud:copy($source, $target, $overwrite, $title) return if(request:get-header('Accept') eq 'application/json') From 2159bd3869e203c09d95a5ada32d16a83c0bb062 Mon Sep 17 00:00:00 2001 From: Peter Stadler Date: Fri, 18 Feb 2022 13:07:46 +0100 Subject: [PATCH 18/47] rework frontend for copying and deleting files --- confirm.html | 25 +++++++ modules/list_files.xq | 4 +- modules/list_utils.xqm | 29 +++++--- resources/css/list_style.css | 31 ++++++++- resources/js/confirm.js | 124 ++++++++++++++++++++++++----------- 5 files changed, 164 insertions(+), 49 deletions(-) create mode 100644 confirm.html diff --git a/confirm.html b/confirm.html new file mode 100644 index 00000000..138c8fa2 --- /dev/null +++ b/confirm.html @@ -0,0 +1,25 @@ + \ No newline at end of file diff --git a/modules/list_files.xq b/modules/list_files.xq index 1008138a..99fa97d7 100755 --- a/modules/list_files.xq +++ b/modules/list_files.xq @@ -316,10 +316,8 @@ declare function local:format-reference( } {doc('../login.html')/*} + {doc('../confirm.html')/*} {config:replace-properties(config:get-property('footer'))} - diff --git a/modules/list_utils.xqm b/modules/list_utils.xqm index 44a49379..98efe572 100755 --- a/modules/list_utils.xqm +++ b/modules/list_utils.xqm @@ -2,6 +2,7 @@ xquery version "1.0" encoding "UTF-8"; module namespace app="http://kb.dk/this/listapp"; import module namespace config="https://github.com/edirom/mermeid/config" at "./config.xqm"; +import module namespace common="https://github.com/edirom/mermeid/common" at "./common.xqm"; declare namespace file="http://exist-db.org/xquery/file"; declare namespace fn="http://www.w3.org/2005/xpath-functions"; @@ -112,12 +113,22 @@ let $options:= declare function app:copy-document-reference($doc as node()) as node() { let $doc-name := util:document-name($doc) + let $title := common:get-title($doc) let $uri := concat($config:data-public-root, "/", util:document-name($doc)) return -
- - - + + + + + + + + +
}; @@ -149,10 +160,12 @@ let $options:= Remove (disabled) else -
- - - + + + + +
}; diff --git a/resources/css/list_style.css b/resources/css/list_style.css index ee5e9c2e..eff67926 100755 --- a/resources/css/list_style.css +++ b/resources/css/list_style.css @@ -354,4 +354,33 @@ form.search a.help .help_label { padding-right: 8px; white-space: nowrap; font-family: courier, fixed, monospace; -} \ No newline at end of file +} + +.ajaxform .ajaxform_input, .ajaxform .ajaxform_label { + display: none; +} + +form.modal-content button { + display: block; + margin-top: 2ex; +} + +#confirm_modal_statusmessage { + display:none; +} + +#confirm_modal_statusmessage.error { + display: block; + border: 1px solid red; + padding: 1ex; + color: red; + font-weight: bold; +} + +#confirm_modal_statusmessage.info { + display: block; + border: 1px solid green; + padding: 1ex; + color: green; + font-weight: bold; +} diff --git a/resources/js/confirm.js b/resources/js/confirm.js index 78ac6120..06b1cb62 100755 --- a/resources/js/confirm.js +++ b/resources/js/confirm.js @@ -1,13 +1,3 @@ -function show_confirm(formid, text) -{ - var r=confirm("Do you really want to delete the file '" + text +"'?" ); - if (r==true) { - var form = document.getElementById(formid); - form.submit(); - } else { - } -} - function filename_prompt(formid, text, published) { if (published) { @@ -23,42 +13,94 @@ function filename_prompt(formid, text, published) } } - /* - * callback for the copy task + * main event handler for the forms on the main page + * it will simply show the modal and clone all inputs etc + * to the modal form */ -function copyprompt(params) { - var source = params.get("source"), - target = prompt("Copy " + source + " to:", source.substring(0, source.length -4) + "-copy.xml"); - params.append('target', target); +function ajaxform(ev) { + ev.preventDefault(); + const curForm = ev.target, + orgButton = curForm.querySelector("button[type=submit]"); + + // remove class information from the status message to hide it initially + document.getElementById("confirm_modal_statusmessage").className = ""; + // set the title of the modal + document.getElementById("confirm_modal_heading").innerHTML = ev.target.getAttribute("title"); + // copy the action and method attribute to the modal form + document.querySelector("#confirm_modal form").setAttribute("action", curForm.getAttribute("action")); + document.querySelector("#confirm_modal form").setAttribute("method", curForm.getAttribute("method")); + // empty the modal body + document.getElementById("confirm_modal_body").innerHTML = ""; + // fill the modal body by (deep) cloning inputs and labels from the original form + curForm.querySelectorAll(".ajaxform_input, .ajaxform_label").forEach(function(p) { + document.getElementById("confirm_modal_body").appendChild(p.cloneNode(true)); + }) + // add the submit button by (shallow) cloning the original button + document.getElementById("confirm_modal_body").appendChild(orgButton.cloneNode(false)).innerHTML=orgButton.value; + // finally show the modal + document.getElementById("confirm_modal").style.display = "block"; } /* - * main event handler for the forms on the main page + * event handler for the modal form + * it will call the backend crud functions via AJAX and + * update the main table if successfull */ -function ajaxform(ev) { +function modal_submit_handler(ev) { ev.preventDefault(); - var source = ev.target[0].value, - endpoint = new URL(ev.target.getAttribute('action')), - method = ev.target.getAttribute('method'), - callback = ev.target.querySelector('[name=callback]').value, - params = new URLSearchParams(endpoint.search); - // append all input name-value pairs (e.g. ) as URL parameters - ev.target.querySelectorAll('input').forEach( - function(a,b) { - params.append(a.getAttribute('name'), a.getAttribute('value')) + const curForm = ev.target, + endpoint = new URL(curForm.getAttribute("action")), + method = curForm.getAttribute("method"); + params = new URLSearchParams(new FormData(curForm)); + + fetch(endpoint, { + method: method, + headers: { + "Content-type": "application/x-www-form-urlencoded", + "Accept": "application/json" + }, + body: params + }) + .then(response => { + if (!response.ok) { + throw response; + } + return response.json(); + }) + .then(data => { + update_statusmessage(data, "info"); + setTimeout(() => { + document.getElementById("confirm_modal").style.display = "none"; + location.reload(); // instead of simply reloading the page we might update the table dynamically + }, 1000); + + }) + .catch(error => { + if (typeof error.json === 'function') { + error.json().then( + obj => { + update_statusmessage(obj, "error"); + } + )} + else { + update_statusmessage("Some unknown error occured", "error"); } - ) - // check if a callback function is given - if(eval("typeof " + callback) === 'function') { - window[callback](params) + }) +}; + +/* + * helper function for modal_submit_handler() + */ +function update_statusmessage(obj, classname) { + if(obj.constructor === Array) { + document.getElementById("confirm_modal_statusmessage").innerHTML = obj[0].message; + document.getElementById("confirm_modal_statusmessage").className = classname; + } + else { + document.getElementById("confirm_modal_statusmessage").innerHTML = obj.message; + document.getElementById("confirm_modal_statusmessage").className = classname; } - //console.log(params.toString()); - const xhttp = new XMLHttpRequest(); - xhttp.open(method, endpoint, true); - xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); - xhttp.setRequestHeader("Accept", "application/json"); - xhttp.send(params.toString()); } /* @@ -66,3 +108,11 @@ function ajaxform(ev) { * to capture clicks on e.g. "copy", or "rename" buttons */ document.querySelectorAll(".ajaxform").forEach(el => {el.addEventListener("submit", ajaxform)}); +document.querySelector("#confirm_modal form").addEventListener("submit", modal_submit_handler); + +/* + * close modal on click + */ +document.querySelectorAll(".close-modal").forEach(el => {el.addEventListener("click", function(ev) { + document.getElementById("confirm_modal").style.display = "none";}, false) +}); From 4d406ff8b9f9a40ce70471b2fc2f7076e8747506 Mon Sep 17 00:00:00 2001 From: Peter Stadler Date: Fri, 18 Feb 2022 17:10:33 +0100 Subject: [PATCH 19/47] add a "rename" endpoint as a simple chain of "copy" and a "delete" --- data/controller.xql | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/data/controller.xql b/data/controller.xql index 02734124..e1db6be2 100644 --- a/data/controller.xql +++ b/data/controller.xql @@ -100,7 +100,7 @@ if (ends-with($exist:resource, ".xml")) then : of the crud operation (a map object) is returned as JSON, for all other : Accept headers the client is redirected to the main page (after the : execution of the crud operation) -~:) + :) else if($exist:path = '/copy' and request:get-method() eq 'POST') then let $source := request:get-parameter('source', '') let $target := request:get-parameter('target', util:uuid() || '.xml') (: generate a unique filename if none is provided :) @@ -118,7 +118,7 @@ else if($exist:path = '/copy' and request:get-method() eq 'POST') then : of the crud operation (an array) is returned as JSON, for all other : Accept headers the client is redirected to the main page (after the : execution of the crud operation) -~:) + :) else if($exist:path = '/delete' and request:get-method() eq 'POST') then let $filename := request:get-parameter('filename', '') let $backend-response := crud:delete($filename) @@ -128,7 +128,10 @@ else if($exist:path = '/delete' and request:get-method() eq 'POST') then else output:redirect-to-main-page() (:~ : read files endpoint -~:) + : For GET requests with Accept header 'application/json' the response + : of the crud operation (a map object) is returned as JSON, for an + : "application/xml" the raw XML document is returned + :) else if($exist:path = '/read' and request:get-method() eq 'GET') then let $filename := request:get-parameter('filename', '') let $backend-response := crud:read($filename) @@ -138,6 +141,28 @@ else if($exist:path = '/read' and request:get-method() eq 'GET') then else if(contains(request:get-header('Accept'), 'application/xml')) then $backend-response?document-node else () +(:~ + : rename files endpoint + : this simply chains a "copy" and a "delete" (if the first operation was successfull) + : the returned object is a merge of the copy-response and the delete-response with a precedence for the former + :) +else if($exist:path = '/rename' and request:get-method() eq 'POST') then + let $source := request:get-parameter('source', '') + let $target := request:get-parameter('target', util:uuid() || '.xml') (: generate a unique filename if none is provided :) + let $title := request:get-parameter('title', ()) (: empty titles will get passed on and filled in later :) + let $overwriteString := request:get-parameter('overwrite', 'false') + let $overwrite := $overwriteString = ('1', 'yes', 'ja', 'y', 'on', 'true', 'true()') (: some string values that are considered boolean "true()" :) + let $backend-response-copy := crud:copy($source, $target, $overwrite, $title) + let $backend-response-delete := + if($backend-response-copy instance of map(*) and $backend-response-copy?code = 200) + then crud:delete($source) + else () + return + if(request:get-header('Accept') eq 'application/json') + then if($backend-response-delete instance of array(*)) + then output:stream-json(map:remove(map:merge(($backend-response-delete(1), $backend-response-copy)), 'document-node'), $backend-response-copy?code) + else output:stream-json($backend-response-copy, $backend-response-copy?code) + else output:redirect-to-main-page() else (: everything else is passed through :) (console:log('/data Controller: passthrough'), From 5875ab30b2e5097f3ba4c5ba1df7ba6bc3a2fd63 Mon Sep 17 00:00:00 2001 From: Peter Stadler Date: Fri, 18 Feb 2022 17:12:17 +0100 Subject: [PATCH 20/47] align html markup of the XML link especially add a button --- modules/list_files.xq | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/modules/list_files.xq b/modules/list_files.xq index 99fa97d7..fd7fa487 100755 --- a/modules/list_files.xq +++ b/modules/list_files.xq @@ -83,14 +83,17 @@ declare function local:format-reference( {common:display-date($doc)} {common:get-edition-and-number($doc)} - - view source - + action="{config:link-to-app('data/read')}"> + + + {app:edit-form-reference($doc)} {app:copy-document-reference($doc)} From 077869c53e25d21b5eba034ae20fa482b73b6ca7 Mon Sep 17 00:00:00 2001 From: Peter Stadler Date: Fri, 18 Feb 2022 17:13:55 +0100 Subject: [PATCH 21/47] make use of the new "rename" endpoint and some reformatting, and addition of some HTML namespaces --- modules/list_utils.xqm | 226 ++++++++++++++++++++--------------------- 1 file changed, 109 insertions(+), 117 deletions(-) diff --git a/modules/list_utils.xqm b/modules/list_utils.xqm index 98efe572..e04590af 100755 --- a/modules/list_utils.xqm +++ b/modules/list_utils.xqm @@ -1,4 +1,4 @@ -xquery version "1.0" encoding "UTF-8"; +xquery version "3.1" encoding "UTF-8"; module namespace app="http://kb.dk/this/listapp"; import module namespace config="https://github.com/edirom/mermeid/config" at "./config.xqm"; @@ -14,6 +14,7 @@ declare namespace response="http://exist-db.org/xquery/response"; declare namespace util="http://exist-db.org/xquery/util"; declare namespace xl="http://www.w3.org/1999/xlink"; declare namespace xdb="http://exist-db.org/xquery/xmldb"; +declare namespace html="http://www.w3.org/1999/xhtml"; declare function app:options() as node()* @@ -29,145 +30,136 @@ let $options:= }; - - declare function app:get-publication-reference($doc as node() ) as node()* - { - let $doc-name:=util:document-name($doc) - let $color_style := - if(doc-available(concat($config:data-public-root,'/',$doc-name))) then - ( - let $dcmtime := xs:dateTime(xdb:last-modified($config:data-root, $doc-name)) - let $pubtime := xs:dateTime(xdb:last-modified($config:data-public-root, $doc-name)) - return - if($dcmtime lt $pubtime) then - "publishedIsGreen" - else - "pendingIsYellow" - ) - else - "unpublishedIsRed" - - let $form:= -
- -
- - - - - -
-
- return $form - }; +declare function app:get-publication-reference($doc as node()) as element(html:form) { + let $doc-name:= util:document-name($doc) + let $color_style := + if(doc-available(concat($config:data-public-root,'/',$doc-name))) + then ( + let $dcmtime := xs:dateTime(xdb:last-modified($config:data-root, $doc-name)) + let $pubtime := xs:dateTime(xdb:last-modified($config:data-public-root, $doc-name)) + return + if($dcmtime lt $pubtime) + then "publishedIsGreen" + else "pendingIsYellow" + ) + else "unpublishedIsRed" + return +
+
+ + +
+
+}; - declare function app:view-document-notes($doc as node()) as node() { - let $note := $doc//m:fileDesc/m:notesStmt/m:annot[@type='private_notes']/string() - let $n := +declare function app:view-document-notes($doc as node()) as element() { + let $note := $doc//m:fileDesc/m:notesStmt/m:annot[@type='private_notes']/string() + return if (string-length($note)>20) then - {concat(substring($note,1,20), substring-before(substring($note,21),' '))}...{$note} + {concat(substring($note,1,20), + substring-before(substring($note,21),' '))}...{$note} else - {$note} - return $n - }; - - - declare function app:edit-form-reference($doc as node()) as node() - { - (: - Beware: Partly hard coded reference here!!! - It still assumes that the document resides on the same host as this - xq script but on port 80 - - The old form is called edit_mei_form.xml the refactored one starts on - edit-work-case.xml - :) - - let $form-id := util:document-name($doc) - let $ref := - - return $ref + {$note} +}; - }; +declare function app:edit-form-reference($doc as node()) as element(html:form) { +
+ + +
+}; - declare function app:copy-document-reference($doc as node()) as node() - { - let $doc-name := util:document-name($doc) - let $title := common:get-title($doc) - let $uri := concat($config:data-public-root, "/", util:document-name($doc)) - return -
- - - - - - - - -
- }; +declare function app:copy-document-reference($doc as node()) as element(html:form) { + let $doc-name := util:document-name($doc) + let $title := common:get-title($doc) + let $uri := concat($config:data-public-root, "/", $doc-name) + return +
+ + + + + + + + +
+}; - declare function app:rename-document-reference($doc as node()) as node() - { - let $doc-name := util:document-name($doc) - let $form-id := concat("rename",$doc-name) - let $uri := concat($config:data-public-root,"/",$doc-name) - let $form := -
- - - Rename -
- return $form - }; +declare function app:rename-document-reference($doc as node()) as element(html:form) { + let $doc-name := util:document-name($doc) + let $title := common:get-title($doc) + let $uri := concat($config:data-public-root, "/", $doc-name) + return +
+ + + + + + + + +
+}; - declare function app:delete-document-reference($doc as node()) as node() - { - let $doc-name := util:document-name($doc) - let $uri := concat($config:data-public-root,"/",util:document-name($doc)) - return +declare function app:delete-document-reference($doc as node()) as element() { + let $doc-name := util:document-name($doc) + let $uri := concat($config:data-public-root,"/",util:document-name($doc)) + return if(doc-available($uri)) then - + Remove (disabled) else
- }; +}; + declare function app:list-title() { From b7a78735056aa427b0544d3e5d8f0faec343bed5 Mon Sep 17 00:00:00 2001 From: Peter Stadler Date: Fri, 18 Feb 2022 17:14:42 +0100 Subject: [PATCH 22/47] remove border so the look of the buttons stays the same as before --- resources/css/list_style.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/resources/css/list_style.css b/resources/css/list_style.css index eff67926..ec80ef69 100755 --- a/resources/css/list_style.css +++ b/resources/css/list_style.css @@ -384,3 +384,7 @@ form.modal-content button { color: green; font-weight: bold; } + +.result_table button { + border: none; +} From 7ed5dd392e8ba0655693639784108bf8d5bb8265 Mon Sep 17 00:00:00 2001 From: Peter Stadler Date: Fri, 18 Feb 2022 17:22:21 +0100 Subject: [PATCH 23/47] replace hyphen with ndash --- modules/common.xqm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/common.xqm b/modules/common.xqm index 86f13bbb..ffd896ce 100644 --- a/modules/common.xqm +++ b/modules/common.xqm @@ -14,7 +14,7 @@ declare function common:display-date($doc as node()?) as xs:string { if($doc//mei:workList/mei:work/mei:creation/mei:date/(@notbefore|@notafter|@startdate|@enddate)!='') then concat(substring($doc//mei:workList/mei:work/mei:creation/mei:date/@notbefore,1,4), substring($doc//mei:workList/mei:work/mei:creation/mei:date/@startdate,1,4), - '-', + '–', substring($doc//mei:workList/mei:work/mei:creation/mei:date/@enddate,1,4), substring($doc//mei:workList/mei:work/mei:creation/mei:date/@notafter,1,4)) else if($doc//mei:workList/mei:work/mei:creation/mei:date/@isodate!='') then @@ -22,7 +22,7 @@ declare function common:display-date($doc as node()?) as xs:string { else if($doc//mei:workList/mei:work/mei:expressionList/mei:expression[mei:creation/mei:date][1]/mei:creation/mei:date/(@notbefore|@notafter|@startdate|@enddate)!='') then concat(substring($doc//mei:workList/mei:work/mei:expressionList/mei:expression[mei:creation/mei:date][1]/mei:creation/mei:date/@notbefore,1,4), substring($doc//mei:workList/mei:work/mei:expressionList/mei:expression[mei:creation/mei:date][1]/mei:creation/mei:date/@startdate,1,4), - '-', + '–', substring($doc//mei:workList/mei:work/mei:expressionList/mei:expression[mei:creation/mei:date][1]/mei:creation/mei:date/@enddate,1,4), substring($doc//mei:workList/mei:work/mei:expressionList/mei:expression[mei:creation/mei:date][1]/mei:creation/mei:date/@notafter,1,4)) else From f287482fd0758ac856662f3039bc012fc7de2cd1 Mon Sep 17 00:00:00 2001 From: Peter Stadler Date: Fri, 18 Feb 2022 17:30:00 +0100 Subject: [PATCH 24/47] fix toggling of hide/show depending on login status --- resources/css/list_style.css | 1 + resources/js/login.js | 16 ++-------------- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/resources/css/list_style.css b/resources/css/list_style.css index ec80ef69..05a1402e 100755 --- a/resources/css/list_style.css +++ b/resources/css/list_style.css @@ -387,4 +387,5 @@ form.modal-content button { .result_table button { border: none; + cursor: pointer; } diff --git a/resources/js/login.js b/resources/js/login.js index 053c296d..64338ea9 100644 --- a/resources/js/login.js +++ b/resources/js/login.js @@ -75,23 +75,11 @@ function updateLoginInfo(obj) { } /* - * Toggle css and events for edit buttons + * Toggle css and events for forms * param: hide|show */ function toggleButtons(status) { - // input elements toggle the "disabled" attribute - document.querySelectorAll(".loginRequired input").forEach(el => { - if(status === 'show') { - el.removeAttribute('disabled'); - el.parentElement.parentElement.classList.add(status); - } - else { - el.setAttribute('disabled', 'disabled'); - el.parentElement.parentElement.classList.remove('show'); - } - }); - // all other elements toggle the pointerEvents and toggle the class "show" - document.querySelectorAll(".loginRequired a, .loginRequired img, .loginRequired label").forEach(el => { + document.querySelectorAll(".loginRequired form").forEach(el => { if(status === 'show') { el.style.pointerEvents = "auto"; el.parentElement.classList.add(status); From 48c863b2d89091f491841e57b2c00ae8020242a7 Mon Sep 17 00:00:00 2001 From: Peter Stadler Date: Fri, 18 Feb 2022 18:00:36 +0100 Subject: [PATCH 25/47] improve suggested new filenames --- modules/common.xqm | 13 +++++++++++++ modules/list_utils.xqm | 12 ++++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/modules/common.xqm b/modules/common.xqm index ffd896ce..d333776d 100644 --- a/modules/common.xqm +++ b/modules/common.xqm @@ -9,6 +9,7 @@ declare namespace mei="http://www.music-encoding.org/ns/mei"; declare namespace map="http://www.w3.org/2005/xpath-functions/map"; import module namespace config="https://github.com/edirom/mermeid/config" at "config.xqm"; +import module namespace functx="http://www.functx.com"; declare function common:display-date($doc as node()?) as xs:string { if($doc//mei:workList/mei:work/mei:creation/mei:date/(@notbefore|@notafter|@startdate|@enddate)!='') then @@ -51,3 +52,15 @@ declare function common:get-composers($doc as node()?) as xs:string? { declare function common:get-title($doc as node()?) as xs:string { ($doc//mei:workList/mei:work/mei:title[text()])[1] => normalize-space() }; + +declare function common:propose-filename($filename as xs:string) as xs:string { + let $tokens := $filename => tokenize('\.') + let $suffix := + if(count($tokens) gt 1) + then $tokens[last()] + else 'xml' + return + if(count($tokens) eq 1) + then $tokens || '-copy.' || $suffix + else (subsequence($tokens, 1, count($tokens) -1) => string-join('.')) || '-copy.' || $suffix +}; diff --git a/modules/list_utils.xqm b/modules/list_utils.xqm index e04590af..86eb291c 100755 --- a/modules/list_utils.xqm +++ b/modules/list_utils.xqm @@ -91,7 +91,9 @@ declare function app:edit-form-reference($doc as node()) as element(html:form) { declare function app:copy-document-reference($doc as node()) as element(html:form) { let $doc-name := util:document-name($doc) + let $proposed-name := common:propose-filename($doc-name) let $title := common:get-title($doc) + let $proposed-title := $title || ' (Copy)' let $uri := concat($config:data-public-root, "/", $doc-name) return
- -