From 5d822b291b95e6fc7f61c1924a5a387da3707682 Mon Sep 17 00:00:00 2001 From: Timm Fitschen <timm.fitschen@ds.mpg.de> Date: Thu, 21 Mar 2019 12:53:18 +0100 Subject: [PATCH] MAINT: new structure: core/ext --- conf/ext/README | 1 + conf/json/templates.json | 36 - makefile | 31 +- src/core/css/webcaosdb.css | 471 +++++ src/core/css/webcaosdb.less | 401 ++++ src/core/draft/welcome.html | 87 + src/core/js/annotation.js | 390 ++++ src/core/js/caosdb.js | 788 +++++++ src/core/js/edit_mode.js | 1051 ++++++++++ src/core/js/ext_cosmetics.js | 23 + src/core/js/ext_references.js | 58 + src/core/js/preview.js | 721 +++++++ src/core/js/templates_ext.js | 109 + src/core/js/webcaosdb.js | 1137 ++++++++++ src/core/owner.css | 35 + src/core/owner.xsl | 73 + src/core/permissions.css | 44 + src/core/permissions.xsl | 166 ++ src/core/pics/caosdb_logo_42.png | Bin 0 -> 2310 bytes src/core/pics/caosdb_logo_medium.png | Bin 0 -> 1940 bytes src/core/pics/caosdb_logo_small.png | Bin 0 -> 583 bytes src/core/pics/caosdb_no_undertitle.png | Bin 0 -> 11918 bytes src/core/pics/caosdb_no_undertitle_622.png | Bin 0 -> 17962 bytes src/core/webcaosdb.xsl | 53 + src/core/xsl/annotation.xsl | 79 + src/core/xsl/common.xsl | 36 + src/core/xsl/entity.xsl | 462 +++++ src/core/xsl/entity_palette.xsl | 59 + src/core/xsl/filesystem.xsl | 122 ++ src/core/xsl/main.xsl | 162 ++ src/core/xsl/messages.xsl | 75 + src/core/xsl/navbar.xsl | 212 ++ src/core/xsl/parent.xsl | 44 + src/core/xsl/property.xsl | 29 + src/core/xsl/query.xsl | 232 +++ src/core/xsl/welcome.xsl | 40 + src/ext/README | 1 + test/core/index.html | 56 + test/core/js/modules/annotation.xsl.js | 102 + test/core/js/modules/caosdb.js.js | 394 ++++ test/core/js/modules/entity.xsl.js | 210 ++ test/core/js/modules/ext_references.js.js | 49 + test/core/js/modules/navbar.xsl.js | 81 + test/core/js/modules/query.xsl.js | 131 ++ test/core/js/modules/templates_ext.js.js | 52 + test/core/js/modules/webcaosdb.css.js | 38 + test/core/js/modules/webcaosdb.js.js | 1842 +++++++++++++++++ test/core/js/modules/welcome.xsl.js | 76 + test/core/js/setup.js | 62 + test/core/xml/test_case_file_preview.xml | 55 + .../xml/test_case_list_of_myrecordtype.xml | 54 + test/core/xml/test_case_preview_entities.xml | 113 + test/ext/README | 1 + 53 files changed, 10504 insertions(+), 40 deletions(-) create mode 100644 conf/ext/README delete mode 100644 conf/json/templates.json create mode 100644 src/core/css/webcaosdb.css create mode 100644 src/core/css/webcaosdb.less create mode 100644 src/core/draft/welcome.html create mode 100644 src/core/js/annotation.js create mode 100644 src/core/js/caosdb.js create mode 100644 src/core/js/edit_mode.js create mode 100644 src/core/js/ext_cosmetics.js create mode 100644 src/core/js/ext_references.js create mode 100644 src/core/js/preview.js create mode 100644 src/core/js/templates_ext.js create mode 100644 src/core/js/webcaosdb.js create mode 100644 src/core/owner.css create mode 100644 src/core/owner.xsl create mode 100644 src/core/permissions.css create mode 100644 src/core/permissions.xsl create mode 100644 src/core/pics/caosdb_logo_42.png create mode 100644 src/core/pics/caosdb_logo_medium.png create mode 100644 src/core/pics/caosdb_logo_small.png create mode 100644 src/core/pics/caosdb_no_undertitle.png create mode 100644 src/core/pics/caosdb_no_undertitle_622.png create mode 100644 src/core/webcaosdb.xsl create mode 100644 src/core/xsl/annotation.xsl create mode 100644 src/core/xsl/common.xsl create mode 100644 src/core/xsl/entity.xsl create mode 100644 src/core/xsl/entity_palette.xsl create mode 100644 src/core/xsl/filesystem.xsl create mode 100644 src/core/xsl/main.xsl create mode 100644 src/core/xsl/messages.xsl create mode 100644 src/core/xsl/navbar.xsl create mode 100644 src/core/xsl/parent.xsl create mode 100644 src/core/xsl/property.xsl create mode 100644 src/core/xsl/query.xsl create mode 100644 src/core/xsl/welcome.xsl create mode 100644 src/ext/README create mode 100644 test/core/index.html create mode 100644 test/core/js/modules/annotation.xsl.js create mode 100644 test/core/js/modules/caosdb.js.js create mode 100644 test/core/js/modules/entity.xsl.js create mode 100644 test/core/js/modules/ext_references.js.js create mode 100644 test/core/js/modules/navbar.xsl.js create mode 100644 test/core/js/modules/query.xsl.js create mode 100644 test/core/js/modules/templates_ext.js.js create mode 100644 test/core/js/modules/webcaosdb.css.js create mode 100644 test/core/js/modules/webcaosdb.js.js create mode 100644 test/core/js/modules/welcome.xsl.js create mode 100644 test/core/js/setup.js create mode 100644 test/core/xml/test_case_file_preview.xml create mode 100644 test/core/xml/test_case_list_of_myrecordtype.xml create mode 100644 test/core/xml/test_case_preview_entities.xml create mode 100644 test/ext/README diff --git a/conf/ext/README b/conf/ext/README new file mode 100644 index 00000000..9f476656 --- /dev/null +++ b/conf/ext/README @@ -0,0 +1 @@ +This directory should not contain any files. It is a placeholder for the extensions which are copied to this directory from other repositories. diff --git a/conf/json/templates.json b/conf/json/templates.json deleted file mode 100644 index 029826b8..00000000 --- a/conf/json/templates.json +++ /dev/null @@ -1,36 +0,0 @@ -[ - { - "help": "Suche alle Experimente der Experimentserie {text}", - "query": "FIND Experiment with ExperimentSeries = \"$1\"" - }, { - "help": "Tabelle mit Experimenten in Zeitraum {text}", - "query": "SELECT date, species, vivoness FROM Experiment with date in $1" - }, { - "help": "Show the box with number {text}", - "query": "FIND Record Box with Number = \"$1\"" - }, { - "help": "Generate table with boxes that have a content with {text}", - "query": "SELECT Number, Content from Box with Content like \"*$1*\"" - }, { - "help": "Find all boxes with name that contains {text}", - "query": "FIND Record Box with Number like \"*$1*\"" - }, { - "help": "Generate table with all boxes borrowed by {text} (last name)", - "query": "SELECT Number from Box with Loan with Borrower with LastName= \"$1\"" - }, { - "help": "Generate Table with state of all lent boxes", - "query": "SELECT Borrower, expectedReturn, lent, returned, Box from Loan" - }, { - "help": "Show loan state for box with number {text}", - "query": "SELECT Borrower, expectedReturn, lent, returned from loan with (box with number = \"$1\")" - }, { - "help": "Show location of box in Fischereihafen Storage {text}", - "query": "SELECT Number, Site, Aisle, Level from Palette which is referenced by box with number= \"$1\"" - }, { - "help": "Find all fabrics from core {text} (name)", - "query": "find fabric which references section which references bag which references ppstrip which references core with name=\"$1\"" - }, { - "help": "Show open loan requests", - "query": "select Box, Borrower, expectedReturn, comment, destination, exhaustContents from Record Loan with (loanRequested and not loanAccepted)" - } -] diff --git a/makefile b/makefile index 513c7bce..3deb13e6 100644 --- a/makefile +++ b/makefile @@ -27,7 +27,8 @@ SQ=\' ROOT_DIR = $(abspath .) MISC_DIR = $(abspath misc) PUBLIC_DIR = $(abspath public) -CONF_DIR = $(abspath conf) +CONF_CORE_DIR = $(abspath conf/core) +CONF_EXT_DIR = $(abspath conf/ext) SRC_CORE_DIR = $(abspath src/core) SRC_EXT_DIR = $(abspath src/ext) LIBS_DIR = $(abspath libs) @@ -44,9 +45,14 @@ ALL: test install install: clean cp-src cp-ext cp-conf $(addprefix $(PUBLIC_DIR)/, $(LIBS)) -test: clean cp-src cp-ext cp-conf $(addprefix $(PUBLIC_DIR)/, $(TEST_LIBS)) - for f in $(wildcard $(SRC_EXT_DIR)/js/*) ; do \ +test: clean cp-src cp-ext cp-ext-test cp-conf $(addprefix $(PUBLIC_DIR)/, $(TEST_LIBS)) + @for f in $(shell find $(TEST_EXT_DIR) -type f -iname *.js) ; do \ + sed -i "/EXTENSIONS/a \<script src=\"$${f#$(TEST_EXT_DIR)/}\" ></script>" $(PUBLIC_DIR)/index.html ; \ + echo include $$f; \ + done + @for f in $(wildcard $(SRC_EXT_DIR)/js/*.js) ; do \ sed -i "/EXTENSIONS/a \<script src=\"$${f#$(SRC_EXT_DIR)/}\" ></script>" $(PUBLIC_DIR)/index.html ; \ + echo include $$f; \ done ln -s $(PUBLIC_DIR) $(PUBLIC_DIR)/webinterface @@ -97,8 +103,22 @@ cp-ext: cp -i -r $$(realpath $$f) $(PUBLIC_DIR)/xsl/ ; \ done +cp-ext-test: + for f in $(wildcard $(TEST_EXT_DIR)/js/*) ; do \ + cp -i -r $$f $(PUBLIC_DIR)/js/ ; \ + sed -i "/EXTENSIONS/a \<xsl:element name=\"script\"><xsl:attribute name=\"src\"><xsl:value-of select=\"concat\(\$$basepath, 'webinterface$${f#$(SRC_EXT_DIR)}'\)\" /></xsl:attribute></xsl:element>" $(PUBLIC_DIR)/xsl/main.xsl ; \ + done + mkdir -p $(PUBLIC_DIR)/html + for f in $(wildcard $(TEST_EXT_DIR)/html/*) ; do \ + cp -i -r $$(realpath $$f) $(PUBLIC_DIR)/html/ ; \ + done + for f in $(wildcard $(TEST_EXT_DIR)/xsl/*) ; do \ + cp -i -r $$(realpath $$f) $(PUBLIC_DIR)/xsl/ ; \ + done + cp-conf: - cp -r $(CONF_DIR) $(PUBLIC_DIR) + cp -r $(CONF_CORE_DIR) $(PUBLIC_DIR) + cp -r $(CONF_EXT_DIR) $(PUBLIC_DIR) cp-src: cp -r $(SRC_CORE_DIR) $(PUBLIC_DIR) @@ -109,6 +129,9 @@ $(PUBLIC_DIR)/%: $(LIBS_DIR)/% $(PUBLIC_DIR)/%: $(TEST_CORE_DIR)/% ln -s $< $@ +$(PUBLIC_DIR)/%: $(TEST_EXT_DIR)/% + ln -s $< $@ + $(LIBS_DIR)/fonts: unzip ln -s $(LIBS_DIR)/bootstrap-3.3.7-dist/fonts $@ diff --git a/src/core/css/webcaosdb.css b/src/core/css/webcaosdb.css new file mode 100644 index 00000000..2ada51a5 --- /dev/null +++ b/src/core/css/webcaosdb.css @@ -0,0 +1,471 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ +@CHARSET "UTF-8"; +.caosdb-v-show-only-child { + display: none; +} + +.caosdb-v-show-only-child:only-child { + display: initial; +} + +.caosdb-property-text-value { + white-space: pre-line; +} +.caosdb-comment-annotation-text h1 { + font-size: 24px; +} + +.caosdb-comment-annotation-text h2 { + font-size: 20px; +} + +.caosdb-comment-annotation-text h3 { + font-size: 18px; +} + +.caosdb-comment-annotation-text h4 { + font-size: 16px; +} + +.caosdb-show-preview-button, .caosdb-hide-preview-button { + position: absolute; + top: 5px; + left: -5px; +} + +.caosdb-entity-heading-attr { + overflow-x: auto; +} + +a.label.caosdb-id-button:hover { + background-color: #5E6762; +} + +h5 { + margin: auto; +} + +.caosdb-properties-heading { + display: none; +} + +.caosdb-entity-preview .caosdb-entity-panel-body { + overflow-y: auto; + max-height: 250px; +} + +.caosdb-preview-carousel-nav > .caosdb-value-list > .btn-group > .btn:first-child { + margin-left: 34px; +} +.caosdb-preview-carousel-nav > .caosdb-value-list > .btn-group > .btn:last-child { + margin-right: 34px; +} + +.caosdb-preview-carousel-nav { + position: relative; +} +.caosdb-query-panel { + padding-top: 20px; + padding-bottom: 20px; +} + +.navbar-default .btn-link:hover { + text-decoration: none; +} + +.navbar-default .btn-link:focus { + text-decoration: none; +} + +.caosdb-property-name { + font-weight: bold; + margin-left: 8px; +} + +.caosdb-square { + position: relative; + overflow: hidden; +} + +.caosdb-entity-actions-panel { + margin-top: 5px; +} + +#subnav { + height: 60px; +} + +.caosdb-square::before { + content: ""; + display: block; + padding-top: 100%; +} + +.caosdb-square-content { + position: absolute; + top: 0px; + right: 0px; + bottom: 0px; + left: 0px; +} + +.caosdb-parent-name { + font-weight: bold; + margin-right: 12px; + font-size: 14px; +} + +/* lists of values */ +/* INLINE (with default scroll-bar) */ +.caosdb-value-list { + overflow-x: auto; +} + +.caosdb-value-list > .btn-group > .btn { + float: none; +} +.caosdb-value-list > .btn-group, +.caosdb-value-list > ol { + display: inline-block; + float: none; + white-space: nowrap; + margin-bottom: 0px; + margin-left: 0px; +} + +.caosdb-value-list > .list-inline > li { + border-left-width: 0px; +} + +.caosdb-value-list > .list-inline > li:first-child { + border-radius: 4px 0px 0px 4px; + border-left-width: 1px; +} + +.caosdb-value-list > .list-inline > li:last-child { + border-radius: 0px 4px 4px 0px; +} + +/* single boolean values */ +.caosdb-boolean-true { + font-weight: bold; + font-size: 90%; + border: 1px solid #bbb; + padding: 2px 8px; + border-radius: 8px; +} + +.caosdb-boolean-false { + font-weight: bold; + font-size: 90%; + border: 1px solid #bbb; + padding: 2px 8px; + border-radius: 8px; +} + +.caosdb-entity-panel-heading { + border-bottom-left-radius: 3px; + border-bottom-right-radius: 3px; +} + +.caosdb-entity-panel-body { + padding: 0px 10px; +} + +.caosdb-entity-panel-body > :first-child { + margin-top: 15px; +} + +.caosdb-entity-heading-attr-name { + color: #6c6c6c; + font-size: 90%; + margin-right: 0.3em; +} + +.caosdb-subproperty-divider { + margin: 4px 0px; + border-top: 1px solid #dddddd +} + +.caosdb-prop-label { + background-color: #f5f5f5; + font-weight: bold; + padding: 8px 15px; +} + +.caosdb-prop-value { + background-color: #ffffff; + padding: 8px 15px; +} + +.caosdb-prop-list-group .row { + margin-left: 0px; + margin-right: 0px; +} + +.caosdb-v-edit-drag { + padding: 5px; +} + +.caosdb-v-edit-list { + padding-left: 0px; +} +.caosdb-v-edit-panel { + padding: 0px; +} + +.caosdb-prop-list-group>.list-group-item { + padding: 0px; +} + +.caosdb-prop-list-group>.list-group-item:first-child .caosdb-prop-label + { + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} + +.caosdb-prop-list-group>.list-group-item:first-child .caosdb-prop-value + { + border-top-right-radius: 4px; +} + +.caosdb-prop-list-group>.list-group-item:last-child .caosdb-prop-label { + border-bottom-left-radius: 4px; +} + +.caosdb-prop-list-group>.list-group-item:last-child .caosdb-prop-value { + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; +} + +.caosdb-label-record { + background-color: #F92108; + margin-right: 8px; + margin-top: -4px; +} + +.caosdb-label-recordtype { + background-color: #00A32E; + margin-right: 8px; +} + +.caosdb-label-property { + background-color: #496DAB; + margin-right: 8px; +} + +.caosdb-label-file { + background-color: #C92E86; + margin-right: 8px; +} + +.label.caosdb-id-button { + background-color: #4E5752; +} + +.caosdb-id { + margin:5px; + background-color:orange; + display:none; +} + + +.caosdb-properties-heading { + padding-top: 2px; + padding-bottom: 2px; + background-color: #f5f5f5; + color: #7c7c7c +} + +.caosdb-parents-heading { + padding-top: 2px; + padding-bottom: 2px; + background-color: #f5f5f5; + color: #7c7c7c +} + +.caosdb-comments-heading { + padding-top: 2px; + padding-bottom: 2px; + background-color: #f5f5f5; + color: #7c7c7c +} + +.caosdb-parent-item { + +} + +.caosdb-unit { + color: #8c8c8c; + font-size: 80%; + margin-left: 0.3em; +} + +.navbar-brand { + display: flex; + align-items: center; +} + +.navbar-brand>img { + padding: 0px 0px; + height: 100% +} + +.caosdb-fs-cwd::before { + content: " > "; +} + +.caosdb-fs-dir>.glyphicon::before { + content: "\e117"; +} + +.caosdb-fs-dir>.glyphicon { + margin-right: 8px; +} + +.caosdb-fs-dir:hover>.glyphicon::before { + content: "\e118"; +} + +.caosdb-fs-file>.glyphicon::before { + content: "\e022"; +} + +.caosdb-fs-file>.glyphicon { + margin-right: 8px; +} + +.caosdb-fs-file:hover>.glyphicon::before { + content: "\e025"; +} + +.caosdb-fs-btn-file { + padding: 0px; + background-color: transparent; + border: 0px; +} + +.caosdb-fs-btn-file:hover .caosdb-label-file { + background-color: #F96EB6; +} + +.caosdb-fs-btn-file:hover .caosdb-label-id { + background-color: #6E8782; +} + +.back-to-top { + cursor: pointer; + position: fixed; + bottom: 10px; + right: 15px; + display: none; + z-index: 1050; +} + +.caosdb-logo { + margin-right: 8px; +} + +.caosdb-comment-action-item { + padding-left: 4px; + padding-right: 4px; + border-left: 1px solid #7c7c7c; +} + +.caosdb-comment-action>.caosdb-comment-action-item:first-child { + border-left: 0px solid #7c7c7c; +} + +.caosdb-pagination { + margin: 5px 15px; +} + +.caosdb-pagination-navbar { + padding-bottom: 5px; + position: fixed; + bottom: 0px; + width: 100%; + border: 0px; + border-top: 1px solid #e7e7e7; + z-index: 1000; + position: fixed; +} + +.caosdb-heading { + color: #5e5e5e; + background-color: #f8f8f8; + border-bottom: 1px solid #e7e7e7; +} + +.caosdb-heading>.container { + padding: 20px 0px; +} + +.spinning { + animation: spin 2s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.caosdb-property-row { + animation: appear 0.5s 1; +} + +@keyframes appear { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +.flipped-horiz-icon { + transform: scaleX(-1); +} + +.spacer { + margin-left: 12px; +} + +input[type="file"] { + display: none; +} + +.caosdb-form-row { + margin: 10px; +} + +.caosdb-v-property-value { + max-height: 120px; + overflow-y: auto; +} + +.caosdb-v-edit-panel { + max-height: 80vh; + overflow-y: auto; +} diff --git a/src/core/css/webcaosdb.less b/src/core/css/webcaosdb.less new file mode 100644 index 00000000..5ecb7fdf --- /dev/null +++ b/src/core/css/webcaosdb.less @@ -0,0 +1,401 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ + +@radius_normal: 4px; +@margin_normal: 8px; +@border1: 1px solid #e7e7e7; + +.caosdb-v-show-only-child { + display: none; +} + +.caosdb-v-show-only-child:only-child { + display: initial; +} + +.caosdb-comment-annotation-text h1 { + font-size: 24px; +} + +.caosdb-comment-annotation-text h2 { + font-size: 20px; +} + +.caosdb-comment-annotation-text h3 { + font-size: 18px; +} + +.caosdb-comment-annotation-text h4 { + font-size: 16px; +} + +.caosdb-show-preview-button, .caosdb-hide-preview-button { + position: absolute; + top: 5px; + left: -5px; +} + +.caosdb-entity-heading-attr { + overflow-x: auto; +} + +a.label.caosdb-id-button:hover { + background-color: #5E6762; +} + +.caosdb-entity-preview .caosdb-entity-panel-body { + overflow-y: auto; + max-height: 250px; +} + +.caosdb-preview-carousel-nav > .caosdb-value-list > .btn-group > .btn:first-child { + margin-left: 34px; +} +.caosdb-preview-carousel-nav > .caosdb-value-list > .btn-group > .btn:last-child { + margin-right: 34px; +} + +.caosdb-preview-carousel-nav { + position: relative; +} +.caosdb-query-panel { + padding-top: 20px; + padding-bottom: 20px; +} + +.navbar-default .btn-link:hover { + text-decoration: none; +} + +.navbar-default .btn-link:focus { + text-decoration: none; +} + +.caosdb-property-name { + font-weight: bold; + margin-left: @margin-normal; +} + +.caosdb-square { + position: relative; + overflow: hidden; +} + +.caosdb-square::before { + content: ""; + display: block; + padding-top: 100%; +} + +.caosdb-square-content { + position: absolute; + top: 0px; + right: 0px; + bottom: 0px; + left: 0px; +} + +.caosdb-parent-name { + font-weight: bold; + margin-right: 4px; +} + +/* lists of values */ +/* INLINE (with default scroll-bar) */ +.caosdb-value-list { + overflow-x: auto; +} + +.caosdb-value-list > .btn-group > .btn { + float: none; +} +.caosdb-value-list > .btn-group, +.caosdb-value-list > ol { + display: inline-block; + float: none; + white-space: nowrap; + margin-bottom: 0px; + margin-left: 0px; +} + +.caosdb-value-list > .list-inline > li { + border-left-width: 0px; +} + +.caosdb-value-list > .list-inline > li:first-child { + border-radius: @radius_normal 0px 0px @radius_normal; + border-left-width: 1px; +} + +.caosdb-value-list > .list-inline > li:last-child { + border-radius: 0px @radius_normal @radius_normal 0px; +} + +/* single boolean values */ +.caosdb-boolean-true { + font-weight: bold; + font-size: 90%; + border: 1px solid #bbb; + padding: 2px 8px; + border-radius: 8px; +} + +.caosdb-boolean-false { + .caosdb-boolean-true(); +} + +.caosdb-entity-panel-heading { + border-bottom-left-radius: 3px; + border-bottom-right-radius: 3px; +} + +.caosdb-entity-panel-body { + padding: 0px 15px; +} + +.caosdb-entity-panel-body > :first-child { + margin-top: 15px; +} + +.caosdb-entity-heading-attr-name { + color: #6c6c6c; + font-size: 90%; + margin-right: 0.3em; +} + +.caosdb-subproperty-divider { + margin: 4px 0px; + border-top: 1px solid #dddddd +} + +.caosdb-prop-label { + background-color: #f5f5f5; + font-weight: bold; + padding: 8px 15px; +} + +.caosdb-prop-value { + background-color: #ffffff; + padding: 8px 15px; +} + +.caosdb-prop-list-group .row { + margin-left: 0px; + margin-right: 0px; +} + +.caosdb-prop-list-group>.list-group-item { + padding: 0px +} + +.caosdb-prop-list-group>.list-group-item:first-child .caosdb-prop-label + { + border-top-left-radius: @radius_normal; + border-top-right-radius: @radius_normal; +} + +.caosdb-prop-list-group>.list-group-item:first-child .caosdb-prop-value + { + border-top-right-radius: @radius_normal; +} + +.caosdb-prop-list-group>.list-group-item:last-child .caosdb-prop-label { + border-bottom-left-radius: @radius_normal; +} + +.caosdb-prop-list-group>.list-group-item:last-child .caosdb-prop-value { + border-bottom-left-radius: @radius_normal; + border-bottom-right-radius: @radius_normal; +} + +.caosdb-label-record { + background-color: #F92108; + margin-right: @margin-normal; +} + +.caosdb-label-recordtype { + background-color: #00A32E; + margin-right: @margin-normal; +} + +.caosdb-label-property { + background-color: #496DAB; + margin-right: @margin-normal; +} + +.caosdb-label-file { + background-color: #C92E86; + margin-right: @margin-normal; +} + +.label.caosdb-id-button { + background-color: #4E5752; +} + + +.caosdb-properties-heading { + padding-top: 2px; + padding-bottom: 2px; + background-color: #f5f5f5; + color: #7c7c7c +} + +.caosdb-parents-heading { + .caosdb-properties-heading(); +} + +.caosdb-comments-heading { + .caosdb-properties-heading(); +} + +.caosdb-parent-item { + padding-left: 40px; + text-indent: -40px +} + +.caosdb-unit { + color: #8c8c8c; + font-size: 80%; + margin-left: 0.3em; +} + +.navbar-brand { + display: flex; + align-items: center; +} + +.navbar-brand>img { + padding: 0px 0px; + height: 100% +} + +.caosdb-fs-cwd::before { + content: " > "; +} + +.caosdb-fs-dir>.glyphicon::before { + content: "\e117"; +} + +.caosdb-fs-dir>.glyphicon { + margin-right: @margin-normal; +} + +.caosdb-fs-dir:hover>.glyphicon::before { + content: "\e118"; +} + +.caosdb-fs-file>.glyphicon::before { + content: "\e022"; +} + +.caosdb-fs-file>.glyphicon { + margin-right: @margin-normal; +} + +.caosdb-fs-file:hover>.glyphicon::before { + content: "\e025"; +} + +.caosdb-fs-btn-file { + padding: 0px; + background-color: transparent; + border: 0px; +} + +.caosdb-fs-btn-file:hover .caosdb-label-file { + background-color: #F96EB6; +} + +.caosdb-fs-btn-file:hover .caosdb-label-id { + background-color: #6E8782; +} + +.back-to-top { + cursor: pointer; + position: fixed; + bottom: 10px; + right: 15px; + display: none; + z-index: 1050; +} + +.caosdb-logo { + margin-right: @margin-normal; +} + +.caosdb-comment-action-item { + padding-left: 4px; + padding-right: 4px; + border-left: 1px solid #7c7c7c; +} + +.caosdb-comment-action>.caosdb-comment-action-item:first-child { + border-left: 0px solid #7c7c7c; +} + +.caosdb-pagination { + margin: 5px 15px; +} + +.caosdb-pagination-navbar { + padding-bottom: 5px; + position: fixed; + bottom: 0px; + width: 100%; + border: 0px; + border-top: @border1; + z-index: 1000; + position: fixed; +} + +.caosdb-heading { + color: #5e5e5e; + background-color: #f8f8f8; + border-bottom: @border1; +} + +.caosdb-heading>.container { + padding: 20px 0px; +} + +.spinning { + animation: spin 2s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.flipped-horiz-icon { + transform: scaleX(-1); +} + +.spacer { + margin-left: 12px; +} + +input[type="file"] { + display: none; +} diff --git a/src/core/draft/welcome.html b/src/core/draft/welcome.html new file mode 100644 index 00000000..2f91a292 --- /dev/null +++ b/src/core/draft/welcome.html @@ -0,0 +1,87 @@ +<!DOCTYPE html> +<!-- + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header +--> +<html lang="en"> +<head> +<title>CaosDB</title> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<link rel="stylesheet" + href="https://baal:8433/mpidsserver/webinterface/bootstrap-3.3.7-dist/css/bootstrap.min.css"> +<link rel="stylesheet" + href="https://baal:8433/mpidsserver/webinterface/css/webcaosdb.css"> +<script src="https://baal:8433/mpidsserver/webinterface/jquery.min.js"></script> +<script + src="https://baal:8433/mpidsserver/webinterface/bootstrap-3.3.7-dist/js/bootstrap.min.js"></script> + +</head> +<body> + <div class="container"> + <div class="row"> + <div class="col-md-3 col-sm-6"> + <div class="caosdb-square"> + <div + style="border: 1px solid black; background-color: red; padding: 10px;" + class="caosdb-square-content">content</div> + </div> + </div> + <div class="col-md-3 col-sm-6"> + <div class="caosdb-square"> + <div + style="border: 1px solid black; background-color: red; padding: 10px;" + class="caosdb-square-content"> + <h2> + <a>Entities</a><br /> <a>RecordTypes</a><br /> <a>Properties</a><br /> + <a>Records</a><br /> <a>Files</a> + </h2> + </div> + </div> + </div> + <div class="col-md-3 col-sm-6"> + <div class="caosdb-square"> + <div + style="border: 1px solid black; background-color: red; padding: 10px;" + class="caosdb-square-content text-center"> + <h2> + <span class="glyphicon glyphicon-search"></span><br />search + </h2> + </div> + </div> + </div> + <div class="col-md-3 col-sm-6"> + <div class="caosdb-square"> + <div + style="border: 1px solid black; background-color: red; padding: 10px;" + class="caosdb-square-content"> + <h2> + <span class="glyphicon glyphicon-search"></span><br />search + </h2> + </div> + </div> + </div> + </div> + </div> + + +</body> +</html> \ No newline at end of file diff --git a/src/core/js/annotation.js b/src/core/js/annotation.js new file mode 100644 index 00000000..fce3ed77 --- /dev/null +++ b/src/core/js/annotation.js @@ -0,0 +1,390 @@ +/** + * annotation module contains all code for retrieving and posting annotations on + * entities. + */ +this.annotation = new function() { + /** + * Create a form for a new CommentAnnotation. + * + * @param entityId, + * integer, the entity which is to be annotated. + * @return HTML form element. + */ + this.createNewCommentForm = function(entityId) { + var form = $('<form class="caosdb-new-comment-form">' + + '<input type="hidden" name="annotationOf" value="' + entityId + '"> ' + + '<div class="form-group">' + + '<label for="comment">Your new comment:</label>' + + '<textarea class="form-control" rows="5" name="newComment" title="Your comment with 5 or more characters." pattern=".{5,}"></textarea>' + + '</div>' + + '<button class="btn btn-default" title="Submit this comment." type="submit" name="submit" value="Submit">Submit</button>' + + '<button class="btn btn-default" title="Cancel this comment." type="reset" name="cancel" value="Cancel">Cancel</button>' + + '</form>'); + return form[0]; + } + + /** + * Returns the NewCommentButton for a given AnnotationSection. + * + * @param annotationSection, + * HTML element + * @return HTML button element + */ + this.getNewCommentButton = function(annotationSection) { + return $(annotationSection).find("button.caosdb-new-comment-button")[0] + } + + /** + * Returns the NewCommentForm for a given AnnotationSection. + * + * @param annotationSection, + * HTML element + * @return HTML form element + */ + this.getNewCommentForm = function(annotationSection) { + return $(annotationSection).find("form.caosdb-new-comment-form")[0]; + } + + /** + * Returns the entityId for a given AnnotationSection. I.e. the ID of the + * entity which the annotationSection is attached to. + * + * @param annotationSection, + * HTML element + * @return String + */ + this.getEntityId = function(annotationSection) { + return $(annotationSection).attr("data-entity-id"); + } + + /** + * Returns cancel button for a new comment. That is the reset button of the + * NewCommentForm for a given AnnotationSection. + * + * @param annotationSection, + * HTML element + * @return HTML button element + */ + this.getCancelNewCommentButton = function(annotationSection) { + return $(annotationSection).find("form.caosdb-new-comment-form button[type='reset']")[0]; + } + + /** + * Returns submit button of the NewCommentForm for a given + * AnnotationSection. + * + * @param annotationSection, + * HTML element + * @return HTML button element + */ + this.getSubmitNewCommentButton = function(annotationSection) { + return $(annotationSection).find("form.caosdb-new-comment-form button[type='submit']")[0]; + } + + /** + * Returns the PleaseWaitNotification for a given AnnotationSection. + * + * @param annotationSection, + * HTML element + * @return HTML element + */ + this.getPleaseWaitNotification = function(annotationSection) { + return $(annotationSection).find('.caosdb-please-wait-notification')[0]; + } + + this.createError = function(error) { + var ret = $('<div class="alert alert-danger caosdb-new-comment-error alert-dismissable">' + + '<button class="close" data-dismiss="alert" aria-label="close">×</button>' + + '<strong>' + error.name + '!</strong> ' + error.message + '<p class="small"><pre><code>' + + (error.stack ? error.stack : "") + '</code></pre></p></div>')[0]; + return ret; + } + + /** + * Initialize the process of writing a new comment, posting it to the server + * and retrieving the answer of the server. This process is tied to a + * particular 'annotationSection' which in turn belongs to a particular + * entity which is currently displayed. Every step of this process is + * reflected in the annotationSection (show a form with a textarea and + * submit/cancel buttons, show a waiting note, show any results or errors if + * they occur). + * + * Returns a StateMachine which monitors/controls the process and all events + * (button clicks, termination of asynchronous functions). + * + * @param annotationSection, + * HTML element + * @return StateMachine object + */ + this.initNewCommentApp = function(annotationSection) { + if (annotationSection == null) { + throw new Error("annotationSection was null"); + } + var minimalLengthOfComments = 1; // change it here if desired! + var formWrapper = $('<li class="list-group-item"></li>')[0]; + var newCommentButton = annotation.getNewCommentButton(annotationSection); + + // handles all the logic via the app.on<...> functions!!! + var app = new StateMachine({ + transitions: [{ + name: "init", + from: 'none', + to: "read" + }, { + name: "openForm", + from: 'read', + to: 'write' + }, { + name: "submitForm", + from: 'write', + to: 'send' + }, { + name: "cancelForm", + from: 'write', + to: 'read' + }, { + name: "receiveResponse", + from: 'send', + to: 'read' + }, { + name: "resetApp", + from: '*', + to: 'read' + }, ], + }); + app.errorHandler = function(fn) { + try { + fn(); + } catch (e) { + annotationSection.insertBefore(annotation.createError(e), annotationSection.children[2]); + setTimeout(() => app.resetApp(), 1000); + } + } + app.onEnterRead = function(e) { + app.errorHandler(function(e) { + formWrapper.innerHTML = ""; + if (formWrapper.parentNode === annotationSection) { + annotationSection.removeChild(formWrapper); + } + // (re-)enable newCommentButton + $(newCommentButton).toggleClass("disabled", false); + newCommentButton.onclick = function(event) { + app.openForm(); + }; + }); + }; + app.onLeaveRead = function(e) { + app.errorHandler(function(e) { + // disable newCommentButton + $(newCommentButton).toggleClass("disabled", true); + newCommentButton.onclick = null; + }); + }; + app.onEnterWrite = function(e) { + app.errorHandler(function(e) { + // add the newCommentForm + var entityId = annotation.getEntityId(annotationSection); + var form = annotation.createNewCommentForm(entityId); + form.onsubmit = function(e) { + app.submitForm(form); + // return false to interrupt the standard form submission (which + // is synchronous) + return false; + }; + form.onreset = function(e) { + app.cancelForm(form); + }; + formWrapper.appendChild(form); + annotationSection.appendChild(formWrapper); + }); + } + app.onBeforeSubmitForm = function(e, form) { + // validate form + return annotation.validateNewCommentForm(form, minimalLengthOfComments); + } + app.onEnterSend = function(e, form) { + var pleaseWait = annotation.createPleaseWaitNotification(); + // remove the newCommentForm + formWrapper.removeChild(formWrapper.children[0]); + // add waiting notification + formWrapper.appendChild(pleaseWait); + + // convert and send form + var xml = annotation.convertNewCommentForm(form); + var xslPromise = annotation.loadAnnotationXsl(window.sessionStorage.caosdbBasePath); + var responsePromise = annotation.postCommentXml(xml); + var commentPromise = annotation.convertNewCommentResponse(responsePromise, xslPromise); + commentPromise.then(function(resolve) { + app.receiveResponse(resolve); + }); + } + app.onBeforeReceiveResponse = function(e, response) { + // remove waiting notification and append response + annotationSection.removeChild(formWrapper); + annotationSection.appendChild(response[0]); + } + + + // start with read state + app.init(); + return app; + } + + /** + * Create a notification to ask the user to wait while the post request is + * pending. + * + * @return HTML element; + */ + this.createPleaseWaitNotification = function() { + return $('<div class="caosdb-please-wait-notification">Please wait while your comment is being submitted.</div>')[0]; + } + + /** + * Check if the NewCommentForm has non-empty fields 'annotationOf' and + * 'newComment'. An optional parameter can specify the minimal length in + * characters for the newComment field. Default is 1. + * + * @param form, + * a HTML form element. + * @param commentLength, + * integer > 0 (default 1) + * @return boolean + */ + this.validateNewCommentForm = function(form, min = 1) { + var fieldsThere = form.annotationOf !== null && form.newComment !== null; + var fieldsNotEmpty = form.annotationOf.value.length > 0 && form.newComment.value.length >= min; + return fieldsThere && fieldsNotEmpty; + } + + /** + * Convert a NewCommentForm into an XML document for posting. + * + * @param form, + * HTML form element + * @return XML document + */ + this.convertNewCommentForm = function(form) { + var xml_str = "<Insert>"; + xml_str += "<Record>"; + xml_str += '<Parent name="CommentAnnotation"/>'; + xml_str += '<Property name="comment">' + xml_str += form.elements["newComment"].value; + xml_str += '</Property>'; + xml_str += '<Property name="annotationOf">'; + xml_str += form.elements["annotationOf"].value; + xml_str += '</Property>'; + xml_str += "</Record>"; + xml_str += "</Insert>"; + return str2xml(xml_str); + }; + + /** + * Shortcut for postXml. The renaming is also good to be able to replace the + * function during unit tests with a dummy postXml function + */ + this.postCommentXml = (xml) => postXml(xml, window.sessionStorage.caosdbBasePath, "?H"); + + /** + * Convert a the response of a POST request for a new CommentAnnotation to + * HTML via XSLT. + * + * @param response, + * an xml document + * @param xslPromise, + * an xsl document or a promise for one. + * @return an HTML element. + */ + this.convertNewCommentResponse = async function(response, xslPromise) { + var ret = (await asyncXslt(response, xslPromise)).firstChild.children; + $(ret).find("p.small>pre>code").each(function() { + var text = $(this).html(); + $(this).html(''); + $(this).text(text); + }); + return markdown.toHtml(ret); + } + + /** + * Initialize the annotation sections for each entity. To be called once + * after the document is ready. + */ + this.init = function() { + // call initSingleAnnotationSection for each annotationSection in the + // document + $('.caosdb-annotation-section').each( + function(index, element) { + annotation.initAnnotationSection(element); + }); + } + + /** + * Retrieves all annotations for an entity from the database via the + * 'requestDatabase' function and transform the response into an array of + * DOMElement via the xslPromise's result. + * + * @param entityId, + * an id of an entity. + * @param requestDatabase, + * a function with one parameter + * @param xslPromise, + * a Promise which resolves to an xsl script. + * @return an array of DOMElements. + */ + this.getAnnotationsForEntity = async function(entityId, requestDatabase, xslPromise) { + return markdown.toHtml((await asyncXslt(requestDatabase(entityId), xslPromise)).firstChild.children); + } + + this.queryAnnotation = function(referencedId) { + return $.ajax({ + cache: true, + dataType: 'xml', + url: window.sessionStorage.caosdbBasePath + "Entity/?H&query=FIND+Annotation+WHICH+REFERENCES+" + referencedId + "+WITH+ID=" + referencedId, + }); + } + + this.loadAnnotationXsl = function(basepath) { + return $.ajax({ + cache: true, + dataType: 'xml', + url: basepath + "webinterface/xsl/annotation.xsl", + }); + } + + this.loadComments = async function(annotationSection) { + var entityId = annotation.getEntityId(annotationSection); + var annotations = await annotation.getAnnotationsForEntity(entityId, annotation.queryAnnotation, annotation.loadAnnotationXsl(window.sessionStorage.caosdbBasePath)); + $(annotationSection).append(annotations); + } + + /** + * Initialize the annotation section of the given element. + * + * @param element, + * the element which represents a single entity's annotation + * section + * @return undefined + */ + this.initAnnotationSection = function(element) { + if (element == null) { + throw new Error("element was null"); + } else if (typeof element != "object") { + throw new Error("element was not an object."); + } + + annotation.loadComments(element); + + // TODO: Move this to a separate component. + // This can fail, if caosdb.js is not loaded yet: + // if(!userIsAnonymous()) { + // annotation.initNewCommentApp(element); + // } + // QnD Solution: + if (!Array.map(document.getElementsByClassName("caosdb-user-role"), + el => el.innerText).filter(el => el == "anonymous").length > 0) { + annotation.initNewCommentApp(element); + } + } +}; + +$(document).ready(annotation.init); \ No newline at end of file diff --git a/src/core/js/caosdb.js b/src/core/js/caosdb.js new file mode 100644 index 00000000..dde29b17 --- /dev/null +++ b/src/core/js/caosdb.js @@ -0,0 +1,788 @@ +'use strict'; + +/** + * JavaScript client for CaosDB + * A. Schlemmer, 08/2018 + * T. Fitschen, 02/2019 + * + * Dependency: jquery + * Dependency: webcaosdb + * + * TODO: + * - Check whether everything works when previews are opened. + */ + +/** + * Return true iff the current session has an authenticated user. This is the + * case, iff there is a `user` attribute in the server's response and + * subsequently a DIV.caosdb-user-name somewhere in the dom tree. + * + * @return {boolean} + */ +function isAuthenticated() { + return document.getElementsByClassName("caosdb-user-name").length > 0 && document.getElementsByClassName("caosdb-user-name")[0].innerText.trim().length > 0; +} + + +/** + * Return the name of the user currently logged in. + * + * @return Name of the user. + */ +function getUserName() { + return document.getElementsByClassName("caosdb-user-name")[0].innerText; +} + +/** + * Return the realm of the user currently logged in. + * + * @return Realm of the user. + */ +function getUserRealm() { + return document.getElementsByClassName("caosdb-user-realm")[0].innerText; +} + +/** + * Return the roles of the user currently logged in. + * + * @return Array containing the roles of the user. + */ +function getUserRoles() { + return Array.map(document.getElementsByClassName("caosdb-user-role"), + el => el.innerText); +} + +/** + * Find out, whether the user that is currently logged in is an administrator. + * + * @return true if the role administration is present. + */ +function userIsAdministrator() { + return userHasRole("administration"); +} + +/** + * Return true if the user has the role `anonymous`. + */ +function userIsAnonymous() { + return userHasRole("anonymous"); +} + + +/** + * Return true if the user has the role `role`. + * + * @param role + */ +function userHasRole(role) { + return getUserRoles().filter(el => el == role).length > 0; +} + + +/** + * Return all entities in the current document. + * + * @return A list of document elements. + */ +function getEntities() { + return document.getElementsByClassName("caosdb-entity-panel"); +} + +/** + * Return the role (Record, RecordType, ...) of element. + * @return A String holding the role or undefined if the role could not be found. + */ +function getEntityRole(element) { + if (typeof element.dataset.entityRole !== 'undefined') { + return element.dataset.entityRole; + } + + let res = element.getElementsByClassName("caosdb-f-entity-role"); + if (res.length == 1) { + return res[0].dataset.entityRole; + } + return undefined; +} + +/** + * Return the unit of element. + * If the corresponding data-attribute can not be found undefined is returned. + * @return A string containing the datatype of the element. + */ +function getEntityUnit(element) { + var res = $(element).find("input.caosdb-f-entity-unit"); + if (res.length == 1) { + var x = res.val(); + return (x == '' ? undefined : x); + } + return undefined; +} + +/** + * Return the datatype of element. + * If the corresponding data-attribute can not be found undefined is returned. + * @return A string containing the datatype of the element. + */ +function getEntityDatatype(element) { + var is_list = ($(element).find("input.caosdb-f-entity-is-list:checked").length > 0); + var res = $(element).find("select.caosdb-f-entity-datatype"); + if (res.length == 1) { + var x = res.val(); + if (typeof x !== 'undefined' && x != '' && is_list) { + return "LIST<" + x + ">"; + } + return (x == '' ? undefined : x); + } + + res = $(element).find(".caosdb-entity-panel-heading[data-entity-datatype]"); + if (res.length == 1) { + var x = res.attr("data-entity-datatype"); + return (x == '' ? undefined : x); + } + return undefined; +} + +/** + * Return the name of element. + * If the corresponding label can not be found or the label is ambigious undefined is returned. + * @return A string containing the name of the element. + */ +function getEntityName(element) { + // TODO deprecated class name + if ($(element).find('[data-entity-name]').length == 1) { + return $(element).find('[data-entity-name]')[0].dataset.entityName; + } else if (typeof $(element).find('.caosdb-f-entity-name').val() !== 'undefined') { + return $(element).find('.caosdb-f-entity-name').val(); + } + var res = element.getElementsByClassName("caosdb-label-name"); + if (res.length == 1) { + return res[0].textContent; + } + return undefined; +} + +/** + * Return the path of element. + * If the corresponding label can not be found or the label is ambigious undefined is returned. + * @return A string containing the name of the element. + */ +function getEntityPath(element) { + return getEntityHeadingAttribute(element, "path"); +} + +/** + * Return the id of an entity. + * @param element The element holding the entity. + * @return The id of the entity as a String or undefined if no ID could be found. + * @throws An error if the ID was ambigous. + */ +function getEntityID(element) { + if (typeof element.dataset.entityId !== 'undefined') { + return element.dataset.entityId; + } + // var res = element.getElementsByClassName("caosdb-id"); + var res = findElementByConditions(element, x => x.classList.contains("caosdb-id"), + x => x.classList.contains("caosdb-entity-panel-body")); + if (res.length == 1) + return res[0].textContent; + else if (res.length == 0) + return undefined; + throw "id is ambigous for this element!" +} + +/** + * Take a datetime from caosdb and return a date and a time + * suitable for html inputs. + * + * If the text contains only a date it is returned. + * + * @param text The string from CaosDB. + * @return A new string for the input element. + */ +function caosdb2InputDate(text) { + if (text.includes("T")) { + var spl = text.split("T"); + return [spl[0], spl[1].substring(0, 8)]; + } + return [text]; +} + +/** + * Return the id of an href attribute. + * This is needed for retrieving an ID that is contained in a link to another entity. + * + * Some anker tags have a data-entity-id tag instead which is returned. + * + * @param el The element holding the link. + * @return A String holding the ID or undefined if no href attribute could be found. + */ +function getIDfromHREF(el) { + if (el.hasAttribute("href")) { + let attr = el["href"]; + let idstr = attr.substring(attr.lastIndexOf("/") + 1); + // Currently the XSLT returns wrong links for missing IDs. + if (idstr.length == 0) + return undefined; + return idstr; + } else if (el.hasAttribute("data-entity-id")) { + return el.getAttribute("data-entity-id"); + } + return undefined; +} + +/** + * Return an entity heading attribute from an element. + * @param element The element holding the entity. + * @return The value of the entity heading attribute or undefined if it is not present. + */ +function getEntityHeadingAttribute(element, attribute_name) { + var res = element.getElementsByClassName("caosdb-entity-heading-attr"); + + for (var i = 0; i < res.length; i++) { + var subres = res[i].getElementsByClassName("caosdb-entity-heading-attr-name"); + if (subres.length != 1) { + throw "Entity heading attribute does not have a name."; + } + if (subres[0].textContent == attribute_name + ":") { + return res[i].childNodes[1].textContent; + } + } + return undefined; +} + +/** + * Return the entity attribute description from an element. + * @param element The element holding the entity. + * @return The value of the description or undefined if it is not present. + */ +function getEntityDescription(element) { + if ($(element).find('[data-entity-description]').length == 1) { + return $(element).find('[data-entity-description]')[0].dataset.entityDescription; + } else if (typeof $(element).find('.caosdb-f-entity-description').val() !== 'undefined') { + return $(element).find('.caosdb-f-entity-description').val(); + } + + return getEntityHeadingAttribute(element, "description"); +} + +/** + * Return the parents of an entity. + * @param element The element holding the entity. + * @return A list of objects with name and id of the parents. + */ +function getParents(element) { + var res = element.getElementsByClassName("caosdb-parent-name"); + var list = []; + for (var i = 0; i < res.length; i++) { + list.push({ + id: getIDfromHREF(res[i]), + name: res[i].textContent + }); + } + return list; +} + +/** + * Find all elements that fulfill a condition. + * Don't traverse elements if except condition is matched. + * @param element The start node. + * @param condition The condition. + * @param except The stop condition. + */ +function findElementByConditions(element, condition, except) { + let found = [] + let echild = element.children; + + for (var i = 0; i < echild.length; i++) { + if (condition(echild[i])) { + found.push(echild[i]); + } + + if (!except(echild[i])) { + found.push.apply(found, findElementByConditions(echild[i], condition, except)); + } + } + return found; +} + +/** + * Return a list of property objects from the dom property element. + * @param propertyelement: A HTMLElement identifying the property element. + * @param names: a map of names tracking the count of usage for each name (optional) + * + * TODO: Retrieval when using list element preview is currently broken. + **/ +function getPropertyFromElement(propertyelement, names = undefined) { + + let property = new Object({}); + let namel = propertyelement.getElementsByClassName("caosdb-property-name")[0]; + let valel = propertyelement.getElementsByClassName("caosdb-property-value")[0]; + let dtel = propertyelement.getElementsByClassName("caosdb-property-datatype")[0]; + let idel = propertyelement.getElementsByClassName("caosdb-property-id")[0]; + let unitel = valel.getElementsByClassName("caosdb-unit")[0]; + + if (typeof unitel == "undefined") { + property.unit = undefined; + } else { + property.unit = unitel.textContent.trim(); + } + if (namel === undefined) { + property.name = undefined; + } else { + property.name = namel.textContent; + } + if (idel === undefined) { + property.id = undefined; + } else { + property.id = idel.textContent; + } + if (dtel === undefined) { + property.datatype = undefined; + property.list = undefined; + property.reference = undefined; + property.value = ""; + } else { + property.datatype = dtel.textContent; + + var base_datatypes = ["TEXT", "INTEGER", "DOUBLE", "DATETIME", "BOOLEAN"]; + if (property.datatype.substring(0, 4) == "LIST") { + property.list = true; + property.value = []; + var list_datatype = property.datatype.substring(5, property.datatype.length - 1); + property.reference = base_datatypes.filter(el => el == list_datatype).length == 0; + } else { + property.list = false; + property.value = ""; + property.reference = base_datatypes.filter(el => el == property.datatype).length == 0; + } + + } + + if (!property.list && valel.getElementsByClassName("caosdb-property-text-value").length == 1) { + valel = valel.getElementsByClassName("caosdb-property-text-value")[0]; + } + + // Needed for multiple properties with the same name: + // It is not set when names is undefined. + if (!(names === undefined)) { + if (names.has(property.name)) { + names.set(property.name, names.get(property.name) + 1); + } else { + names.set(property.name, 0); + } + property.duplicateIndex = names.get(property.name); + } + + if (valel !== undefined && valel.textContent.length > 0) { + // This is set to true, when there is a reference or a list of references: + property.reference = (valel.getElementsByClassName("caosdb-id").length > 0); + if (property.list) { + // list datatypes + let listel; + if (property.reference) { + // list of referernces + // TODO: Fix list preivew here. Fixed, but untested. + listel = findElementByConditions(valel, x => x.classList.contains("caosdb-resolvable-reference"), + x => x.classList.contains("caosdb-preview-container")); + for (var j = 0; j < listel.length; j++) { + property.value.push(getIDfromHREF(listel[j])); + } + } else { + // list of anything but references + // TODO: Fix list preivew here. Fixed, but untested. + listel = findElementByConditions(valel, x => x.classList.contains("list-group-item"), + x => x.classList.contains("caosdb-preview-container")); + for (var j = 0; j < listel.length; j++) { + property.value.push(listel[j].textContent); + } + } + } else if (property.reference) { + // reference datatypes + // let el = findElementByConditions(valel, x => x.classList.contains("caosdb-id"), + // x => x.classList.contains("caosdb-preview-container")); + property.value = getIDfromHREF(valel.getElementsByTagName("a")[0]); + } else { + // all other datatypes + property.value = valel.textContent.trim(); + } + } + + + return property; +} + +/** + * Get the properties from an entity. + * @param element The element holding the entity. + * @return a list of dom elements containing the properties. + */ +function getPropertyElements(element) { + return findElementByConditions(element, + x => x.classList.contains("list-group-item") && + x.classList.contains("caosdb-property-row"), + x => x.classList.contains("caosdb-preview-container")); +} + +/** + * Return high level representations of the properties of an entity. + * @param element The element holding the entity. + * @return a list of objects for the properties according to the following specification. + * + * + * Specification of properties: + * prop = { + * name: ... // String, name of the property + * id: ... // String, id of the property + * value: ... // value of the property + * datatype: ... // full datatype, e.g. INTEGER or LIST<Uboot> + * duplicateIndex: ... // Integer starting from 0 and increasing for multiple properties + * reference: ... // Is this holding an ID of a reference? (boolean) + * list: ... // Is this a list? (boolean) + * } + * + * + */ +function getProperties(element) { + var res = getPropertyElements(element); + var list = []; + + var names = new Map(); + for (var i = 0; i < res.length; i++) { + let property = getPropertyFromElement(res[i], names); + list.push(property); + } + return list; +} + +/** + * Sets a property with some basic type checking. + * + * @param valueelement The dom element where the text content is to be set. + * @param property The new property. + * @param propold The old property belonging to valueelement. + * + * TODO: Server string is hardcoded. + */ +function setPropertySafe(valueelement, property, propold) { + const serverstring = connection.getBasePath() + "Entity/"; + if (propold.list) { + if (property.value.length === undefined) { + throw ("New property must be a list."); + } + + // Currently problems, when list is set to [] and afterwards to something different. + + if (propold.reference) { + var finalstring; + if (property.value.length == 0) { + finalstring = ""; + } else { + finalstring = ''; + for (var i = 0; i < property.value.length; i++) { + finalstring += '<a class="btn btn-default btn-sm caosdb-resolvable-reference" href="' + serverstring + property.value[i] + '"><span class="caosdb-id">' + property.value[i] + '</span><span class="caosdb-resolve-reference-target" /></a>'; + } + } + valueelement.getElementsByClassName("caosdb-value-list")[0].getElementsByClassName("caosdb-overflow-content")[0].innerHTML = finalstring; + } else { + throw ("Not Implemented: Lists with no references."); + } + } else if (propold.reference) { + var llist = valueelement.getElementsByTagName("a"); + if (llist.length > 0) { + var ael = llist[0]; + ael.setAttribute("href", serverstring + property.value); + ael.innerHTML = '<span class="caosdb-id">' + property.value + '</span><span class="caosdb-resolve-reference-target" />'; + } else { + finalstring = '<a class="btn btn-default btn-sm caosdb-resolvable-reference" href="' + serverstring + property.value + '"><span class="caosdb-id">' + property.value + '</span><span class="caosdb-resolve-reference-target" /></a>'; + valueelement.innerHTML = finalstring; + preview.init(); + } + } else { + valueelement.innerHTML = "<span class='caosdb-property-text-value'>" + property.value + "</span>"; + } +} + +/** + * Set a property in the dom model. + * @author Alexander Schlemmer + * @param element The document element of the record. + * @param property The new property as an object. + * @return The number of properties that were set. + * + * If multiple properties with the same name exist and the property + * to be set has not duplicateIndex, all properties will be set to that value. + */ +function setProperty(element, property) { + var elementp = element.getElementsByClassName("caosdb-properties")[0]; + var res = elementp.getElementsByClassName("list-group-item"); + var dindex = property.duplicateIndex; + var counter = 0; + for (var i = 0; i < res.length; i++) { + if (res[i].classList.contains("caosdb-properties-heading")) continue; + let propold = getPropertyFromElement(res[i]); + if (propold.name === property.name) { + if (dindex > 0) { + dindex--; + continue; + } + setPropertySafe(res[i].getElementsByClassName("caosdb-property-value")[0], + property, + propold); + counter++; + } + } + return counter; +} + +/** + * Get a property by name. + * @param element The element holding the entity. + * @param property_name The name of the property. + * @return The value of the the property with property_name. + * This function returns undefined when this property is not available for this entity. + */ +function getProperty(element, property_name) { + var props = getProperties(element).filter(el => el.name == property_name); + if (props.length == 0) { + return undefined; + } + return props[0].value; +} + +/** + * Helper function for setting an id and or a name if contained in parent. + * @param parentElement The element which is to recieve the attributes. + * @param parent The object possibly containing an id and or a name. + */ +function setNameID(parentElement, parent) { + if (typeof parent.id !== 'undefined' && parent.id !== '') { + parentElement.setAttribute("id", parent.id); + } + if (typeof parent.name !== 'undefined' && parent.name !== '') { + parentElement.setAttribute("name", parent.name); + } +} + +/** + * Append a parent node to an XML document. + * @see getParents + * @param doc A document for the XML. + * @param element The element to append to. + * @param parent An object containing a name and or an id. + */ +function appendParent(doc, element, parent) { + var parentElement = doc.createElement("Parent"); + setNameID(parentElement, parent); + element.appendChild(parentElement); +} + +/** + * Append a text node with name name and value value to element element. + * @param doc A document for the XML. + * @param element + * @param name + * @param value + */ +function appendValueNode(doc, element, name, value) { + let el = doc.createElement(name); + let valel = doc.createTextNode(value); + el.appendChild(valel); + element.appendChild(el); +} + +/** + * Append a property node to an XML document. + * @see getProperties + * @param doc An document for the XML. + * @param element The element to append to. + * @param property An object specifying a property. + */ +function appendProperty(doc, element, property, append_datatype = false) { + var propertyElement = doc.createElement("Property"); + setNameID(propertyElement, property); + if (append_datatype == true) { + propertyElement.setAttribute("datatype", property.datatype); + } + if (typeof property.unit !== 'undefined') { + propertyElement.setAttribute("unit", property.unit); + } + + if (!(property.value === undefined)) { + if (("list" in property && property.list) || property.value instanceof Array) { + if (property.value instanceof Array) { + for (var i = 0; i < property.value.length; i++) { + appendValueNode(doc, propertyElement, "Value", property.value[i]); + } + } else { + appendValueNode(doc, propertyElement, "Value", property.value); + } + } else { + let valel = doc.createTextNode(property.value); + propertyElement.appendChild(valel); + } + } + + element.appendChild(propertyElement); +} + + +/** + * Create an XML for an entity. + * This function uses the object notation. + * @see getProperties + * @see getParents + * @param role Record, RecordType or Property + * @param name The name of the entity. Can be undefined. + * @param id The id of the entity. Can be undefined. + * @param properties A list of properties. + * @param parents A list of parents. + * @return A document holding the newly created entity. + * + */ +function createEntityXML(role, name, id, properties, parents, + append_datatypes = false, datatype = undefined, description = undefined, unit = undefined) { + var doc = document.implementation.createDocument(null, role, null); + var nelnode = doc.children[0]; + setNameID(nelnode, { + name: name, + id: id + }); + + if (typeof datatype !== 'undefined' && datatype.length > 0) { + $(nelnode).attr("datatype", datatype); + } + + if (typeof description !== 'undefined' && description.length > 0) { + $(nelnode).attr("description", description); + } + + if (typeof unit !== 'undefined' && unit.length > 0) { + $(nelnode).attr("unit", unit); + } + + if (!(parents === undefined)) { + for (var i = 0; i < parents.length; i++) { + appendParent(doc, nelnode, parents[i]); + } + } + + if (!(properties === undefined)) { + for (var i = 0; i < properties.length; i++) { + appendProperty(doc, nelnode, properties[i], append_datatypes); + } + } + return doc; +} + +/** + * Helper function to wrap xml documents into another node which could e.g. be + * Update, Response, Delete. + * @param The name of the newly created top level node. + * @param The xml document. + * @return A new xml document. + */ +function wrapXML(elementname, xml) { + var doc = document.implementation.createDocument(null, elementname, null); + + doc.children[0].appendChild(xml.children[0]); + return doc; +} + +/** + * Convert this xml document into an update. + * @param The xml document. + * @return A new xml document. + */ +function createUpdate(xml) { + return wrapXML("Request", xml); +} + +/** + * Convert this xml document into an insert. + * @param The xml document. + * @return A new xml document. + */ +function createInsert(xml) { + return wrapXML("Request", xml); +} + +/** + * Convert this xml document into a response. + * @param The xml document. + * @return A new xml document. + */ +function createResponse(xml) { + return wrapXML("Response", xml); +} + + +/** + * Retrieve an entity by using an id or a name. + * @param id The id of the entity. Can be undefined when name is used. + * @param name The name of the entity. Can be undefined when id is used. + * @return The element holding that entity. + */ +async function retrieve(id = undefined, name = undefined) { + var retstr = id; + if (id === undefined) { + retstr = name; + } + let entities = await connection.get("Entity/" + retstr); + return transformation.transformEntities(entities); +} + +/** + * Query the database for querytext. + * @param querytext The search query. + * @return An array of entities. + */ +async function query(querytext) { + let entities = await connection.get("Entity/?query=" + querytext); + return transformation.transformEntities(entities); +} + +/** + * Retrive one entity given by ID id + * and transform them into single entity objects. + * @param id The id. + * @return An array of entities. + */ +async function retrieve_dragged_property(id) { + let entities = await connection.get("Entity/" + id); + return transformation.transformProperty(entities); +} + +/** + * Retrieve all properties and record types and convert them into + * a web page using a specialized XSLT named entity palette. + * @return An array of entities. + */ +async function retrieve_data_model() { + // TODO possibly allow single query + let props = await connection.get("Entity/?query=FIND Property"); + let rts = await connection.get("Entity/?query=FIND RecordType"); + console.log("HERE"); + console.log(props); + console.log(rts); + for (var p of props.children[0].children) { + rts.children[0].appendChild(p.cloneNode()); + } + return transformation.transformEntityPalette(rts); +} + + +/** + * Update an entity using its xml representation. + * @param xml The xml of the entity which will be automatically wrapped with an Update. + */ +async function update(xml) { + return transaction.updateEntitiesXml(createUpdate(xml)); +} + +/** + * Insert an entity using its xml representation. + * @param xml The xml of the entity which will be automatically wrapped with an Insert. + */ +async function insert(xml) { + return transaction.insertEntitiesXml(createInsert(xml)); +} \ No newline at end of file diff --git a/src/core/js/edit_mode.js b/src/core/js/edit_mode.js new file mode 100644 index 00000000..d15e5cf8 --- /dev/null +++ b/src/core/js/edit_mode.js @@ -0,0 +1,1051 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ + +/** + * Edit mode module + */ +var edit_mode = new function() { + + this.init = function() { + if (isAuthenticated()) { + this.add_edit_mode_button(); + if (this.is_edit_mode()) { + this.enter_edit_mode(); + this.toggle_edit_panel(); + } + this.scroll_edit_panel(); + window.onscroll = this.scroll_edit_panel; + $('.caosdb-f-edit').css("transition", "top 1s"); + } else { + window.localStorage.removeItem("edit_mode"); + } + } + + this.scroll_edit_panel = function(e) { + $('.caosdb-f-edit').css("top", document.documentElement.scrollTop); + } + + this.prop_dragstart = function(e) { + e.dataTransfer.setData("text/plain", e.target.id); + } + + this.prop_dragleave = function(e) { + $(this).css("background-color", ""); + } + + this.prop_dragover = function(e) { + console.log(this); + $(this).css("background-color", "lightgreen"); + e.preventDefault(); + e.dataTransfer.dropEffect = "copy"; + } + + this.add_new_property = function(entity, new_prop) { + var rt = entity.getElementsByClassName("caosdb-properties")[0]; + rt.appendChild(new_prop); + edit_mode.make_property_editable(new_prop); + } + + this.add_dropped_property = function(e, panel) { + var propsrcid = e.dataTransfer.getData("text/plain"); + var tmp_id = propsrcid.split("-"); + var prop_id = tmp_id[tmp_id.length - 1]; + var entity_type = tmp_id[tmp_id.length - 2]; + if (entity_type == "p") { + retrieve_dragged_property(prop_id).then(new_prop_doc => { + edit_mode.add_new_property(panel, new_prop_doc.firstChild); + }); + } else if (entity_type == "rt") { + var name = $("#" + propsrcid).text(); + var dragged_rt = str2xml('<Response><Property id="' + prop_id + '" name="' + name + '" datatype="' + name + '"></Property></Response>'); + transformation.transformProperty(dragged_rt).then(new_prop_doc => { + edit_mode.add_new_property(panel, new_prop_doc.firstChild); + }); + } + } + + // Dropping a Property or RecordType in the main panel will add it as a + // property. This is done by this function. + this.prop_drop = function(e) { + $(this).css("background-color", ""); + e.preventDefault(); + var panel = this; // the element which owns the listener + edit_mode.add_dropped_property(e, panel); + } + + this.add_dropped_parent = function(e, panel) { + var propsrcid = e.dataTransfer.getData("text/plain"); + var parent_list = panel.getElementsByClassName("caosdb-f-parent-list")[0] + var tmp_id = propsrcid.split("-"); + var prop_id = tmp_id[tmp_id.length - 1]; + var entity_type = tmp_id[tmp_id.length - 2]; + if (entity_type == "p") { + console.log("SHOULD not happend") + } else if (entity_type == "rt") { + var name = $("#" + propsrcid).text(); + var dragged_rt = str2xml('<Response><RecordType id="' + prop_id + '" name="' + name + '"></RecordType></Response>'); + transformation.transformParent(dragged_rt).then(new_prop => { + parent_list.appendChild(new_prop); + /* + edit_mode.add_one_delete_button( + parent_list.children[parent_list.children.length-1], + is_parent=true + ); + */ + edit_mode.add_parent_delete_buttons(panel); + }, globalError); + + } + } + + // Dropping a RecordType in the heading will add it as a parent. + // This is done by this function. + this.parent_drop = function(e) { + e.preventDefault(); + $(this).css("background-color", ""); + var panel = this; // the element which owns the listener + edit_mode.add_dropped_parent(e, panel); + } + + this.set_entity_dropable = function(entity, dragover, dragleave, parent_drop, property_drop) { + var rts = entity.getElementsByClassName("caosdb-entity-panel-body"); + for (var rel of rts) { + rel.addEventListener("dragleave", dragleave); + rel.addEventListener("dragover", dragover); + rel.addEventListener("drop", property_drop); + } + var headings = entity.getElementsByClassName("caosdb-entity-panel-heading"); + for (var rel of headings) { + rel.addEventListener("dragleave", dragleave); + rel.addEventListener("dragover", dragover); + rel.addEventListener("drop", parent_drop); + } + + } + + this.unset_entity_dropable = function(entity, dragover, dragleave, parent_drop, property_drop) { + var rts = entity.getElementsByClassName("caosdb-entity-panel-body"); + for (var rel of rts) { + rel.removeEventListener("dragleave", dragleave); + rel.removeEventListener("dragover", dragover); + rel.removeEventListener("drop", property_drop); + } + var headings = entity.getElementsByClassName("caosdb-entity-panel-heading"); + for (var rel of headings) { + rel.removeEventListener("dragleave", dragleave); + rel.removeEventListener("dragover", dragover); + rel.removeEventListener("drop", parent_drop); + } + } + + this.init_drag_n_drop = function(scope = document) { + edit_mode.set_entity_dropable(scope, this.prop_dragover, this.prop_dragleave, this.parent_drop, this.prop_drop); + } + + this.remove_save_button = function(ent) { + $(ent).find('.caosdb-f-entity-save-button').remove(); + } + + this.add_save_button = function(ent, callback) { + var save_btn = $('<button class="btn btn-link caosdb-update-entity-button caosdb-f-entity-save-button">Save</button>'); + + $(ent).find(".caosdb-entity-actions-panel").append(save_btn); + + $(save_btn).click(function() { + callback(); + // edit_mode.update_entity(this.dataset.editid); + }); + } + + /* TODO + this.add_edit_xml_button = function(entity) { + var button = $('<button class="btn btn-link caosdb-update-entity-button" title="Edit the XML representaion of this entity."><span class="glyphicon glyphicon-pencil"/> Edit XML</button>'); + $(entity).find(".caosdb-entity-actions-panel").append(button); + + var callback = (e) => { + e.stopPropagation(); + transaction.update.initUpdate(entity); + } + + button.click(callback); + } + */ + + this.add_trash_button = function(appendable, deletable, className, callback = undefined) { + var button = $('<button class="btn btn-link ' + className + ' caosdb-f-entity-trash-button"><span class="glyphicon glyphicon-trash"></span></button>'); + $(appendable).append(button); + button.click((e) => { + e.stopPropagation(); + deletable.remove(); + if (typeof callback == 'function') { + callback(); + } + }); + } + + this.add_parent_trash_button = function(appendable, deletable, callback = undefined) { + edit_mode.add_trash_button(appendable, deletable, "caosdb-f-parent-trash-button", callback); + } + + this.add_property_trash_button = function(appendable, deletable) { + edit_mode.add_trash_button(appendable, deletable, "caosdb-f-property-trash-button"); + } + + /* + this.add_one_delete _button = function (entity, is_parent) { + if (is_parent){ + var btn_type = "parent"; + } else { + var btn_type = "property"; + } + var trash_btn = $('<button data-nodeid="'+entity.id + +'" class="btn btn-link caosdb-f-'+btn_type+'-trash-button caosdb-f-entity-trash-button"><span class="glyphicon glyphicon-trash"></span></button>'); + + $(entity).find(".caosdb-property-edit, .caosdb-f-parent-action-panel").append(trash_btn); + + $(trash_btn).click(function () { + $("#"+this.dataset.nodeid).remove(); + if (is_parent){ + // reset the delete buttons + edit_mode.add_parent_delete_buttons(entity); + } + }); + } + + this.add_all_delete_ buttons = function () { + this.add_property _delete_buttons(); + this.add_parent_delete_buttons(); + } + + this.add_name_edit_ buttons = function () { + $(".caosdb-f-name-edit-button").remove(); + var entity_panels = document.getElementsByClassName("caosdb-entity-panel"); + for (panel of entity_panels){ + if ((getEntityRole(panel) != "Record") && (getEntityRole(panel) != "RecordType")){ + continue + } + var properties = panel.getElementsByClassName("caosdb-property-row"); + for (var prop of properties){ + this.add_one_delete_button(prop, is_parent=false); + } + } + } + this.add_property _delete_buttons = function () { + $(".caosdb-f-property-trash-button").remove(); + var entity_panels = document.getElementsByClassName("caosdb-entity-panel"); + for (panel of entity_panels){ + if ((getEntityRole(panel) != "Record") && (getEntityRole(panel) != "RecordType")){ + continue + } + var properties = panel.getElementsByClassName("caosdb-property-row"); + for (var prop of properties){ + this.add_one_delete_button(prop, is_parent=false); + } + } + } + */ + + this.add_parent_delete_buttons = function(header) { + $(header).find(".caosdb-f-parent-trash-button").remove(); + var parents = $(header).find(".caosdb-parent-item"); + if ((parents.length > 1) || getEntityRole(header) != "Record") { + for (var par of parents) { + edit_mode.add_parent_trash_button($(par).find('.caosdb-f-parent-actions-panel')[0], par, () => { + edit_mode.add_parent_delete_buttons(header); + }); + } + } + } + + this.update_entity_by_id = async function(ent_id) { + var ent_element = $("#" + ent_id)[0]; + return this.update_entity(ent_element); + } + + this.insert_entity = async function(ent_element) { + console.log(getProperties(ent_element)); + var xml = createEntityXML( + getEntityRole(ent_element), + getEntityName(ent_element), + undefined, + edit_mode.getProperties(ent_element), + getParents(ent_element), + true, + getEntityDatatype(ent_element), + getEntityDescription(ent_element), + getEntityUnit(ent_element), + ); + console.log(xml); + return await insert(xml); + } + + /** + * TODO merge with caosdb.js->getProperties + */ + this.getProperties = function(ent_element) { + var properties = []; + for (var element of $(ent_element).find('.caosdb-property-row')) { + console.log(element); + + var valfield = $(element).find(".caosdb-property-value"); + var editfield = $(element).find(".caosdb-property-edit-value"); + var property = getPropertyFromElement(element); + + property.unit = editfield.find(".caosdb-unit").val(); + if (property.datatype == "TEXT" || + property.datatype == "DATE" || + property.datatype == "DOUBLE" || + property.datatype == "INTEGER" || + property.datatype == "FILE") { + property.value = editfield.find(":input").val() + } else if (property.datatype == "DATETIME") { + let es = editfield.find(":input"); + if (es.length == 2) { + property.value = input2caosdbDate( + es[0].value, + es[1].value); + } else { + property.value = es[0].value; + } + } else if (property.reference) { + if (!property.list) { + property.value = $(editfield).find("select").first()[0].selectedOptions[0].value; + } else { + throw ("Reference lists not supported."); + } + } else { + throw ("Not supported."); + } + properties.push(property); + } + return properties; + + } + + this.update_entity = async function(ent_element) { + console.log(ent_element); + + var xml = createEntityXML( + getEntityRole(ent_element), + getEntityName(ent_element), + getEntityID(ent_element), + edit_mode.getProperties(ent_element), + getParents(ent_element), + true, + getEntityDatatype(ent_element), + getEntityDescription(ent_element), + getEntityUnit(ent_element), + ); + console.log(xml); + return await update(xml); + } + + this.add_edit_mode_button = function() { + var edit_mode_li = $('<li><button class="navbar-btn btn btn-link caosdb-f-btn-toggle-edit-mode">Edit Mode</button></li>'); + + $("#top-navbar").find("ul").first().append(edit_mode_li); + + $(".caosdb-f-btn-toggle-edit-mode").click(() => { + console.log("toggle edit mode"); + edit_mode.toggle_edit_mode(); + }); + } + + this.toggle_edit_mode = function() { + this.toggle_edit_panel(); + if (this.is_edit_mode()) { + this.leave_edit_mode(); + } else { + this.enter_edit_mode(); + } + } + + /** + * To be overridden by an instance of `leave_edit_mode_template` during the + * `enter_edit_mode()` execution. + */ + this.leave_edit_mode = function() {} + + this.enter_edit_mode = function(editApp = undefined) { + console.log("enter edit mode"); + window.localStorage.edit_mode = "true"; + + var editPanel = this.get_edit_panel(); + removeAllWaitingNotifications(editPanel); + this.add_wait_datamodel_info(); + + retrieve_data_model().then(model => { + removeAllWaitingNotifications(editPanel); + editPanel.innerHTML = xml2str(model); + $(".caosdb-f-btn-toggle-edit-mode").text("Leave Edit Mode"); + if (typeof editApp == "undefined") { + var editApp = this.init_edit_app(); + } + this.leave_edit_mode = function() { + edit_mode.leave_edit_mode_template(editApp); + }; + }, this.handle_error); + } + + this.init_freezing = function() { + $(".caosdb-entity-panel").each(function(index) {}); + } + + this.make_header_editable = function(entity) { + var header = $(entity).find('.caosdb-entity-panel-heading'); + var roleElem = $(header).find('.caosdb-f-entity-role'); + roleElem.detach(); + var parentsElem = $(header).find('.caosdb-f-parent-list'); + parentsElem.detach(); + var temp = $('<div class="form-group"><label class="col-sm-2 control-label">parents</label><div class="col-sm-10"></div></div>'); + temp.find("div.col-sm-10").append(parentsElem); + + header.attr("title", "Drop parents from the right panel here."); + header.data("toggle", "tooltip"); + + // create inputs + var inputs = [ + roleElem, + temp, + this.make_input("name", getEntityName(entity)), + this.make_input("description", getEntityDescription(entity)), + ]; + if (getEntityRole(roleElem[0]) == "Property") { + for (input of this.make_datatype_input(getEntityDatatype(entity), getEntityUnit(entity))) { + inputs.push(input); + } + temp.hide(); + } else if (getEntityRole(roleElem[0]) == "File") { + inputs.push(this.make_input("path", getEntityPath(entity))); + } + // remove other stuff + header.children().remove(); + header.append($('<form class="form-horizontal"></form>').append(inputs)); + edit_mode.make_dataype_input_logic(header); + + edit_mode.add_parent_delete_buttons(header[0]); + } + + this.isListDatatype = function(datatype) { + return (typeof datatype !== 'undefined' && datatype.substring(0, 5) == "LIST<"); + } + + this.unListDatatype = function(datatype) { + return datatype.substring(5, datatype.length - 1); + } + + this.make_dataype_input_logic = function(header) { + var unitLabel = $(header).find(".caosdb-f-entity-unit-label"); + var unitInput = $(header).find(".caosdb-f-entity-unit"); + var isListLabel = $(header).find(".caosdb-f-entity-is-list-label"); + var isListInput = $(header).find(".caosdb-f-entity-is-list"); + var referenceLabel = $(header).find(".caosdb-f-entity-reference-label"); + var referenceInput = $(header).find(".caosdb-f-entity-reference"); + + // TODO show on reference + referenceInput.hide(); + referenceLabel.hide(); + + // TODO show unit for double and integer + } + + this.make_datatype_input = function(datatype, unit) { + var is_list = edit_mode.isListDatatype(datatype); + if (is_list) { + datatype = edit_mode.unListDatatype(datatype); + } + + var datatypes = { + "TEXT": false, + "DOUBLE": true, + "INTEGER": true, + "DATETIME": false, + "BOOLEAN": false, + /*TODO "REFERENCE":false*/ + } + var select = $('<select></select>'); + for (dt in datatypes) { + select.append('<option data-has-refid="' + (dt == "REFERENCE") + '" data-has-unit="' + datatypes[dt] + '" value="' + dt + '" ' + (dt == datatype ? 'selected="true"' : '') + '>' + dt + '</option>'); + } + + console.log(select.html()); + return [ + $('<div class="form-group"><label class="col-sm-2 control-label caosdb-f-entity-datatype-label">datatype</label><div class="col-sm-3"><select class="form-control caosdb-f-entity-datatype">' + select.html() + '</select></div><label class="col-sm-2 control-label caosdb-f-entity-reference-label">reference</label><div class="col-sm-3"><input readonly="true" class="form-control caosdb-f-entity-reference" value="" placeholder="Drop a RT"></input></div><label class="col-sm-1 control-label caosdb-f-entity-is-list-label">list</label><div class="col-sm-1"><input class="caosdb-f-entity-is-list" type="checkbox" ' + (is_list ? 'checked="true" ' : "") + '/></div>')[0], + $('<div class="form-group"><label class="col-sm-2 control-label caosdb-f-entity-unit-label">unit</label><div class="col-sm-2"><input type="text" class="form-control caosdb-f-entity-unit" value="' + (typeof unit == 'undefined' ? "" : unit) + '"></input></div></div>')[0], + ]; + } + + this.make_input = function(label, value) { + return $('<div class="form-group"><label class="col-sm-2 control-label">' + label + '</label><div class="col-sm-10"><input type="text" class="form-control caosdb-f-entity-' + label + '" value="' + (typeof value == 'undefined' ? "" : value) + '"></input></div></div>')[0]; + } + + this.smooth_replace = function(from, to) { + console.log(from); + $(to).hide(); + $(from).fadeOut(); + $(from).after(to); + $(from).detach(); + $(to).fadeIn(); + console.log(from); + } + + this.make_property_editable = function(element) { + console.log(element); + + edit_mode.add_property_trash_button($(element).find(".caosdb-property-edit")[0], element); + var valfield = $(element).find(".caosdb-property-value"); + var editfield = $(element).find(".caosdb-property-edit-value"); + var property = getPropertyFromElement(element); + + console.log(property); + valfield.hide(); + editfield.show(); + editfield.text(""); + var editelementstring; + if (property.datatype == "TEXT") { + //editelementstring = "<input type='text' value='" + property.value + "'></input>"; + editelementstring = "<textarea>" + property.value + "</textarea>"; + } else if (property.datatype == "DATE") { + let date = caosdbq2InputDate(property.value)[0]; + editelementstring = "<input type='date' value='" + date + "'></input>"; + } else if (property.datatype == "DATETIME") { + let dateandtime = caosdb2InputDate(property.value); + let date = dateandtime[0]; + if (dateandtime.length == 2) { + let time = dateandtime[1]; + editelementstring = "<input type='date' value='" + date + "'></input>" + + "<input type='time' value='" + time + "'></input>"; + } else { + editelementstring = "<input type='date' value='" + date + "'></input>"; + } + } else if (property.datatype == "DOUBLE") { + editelementstring = "<input type='number' step='any' value='" + property.value + "'></input><input class='caosdb-unit' title='unit' style='width: 60px;' placeholder='unit' value='" + (typeof property.unit == 'undefined' ? "" : property.unit) + "' type='text'></input>"; + } else if (property.datatype == "INTEGER") { + editelementstring = "<input type='number' value='" + property.value + "'></input><input class='caosdb-unit' title='unit' style='width: 60px;' placeholder='unit' value='" + (typeof property.unit == 'undefined' ? "" : property.unit) + "' type='text'></input>"; + } else if (property.datatype == "FILE") { + editelementstring = "<input type='text' value='" + property.value + "'></input>"; + } else if (property.reference) { + editelementstring = '<select class="form-control caosdb-list-' + property.datatype + '" data-resolved="false"><option selected class="caosdb-f-option-default" value="' + property.value + '">' + property.value + '</option><option></option></select>'; + edit_mode.retrieve_datatype_list(property.datatype).then(() => { + var elist = $(editfield).find(".caosdb-list-" + property.datatype); + elist.find("[value='" + property.value + "'].caosdb-f-option-default").remove(); + elist.find("[value='" + property.value + "']").attr("selected", "selected"); + }); + edit_mode.retrieve_datatype_list(property.datatype); + } else { + console.log(property.datatype); + } + editfield.append($(editelementstring)); + } + + this.create_new_record = async function(recordtype_id, name = undefined) { + var rt = await retrieve(recordtype_id); + var newrecord = createEntityXML("Record", undefined, undefined, + getProperties(rt[0]), + [{ + name: getEntityName(rt[0]) + }], true); + var doc = str2xml("<Response/>"); + doc.firstElementChild.appendChild(newrecord.firstElementChild); + console.log(doc); + // TODO I dunno whats wrong here: xml -> str -> xml ??? + var x = await transformation.transformEntities(str2xml(xml2str(doc))); + console.log(x); + return x[0]; + } + + + this.init_edit_app = function() { + var props = document.getElementsByClassName("caosdb-f-edit-drag"); + for (var pel of props) { + pel.addEventListener("dragstart", edit_mode.prop_dragstart); + pel.setAttribute("draggable", true); + } + + var new_buttons = $('.caosdb-f-edit-panel-new-button'); + var app = new StateMachine({ + transitions: [{ + name: "init", + from: 'none', + to: "initial" + }, { + name: "newEntity", + from: "initial", + to: "changed" + + }, { + name: "startEdit", + from: "initial", + to: "changed" + }, { + name: "insert", + from: 'changed', + to: 'wait' + }, { + name: "update", + from: 'changed', + to: 'wait' + }, { + name: "showResults", + from: 'wait', + to: 'initial' + }, { + name: "cancel", + from: 'changed', + to: 'initial' + }, { + name: "finish", + from: '*', + to: 'final' + }], + }); + new_buttons.filter('.caosdb-f-hide-on-empty-input').parent().each(function(index) { + var button = $(this); + button.hide(); + var input = button.parent().find("input"); + input.on("input", function(e) { + console.log(e); + console.log(button); + console.log(input); + if (input.val() == '') { + button.fadeOut(); + } else { + button.fadeIn(); + } + }); + }); + new_buttons.filter('.new-property').click(() => { + edit_mode.create_new_entity("Property").then(entity => { + app.newEntity(entity); + }, edit_mode.handle_error); + }); + new_buttons.filter('.new-recordtype').click(() => { + edit_mode.create_new_entity("RecordType").then(entity => { + app.newEntity(entity); + }, edit_mode.handle_error); + }); + var parent_drop_listener = function(e) { + e.preventDefault(); + var entity = $(this).parent(); + app.startEdit(entity); + edit_mode.add_dropped_parent(e, app.entity); + } + var property_drop_listener = function(e) { + e.preventDefault(); + var entity = $(this).parent(); + app.startEdit(entity); + edit_mode.add_dropped_property(e, app.entity); + } + app.errorHandler = function(fn) { + try { + fn(); + } catch (e) { + edit_mode.handle_error(e); + } + }; + app.onEnterInitial = function(e) { + console.log(e); + app.old = undefined; + app.errorHandler(() => { + // make entities dropable and freezable + new_buttons.attr("disabled", false); + $('.caosdb-entity-panel').each(function(index) { + let entity = this; + edit_mode.set_entity_dropable(entity, edit_mode.prop_dragover, edit_mode.prop_dragleave, parent_drop_listener, property_drop_listener); + if (typeof getEntityID(entity) == "undefined" || getEntityID(entity) == '') { + // no id -> insert + edit_mode.add_start_edit_button(entity, () => { + app.newEntity(entity) + }); + } else { + // has id -> delete, edit, create RT + edit_mode.add_delete_button(entity, () => { + edit_mode.smooth_replace(entity, app.waiting); + transaction.deleteEntities([getEntityID(entity)]).then(response => { + console.log(response); + return transformation.transformEntities(response); + }, edit_mode.handle_error).then(entities => { + console.log(entities); + edit_mode.smooth_replace(app.waiting, entities[0]); + app.entity = entities[0]; + if (edit_mode.has_errors(app.entity)) { + hintMessages.hintMessages(app.entity); + edit_mode.set_entity_dropable(app.entity, edit_mode.prop_dragover, edit_mode.prop_dragleave, parent_drop_listener, property_drop_listener); + edit_mode.add_start_edit_button(app.entity, () => { + app.startEdit(app.entity) + }); + if (getEntityRole(app.entity) == "RecordType") { + edit_mode.add_new_record_button(app.entity, () => { + edit_mode.create_new_record(getEntityID(app.entity)).then((entity) => { + app.newEntity(entity); + }, edit_mode.handle_error); + }); + } + } else { + $(app.entity).find('.caosdb-entity-actions-panel').remove(); + var closeButton = $(app.entity).find('.alert-info .close'); + closeButton.text("Ok"); + closeButton.click((e) => { + $(app.entity).remove(); + }); + } + }, edit_mode.handle_error); + }); + edit_mode.add_start_edit_button(entity, () => { + app.startEdit(entity) + }); + if (getEntityRole(entity) == "RecordType") { + edit_mode.add_new_record_button(entity, () => { + edit_mode.create_new_record(getEntityID(entity)).then((entity) => { + app.newEntity(entity); + }, edit_mode.handle_error); + }); + } + } + }); + }); + }; + app.onLeaveInitial = function(e) { + console.log(e); + app.errorHandler(() => { + // remove event listeners which add the save button an so on + $('.caosdb-entity-panel').each(function(index) { + edit_mode.unset_entity_dropable(this, edit_mode.prop_dragover, edit_mode.prop_dragleave, parent_drop_listener, property_drop_listener); + edit_mode.remove_start_edit_button(this); + edit_mode.remove_new_record_button(this); + edit_mode.remove_delete_button(this); + }); + }); + new_buttons.attr("disabled", true); + }; + app.onBeforeStartEdit = function(e, entity) { + console.log(e); + console.log(entity); + app.old = entity; + app.entity = $(entity).clone(true)[0]; + edit_mode.smooth_replace(app.old, app.entity); + + edit_mode.add_save_button(app.entity, () => app.update(app.entity)); + edit_mode.add_cancel_button(app.entity, () => app.cancel(app.entity)); + + edit_mode.freeze_but(app.entity); + }; + app.onBeforeCancel = function(e) { + console.log(e); + edit_mode.smooth_replace(app.entity, app.old); + edit_mode.unfreeze(); + }; + app.onUpdate = function(e, entity) { + console.log(e); + edit_mode.update_entity(entity).then(response => { + console.log(response); + return transformation.transformEntities(response); + }, edit_mode.handle_error).then(entities => { + console.log(entities); + edit_mode.smooth_replace(app.entity, entities[0]); + app.entity = entities[0]; + app.showResults(); + resolve_references.init(); + preview.init(); + edit_mode.init_drag_n_drop(app.entity); + }, edit_mode.handle_error); + }; + app.onEnterChanged = function(e) { + console.log(e); + hintMessages.removeMessages(app.old); + edit_mode.make_header_editable(app.entity); + edit_mode.init_drag_n_drop(app.entity); + hintMessages.hintMessages(app.entity); + $(app.entity).find('.caosdb-annotation-section').remove(); + console.log(app.entity); + for (var element of $(app.entity).find('.caosdb-property-row')) { + edit_mode.make_property_editable(element); + } + } + app.onEnterWait = function(e) { + edit_mode.smooth_replace(app.entity, app.waiting); + console.log(e); + } + app.onLeaveWait = function(e) { + console.log(e); + edit_mode.smooth_replace(app.waiting, app.entity); + } + app.onBeforeNewEntity = function(e, entity) { + console.log(e); + console.log(entity); + if (typeof entity == "undefined") { + throw new TypeError("entity is undefined"); + } + + edit_mode.freeze_but(entity); + + app.entity = entity; + edit_mode.add_save_button(entity, () => app.insert(entity)); + edit_mode.add_cancel_button(entity, () => app.cancel(entity)); + + $(entity).hide(); + if ($('.caosdb-f-main-entities .caosdb-entity-panel').length > 0) { + $('.caosdb-f-main-entities .caosdb-entity-panel').first().before(entity); + } else { + $('.caosdb-f-main-entities').append(entity); + } + $(entity).fadeIn(); + $(window).scrollTop(0); + + app.old = $('<div/>')[0]; + } + app.onInsert = function(e, entity) { + console.log(e); + edit_mode.insert_entity(entity).then(response => { + console.log(response); + return transformation.transformEntities(response); + }, edit_mode.handle_error).then(entities => { + console.log(entities); + edit_mode.smooth_replace(app.entity, entities[0]); + app.entity = entities[0]; + app.showResults(); + }, edit_mode.handle_error); + } + app.onFinish = function(e) { + console.log(e); + if (app.old) { + console.log(app.old); + edit_mode.smooth_replace(app.entity, app.old); + } + edit_mode.unfreeze(); + } + app.onShowResults = function(e) { + if (!edit_mode.has_errors(app.entity)) { + console.log(app.entity); + app.old = false; + } + console.log(e); + hintMessages.hintMessages(app.entity); + edit_mode.unfreeze(); + if (!edit_mode.has_errors(app.entity)) { + edit_mode.enter_edit_mode(app); + } + } + app.waiting = createWaitingNotification("Please wait."); + $(app.waiting).hide(); + app.init(); + return app; + } + + this.has_errors = function(entity) { + return $(entity).find(".alert.alert-danger").length > 0; + } + + this.freeze_but = function(element) { + $('.caosdb-f-main-entities').children().each(function(index) { + edit_mode.freeze_entity(this); + }); + edit_mode.unfreeze_entity(element); + } + + this.unfreeze = function() { + $('.caosdb-f-main-entities').children().each(function(index) { + edit_mode.unfreeze_entity(this); + }); + } + + // TODO: write generic function format property depending on datatype and the property + + this.retrieve_datatype_list = async function(datatype) { + var entities = await query("FIND Record " + datatype); + var files = await query("FIND File " + datatype); + + for (var i = 0; i < entities.length + files.length; i++) { + + if (i < entities.length) { + var eli = entities[i]; + } else { + var eli = files[i]; + } + var prlist = getProperties(eli); + var prdict = []; + for (var j = 0; j < prlist.length; j++) { + prdict.push(prlist[j].name + ": " + prlist[j].value); + } + if (prlist.length == 0) { + prdict.push("ID: " + getEntityID(eli)); + } + $("select.caosdb-list-" + datatype).not('[data-resolved="true"]').append( + $("<option value=\"" + getEntityID(eli) + "\">" + prdict.join(", ") + "</option>")); + } + $("select.caosdb-list-" + datatype).not('[data-resolved="true"]').attr("data-resolved", "true"); + } + + this.highlight = function(entity) { + $(entity).css("border-color", "red"); + } + + this.unhighlight = function(entity) { + $(entity).css("border-color", ""); + } + + this.handle_error = function(err) { + console.log(err); + var editPanel = $(edit_mode.get_edit_panel()); + editPanel.empty(); + editPanel.append(createErrorNotification(err)); + + } + + this.get_edit_panel = function() { + return $('.caosdb-f-edit-panel-body')[0]; + } + + this.add_wait_datamodel_info = function() { + $(this.get_edit_panel()).append(createWaitingNotification("Please wait.")); + } + + this.toggle_edit_panel = function() { + $(".caosdb-f-main").toggleClass("container-fluid").toggleClass("container"); + $(".caosdb-f-main-entities").toggleClass("col-xs-8"); + $(".caosdb-f-edit").toggleClass("hidden").toggleClass("col-xs-4"); + } + + this.leave_edit_mode_template = function(app) { + console.log("leave edit mode"); + app.finish(); + $(".caosdb-f-btn-toggle-edit-mode").text("Edit Mode"); + $(".caosdb-f-entity-save-button").remove(); + $(".caosdb-f-entity-trash-button").remove(); + window.localStorage.removeItem("edit_mode"); + } + + this.is_edit_mode = function() { + return window.localStorage.edit_mode !== undefined; + } + + this.add_cancel_button = function(ent, callback) { + var cancel_btn = $('<button class="btn btn-link caosdb-update-entity-button caosdb-f-entity-cancel-button">Cancel</button>'); + + $(ent).find(".caosdb-entity-actions-panel").append(cancel_btn); + + $(cancel_btn).click(callback); + } + + this.create_new_entity = async function(role) { + var empty_entity = str2xml('<Response><' + role + '/></Response>'); + return (await transformation.transformEntities(empty_entity))[0]; + } + + this.remove_cancel_button = function(entity) { + $(entity).find('.caosdb-f-entity-cancel-button').remove() + } + + /** + * entity : HTMLElement + */ + this.freeze_entity = function(entity) { + $(entity).css("pointer-events", "none").css("filter", "blur(10px)"); + } + + /** + * entity : HTMLElement + */ + this.unfreeze_entity = function(entity) { + $(entity).css("pointer-events", "").css("filter", ""); + } + + this.filter = function(ent_type) { + var text = $("#caosdb-f-filter-" + ent_type).val(); + if (ent_type == "properties") { + var short_type = "p"; + } else if (ent_type == "recordtypes") { + var short_type = "rt"; + } else { + alert("unkown type"); + } + + var keywords = text.toLowerCase().split(" "); + var elements = document.getElementsByClassName("caosdb-f-edit-drag"); + for (var el of elements) { + if (el.id.includes("caosdb-f-edit-" + short_type)) { + var name = el.innerText.toLowerCase(); + var found_kw = false; + for (var kw of keywords) { + if (name.includes(kw)) { + found_kw = true; + break; + } + } + if (found_kw) { + $(el).show() + } else { + $(el).hide() + } + } + } + } + + + this.add_start_edit_button = function(entity, callback) { + edit_mode.remove_start_edit_button(entity); + var button = $('<button title="Edit this ' + getEntityRole(entity) + '." class="btn btn-link caosdb-update-entity-button caosdb-f-entity-start-edit-button">Edit</button>'); + + $(entity).find(".caosdb-entity-actions-panel").append(button); + + $(button).click(callback); + } + + this.remove_start_edit_button = function(entity) { + $(entity).find(".caosdb-f-entity-start-edit-button").remove(); + } + + + this.add_new_record_button = function(entity, callback) { + edit_mode.remove_new_record_button(entity); + var button = $('<button title="Create a new Record from this RecordType." class="btn btn-link caosdb-update-entity-button caosdb-f-entity-new-record-button">+Record</button>'); + + $(entity).find(".caosdb-entity-actions-panel").append(button); + + $(button).click(callback); + } + + this.remove_new_record_button = function(entity) { + $(entity).find(".caosdb-f-entity-new-record-button").remove(); + } + + this.add_delete_button = function(entity, callback) { + edit_mode.remove_delete_button(entity); + var button = $('<button title="Delete this ' + getEntityRole(entity) + '." class="btn btn-link caosdb-update-entity-button caosdb-f-entity-delete-button">Delete</button>'); + + $(entity).find(".caosdb-entity-actions-panel").append(button); + + $(button).click(callback); + } + + this.remove_delete_button = function(entity) { + $(entity).find(".caosdb-f-entity-delete-button").remove(); + } + +} +/** + * Add the extensions to the webui. + */ +$(document).ready(function() { + edit_mode.init(); +}); \ No newline at end of file diff --git a/src/core/js/ext_cosmetics.js b/src/core/js/ext_cosmetics.js new file mode 100644 index 00000000..fe1819ed --- /dev/null +++ b/src/core/js/ext_cosmetics.js @@ -0,0 +1,23 @@ +var cosmetics = new function() { + this.init = function() { + this.linkify(); + } + + this.linkify = function() { + $('.caosdb-property-text-value').each(function(index) { + if (/^https?:\/\//.test(this.innerText)) { + var uri = this.innerText; + var text = uri + + $(this).parent().css("overflow", "hidden"); + $(this).parent().css("text-overflow", "ellipsis"); + $(this).html('<a href="' + uri + '"><span class="glyphicon glyphicon-new-window"></span> ' + text + '</a>'); + } + }); + } +} + + +$(document).ready(function() { + cosmetics.init(); +}); \ No newline at end of file diff --git a/src/core/js/ext_references.js b/src/core/js/ext_references.js new file mode 100644 index 00000000..0fa49dbb --- /dev/null +++ b/src/core/js/ext_references.js @@ -0,0 +1,58 @@ +/** + * Resolve References + * Alexander Schlemmer, 11/2018 + */ + +var resolve_references = new function() { + this.init = function() { + this.references(); + } + + this.get_person_str = function(el) { + var valpr = getProperties(el); + if (valpr == undefined) { + return; + } + return valpr.filter(valprel => valprel.name == "firstName")[0].value + + " " + valpr.filter(valprel => valprel.name == "lastName")[0].value; + } + + this.update_single_resolvable_reference = async function(rs) { + var rseditable = rs.getElementsByClassName("caosdb-resolve-reference-target")[0]; + var el = await retrieve(getIDfromHREF(rs)); + var pr = getParents(el[0]); + if (getEntityHeadingAttribute(el[0], "path") !== undefined || pr[0].name == "Image") { + var pths = getEntityHeadingAttribute(el[0], "path").split("/"); + rseditable.textContent = pths[pths.length - 1]; + } else if (pr[0].name == "Person") { + rseditable.textContent = this.get_person_str(el[0]); + } else if (pr[0].name == "BoxType") { + rseditable.textContent = getEntityName(el[0]); + } else if (pr[0].name == "Loan") { + var persel = await retrieve(getProperty(el[0], "Borrower")); + var loan_state = awi_demo.get_loan_state_string(getProperties(el[0])); + rseditable.textContent = "Borrowed by " + this.get_person_str(persel[0]) + " (" + loan_state.replace("_", " ") + ")"; + } else if (pr[0].name == "Box") { + rseditable.textContent = getProperty(el[0], "Number"); + } else if (pr[0].name == "Palette") { + rseditable.textContent = getProperty(el[0], "Number"); + } else if (pr[0].name.includes("Model")) { + rseditable.textContent = pr[0].name; + } else { + rseditable.textContent = el[0].id; + } + } + + this.references = async function() { + var rs = document.getElementsByClassName("caosdb-resolvable-reference"); + + for (var i = 0; i < rs.length; i++) { + this.update_single_resolvable_reference(rs[i]); + } + } +} + + +$(document).ready(function() { + resolve_references.init(); +}); diff --git a/src/core/js/preview.js b/src/core/js/preview.js new file mode 100644 index 00000000..30b9c95b --- /dev/null +++ b/src/core/js/preview.js @@ -0,0 +1,721 @@ +/** + * preview module contains functionality for the preview of referenced entities. + */ +var preview = new function() { + this.carouselId = 0; + this.classNameEntityPreview = "caosdb-entity-preview"; + this.classNameShowPreviewButton = "caosdb-show-preview-button"; + this.classNameHidePreviewButton = "caosdb-hide-preview-button"; + this.classNamePreview = "caosdb-preview-container"; + this.classNamePreviewCarouselNav = "caosdb-preview-carousel-nav"; + this.classNameNotificationArea = globalClassNames.NotificationArea; + this.classNameWaitingNotification = globalClassNames.WaitingNotification; + this.classNameErrorNotification = globalClassNames.ErrorNotification; + + /** + * Initialize the preview feature for all reference properties in the current window. + * + * @return {HTMLElement[]} The initialized properties. + */ + this.init = function() { + let props = []; + $('.caosdb-entity-panel').each((index, entity) => { + props.concat(preview.initEntity(entity)) + }); + return props; + } + + /** + * Initialize the preview feature for all reference properties which belong to certain entity. + * + * @param {HTMLElement} entity + * @return {HTMLElement[]} The initialized properties. + */ + this.initEntity = function(entity) { + let props = []; + $(entity).find('.caosdb-properties > .list-group-item').each((index, property) => { + let refLinksContainer = preview.getRefLinksContainer(property); + if (refLinksContainer != null) { + props.push(preview.initProperty(property)); + } + }); + return props; + } + /** + * Initialize the preview feature for a certain reference property. + * @param {HTMLElement} ref_property_elem + * @return {Object} A state machine. + */ + this.initProperty = function(ref_property_elem) { + let showPreviewButton = preview.createShowPreviewButton(); + let hidePreviewButton = preview.createHidePreviewButton(); + let notificationArea = preview.createNotificationArea(); + let refLinksContainer = preview.getRefLinksContainer(ref_property_elem); + + let app = new StateMachine({ + transitions: [{ + name: "init", + from: 'none', + to: "showLinks" + }, { + name: "requestPreview", + from: 'showLinks', + to: () => (preview.hasPreview(ref_property_elem) ? 'showPreview' : 'waiting') + }, { + name: "receivePreview", + from: 'waiting', + to: 'showPreview' + }, { + name: "hidePreview", + from: 'showPreview', + to: 'showLinks' + }, { + name: "resetApp", + from: '*', + to: 'showLinks' + }, ], + }); + + let executeFailSave = function(fn) { + try { + fn(); + } catch (e) { + globalError(e); + setTimeout(() => { + app.resetApp(e); + }, 1000); + } + }; + // for debugging: + //app.onTransition = function(e){ + // console.log(e); + //} + app.onEnterShowLinks = function(e) { + executeFailSave(function() { + $(showPreviewButton).show(); + $(hidePreviewButton).hide(); + $(refLinksContainer).show(); + $(preview.getPreviewCarousel(ref_property_elem)).hide(); + }); + }; + app.onLeaveShowLinks = function(e) { + executeFailSave(function() { + $(showPreviewButton).hide(); + preview.removeAllErrorNotifications(ref_property_elem); + }); + }; + app.onEnterWaiting = function(e) { + executeFailSave(function() { + preview.addWaitingNotification(ref_property_elem, preview.createWaitingNotification()); + let entityIds = preview.getEntityIds(refLinksContainer); + preview.retrievePreviewEntities(entityIds).then(entities => { + app.receivePreview(entities); + }, err => { + app.resetApp(err); + }); + }); + }; + app.onReceivePreview = function(e, entities) { + executeFailSave(function() { + preview.addPreview(ref_property_elem, preview.createPreview(entities, refLinksContainer)); + // TODO: Check whether this is needed. + resolve_references.init(); + }); + } + app.onLeaveWaiting = function() { + executeFailSave(function() { + removeAllWaitingNotifications(ref_property_elem); + }); + } + app.onEnterShowPreview = function(e) { + executeFailSave(function() { + $(preview.getPreviewCarousel(ref_property_elem)).show(); + $(hidePreviewButton).show(); + $(refLinksContainer).hide(); + }); + } + app.onResetApp = function(e, error) { + removeAllWaitingNotifications(ref_property_elem); + preview.removeAllErrorNotifications(ref_property_elem); + // remove carousel + if (preview.hasPreview(ref_property_elem)) { + $(preview.getPreviewCarousel(ref_property_elem)).remove(); + } + if (error != null) { + preview.addErrorNotification(ref_property_elem, createErrorNotification(error)); + } + } + + + // start with showLinks state + app.init(); + showPreviewButton.onclick = () => app.requestPreview(); + hidePreviewButton.onclick = () => app.hidePreview(); + preview.addShowPreviewButton(ref_property_elem, showPreviewButton); + preview.addNotificationArea(ref_property_elem, notificationArea); + preview.addHidePreviewButton(ref_property_elem, hidePreviewButton); + + return app; + } + + this.createWaitingNotification = function() { + return createWaitingNotification("Loading preview. Please wait."); + } + + /** + * Determine if a previewContainer is already present in a property's div. + * + * @param {HTMLElement} property. + * @return {Boolean} + */ + this.hasPreview = function(property) { + return property.getElementsByClassName(this.classNamePreview).length > 0; + } + + /** + * Add a preview container to a property div. + * + * @param {HTMLElement} property - Where to add the preview. + * @param {HTMLElement} previewContainer - The container which is to be added. + * @return {HTMLElement} The parameter `property`. + */ + this.addPreview = function(property, previewContainer) { + property.getElementsByClassName("caosdb-property-value")[0].appendChild(previewContainer); + return property; + } + + /** + * Transform the raw xml response of the server into an array of entities for preview. + * + * @param {Promise for XMLDocument} xml - A Promise for the servers xml response. + * @return {Promise for HTMLElement[]} A Promise for an array of entities. + */ + this.processPreviewResponse = function(xml) { + let xsl = preview.getEntityXsl(); + return preview.transformXmlToPreviews(xml, xsl); + } + + /** + * Retrieve the XSL script for entities from the server. + * + * @return {Promise for XMLDocument} A Promise for the XSL script. + */ + this.getEntityXsl = async function _getEntityXsl() { + return transformation.retrieveEntityXsl(); + }; + + /** + * Add a notification area to a reference property. + * + * @param {HTMLElement} property + * @param {HTMLElement} notificationArea + * @return {HTMLElement} The parameter `property`. + */ + this.addNotificationArea = function(property, notificationArea) { + property.getElementsByClassName("caosdb-property-value")[0].appendChild(notificationArea); + return property; + } + + /** + * Create a new `show preview` button. + * @return {HTMLElement} A button for showing the preview carousel. + */ + this.createShowPreviewButton = function() { + return $('<button class="' + preview.classNameShowPreviewButton + ' btn btn-link btn-xs" title="Show preview of the referenced entities."><span class="glyphicon glyphicon-eye-open"></button>')[0]; + } + + /** + * Create a new `hide preview` button. + * @return {HTMLElement} A button for hiding the preview carousel. + */ + this.createHidePreviewButton = function() { + return $('<button class="' + preview.classNameHidePreviewButton + ' btn btn-link btn-xs" title="Hide preview and show links."><span class="glyphicon glyphicon-eye-close"></button>')[0]; + } + + /** + * Create a notification area. That is a div with class `caosdb-preview-notification-area`. + * @return {HTMLElement} A div + */ + this.createNotificationArea = function() { + return $('<div class="' + preview.classNameNotificationArea + '"></div>')[0]; + } + + /** + * Add a showPreviewButton to a reference property's value section. + * + * The button is appended to the first occuring element with class `caosdb-property-value`. + * + * @param {HTMLElement} ref_property_elem + * @param {HTMLElement} buttom_elem + * @return {HTMLElement} parameter `ref_property_elem` + */ + this.addShowPreviewButton = function(ref_property_elem, button_elem) { + ref_property_elem.getElementsByClassName("caosdb-property-value")[0].appendChild(button_elem); + return ref_property_elem; + } + + /** + * Add a hidePreviewButton to a reference property's value section. + * + * The button is appended to the first occuring element with class `caosdb-property-value`. + * + * @param {HTMLElement} ref_property_elem + * @param {HTMLElement} buttom_elem + * @return {HTMLElement} The parameter `ref_property_elem`. + */ + this.addHidePreviewButton = function(ref_property_elem, button_elem) { + ref_property_elem.getElementsByClassName("caosdb-property-value")[0].appendChild(button_elem); + return ref_property_elem; + } + + /** + * Add an error notification to a properties value section. + * + * The error element is appended to the first occuring element with class + * `caosdb-preview-notification_area`. + * + * @param {HTMLElement} ref_property_elem + * @param {HTMLElement} error_elem + * @return {HTMLElement} The parameter `ref_property_elem`. + */ + this.addErrorNotification = function(ref_property_elem, error_elem) { + ref_property_elem.getElementsByClassName( + preview.classNameNotificationArea)[0].appendChild(error_elem); + return ref_property_elem; + } + + /** + * Add a waiting notification to a properties value section. + * + * The notification element is appended to the first occuring element with class + * `caosdb-preview-notification_area`. + * + * Show a waiting notification while the entity request is pending. + * @param {HTMLElement} ref_property_elem - Add `waiting_elem` here. + * @param {HTMLElement} waiting_elem - The waiting notification. + * @return {HTMLElement} The parameter `ref_property_elem`. + */ + this.addWaitingNotification = function(ref_property_elem, waiting_elem) { + ref_property_elem.getElementsByClassName( + preview.classNameNotificationArea)[0].appendChild(waiting_elem); + return ref_property_elem; + } + + /** + * Get a container of reference links or the single reference link of a reference property. + * @param {HTMLElement} ref_property_elem + * @return {HTMLElement} A div with links in it. + */ + this.getRefLinksContainer = function(ref_property_elem) { + if (ref_property_elem == null) { + throw new Error("parameter `ref_property_elem` was null."); + } + let refLinksList = $(ref_property_elem).find('.caosdb-value-list').has('.caosdb-id')[0]; + if (refLinksList == null) { + return $(ref_property_elem).find('.caosdb-property-value > .btn').has('.caosdb-id')[0]; + } + return refLinksList + } + + /** + * Get the preview carousel of a reference property. + * @param {HTMLElement} ref_property_elem + * @return {HTMLElement} A div with the carousel and the navigation bar. + */ + this.getPreviewCarousel = function(ref_property_elem) { + return ref_property_elem.getElementsByClassName(preview.classNamePreview)[0]; + } + + /** + * Get the showPreviewButton of a reference property. + * @param {HTMLElement} ref_property_elem + * @return {HTMLElement} A button for showing the preview carousel. + */ + this.getShowPreviewButton = function(ref_property_elem) { + return ref_property_elem.getElementsByClassName(preview.classNameShowPreviewButton)[0]; + } + + /** + * Get the hidePreviewButton of a reference property. + * @param {HTMLElement} ref_property_elem + * @return {HTMLElement} A button for hiding the preview carousel. + */ + this.getHidePreviewButton = function(ref_property_elem) { + return ref_property_elem.getElementsByClassName(preview.classNameHidePreviewButton)[0]; + } + + /** + * Remove all error notifications from the notification area of a reference property. + * @param {HTMLElement} ref_property_elem + * @return {HTMLElement} The parameter `ref_property_elem`. + */ + this.removeAllErrorNotifications = function(ref_property_elem) { + $(ref_property_elem.getElementsByClassName(preview.classNameErrorNotification)).remove(); + return ref_property_elem; + } + + /** + * Create a preview carousel from an array of entity elements. + * + * A carousel consists of the main div with class `carousel slide` and a unique ID which will + * be generated here. Inside there are the navigation bar with class + * `caosdb-preview-carousel-nav`, and the inner div which contains and show the several slides + * with class `carousel-inner`. + * + * The refLinksContainer are cloned and modified such that they trigger the + * sliding and added to the navigation bar. Then a set of empty slides is added to the inner + * div. The entities are put into the correct slide using the data-slide-to attributes and the + * entity id of each selector button. + * + * @param {HTMLElement[]} entities - The array of entity elements. + * @param {HTMLElement} refLinksContainer - The original reference links. + * @return {HTMLElement} A new preview carousel. + */ + this.createPreviewCarousel = function(entities, refLinksContainer) { + if (entities == null) { + throw new Error("entities must not be null."); + } + let carouselId = ("previewCarousel" + preview.carouselId++); + let nav = preview.createCarouselNav(refLinksContainer, carouselId); //preserves order, first is active + let N = $(nav).find('[data-slide-to]').length; + let inner = preview.createEmptyInner(N) //no content, first is active + + let selectorButtons = preview.getSelectorButtons(nav); + selectorButtons.each((index, button) => { + let slide_id = button.getAttribute("data-slide-to"); + let entity_id = getEntityId(button); + let entity = preview.getEntityById(entities, entity_id); + if (entity == null) throw new Error("Entity with ID " + entity_id + " could not be found!"); + inner.children[slide_id].appendChild(preview.preparePreviewEntity(entity)); + }); + + let mainDiv = $('<div class="carousel slide" data-interval="false"></div>')[0]; + mainDiv.appendChild(nav); + mainDiv.appendChild(inner); + mainDiv.id = carouselId; + + $(mainDiv).on('slid.bs.carousel', preview.triggerUpdateActiveSlideItemSelector); + + return mainDiv; + } + + /** + * Get the selector buttons from a div which contains them or return the single selector button + * if the `refLinksContainer` parameter is itself the selector button. + * + * @param {HTMLElement} refLinksContainer + * @return {jQuery} A collection of selector buttons. + */ + this.getSelectorButtons = function(refLinksContainer) { + return $(refLinksContainer).find('[data-slide-to]').addBack('[data-slide-to]'); + } + + /** + * Create a single div with a preview of a single referenced entity or a fancy carousel if + * there are more than one previews to be shown. + * + * @param {HTMLElement[]} entities - The array of entity elements. + * @param {HTMLElement} refLinksContainer - Container with the original reference links. + * @return {HTMLElement} A div. + */ + this.createPreview = function(entities, refLinksContainer) { + var previewElement; + if (preview.getReferenceLinks(refLinksContainer).length > 1) { + previewElement = preview.createPreviewCarousel(entities, refLinksContainer); + } else { + previewElement = preview.createSinglePreview(entities, refLinksContainer); + } + + $(previewElement).toggleClass(preview.classNamePreview, true); + return previewElement; + } + + /** + * Create a single preview entity + * + */ + this.createSinglePreview = function(entities, refLinksContainer) { + let entityId = getEntityId(preview.getReferenceLinks(refLinksContainer)[0]); + let entity = preview.preparePreviewEntity(preview.getEntityById(entities, entityId)); + return entity; + } + + /** + * Clone and prepare a single preview (which may be one of many previews in a carousel) + * such that the header is clickable and links to the original entity. + * + * @param {HTMLElement} entity + * @return {HTMLElement} The prepared entity. + */ + this.preparePreviewEntity = function(entity) { + var preparedEntity = entity.cloneNode(true); + + // header is clickable: + let href = connection.getBasePath() + transaction.generateEntitiesUri([getEntityId(entity)]); + let link = $('<a title="Load this entity in a new window." href="' + href + '" class="label caosdb-id caosdb-id-button" target="_blank"></a>'); + let entityIdElem = $(preparedEntity).find('.label.caosdb-id'); + link.insertAfter(entityIdElem); + link.append(entityIdElem.text() + " "); + link.append('<span class="glyphicon glyphicon-new-window"/>'); + entityIdElem.remove(); + + return preparedEntity; + } + + /** + * Create the navigation bar for a carousel from original reference links to the entities. + * This contains selector buttons for each individual slide and prev/next buttons. The first + * selector button is active. + * + * @param {HTMLElement} refLinksContainer + * @param {String} carouselId (without leading hashtag) + * @return {HTMLElement} A div with class `caosdb-preview-carousel-nav`. + */ + this.createCarouselNav = function(refLinksContainer, carouselId) { + if (carouselId == null) { + throw new Error("carouselId must not be null."); + } + let prevButton = $('<a role="button" style="z-index: 5;position:absolute; top: 0;left:0px" class="btn btn-default btn-sm" href="#' + carouselId + '" data-slide="prev"></a>')[0]; + prevButton.innerHTML = preview.carouselPrevButtonInnerHTML; + let nextButton = $('<a role="button" style="z-index: 5;position:absolute; top: 0;right:0px" class="btn btn-default btn-sm" href="#' + carouselId + '" data-slide="next"></a>')[0]; + nextButton.innerHTML = preview.carouselNextButtonInnerHTML; + let nav = $('<div class="' + preview.classNamePreviewCarouselNav + '"></div>')[0]; + let selectors = refLinksContainer.cloneNode(true); + $(selectors).show(); + $(selectors).find('a,button,.btn').each((index, button) => { + $(button).toggleClass("active", index === 0); + button.removeAttribute("href"); + button.setAttribute("data-slide-to", index); + button.setAttribute("data-target", "#" + carouselId); + }); + nav.appendChild(prevButton); + nav.appendChild(nextButton); + nav.appendChild(selectors); + + return nav; + }; + + this.carouselPrevButtonInnerHTML = '<span class="glyphicon glyphicon-chevron-left" aria-hidden="true"></span></span><span class="sr-only">Previous</span>'; + this.carouselNextButtonInnerHTML = '<span class="glyphicon glyphicon-chevron-right" aria-hidden="true"></span></span><span class="sr-only">Next</span>'; + + /** + * Create a div with class `carousel-inner` which contains N divs with class `item` while + * the first also has class `active`. These item divs are empty. + * + * @param {Number} N - An integer > 0. + * @return {HTMLElement} A Div with class `carousel-inner`. + */ + this.createEmptyInner = function(N) { + if (N == null || isNaN(N) || N < 1) { + throw new Error("N is to be an integer > 0"); + } + let innerDiv = $('<div class="carousel-inner"><div class="item active"></div></div>')[0]; + let item = $('<div class="item"></div>')[0]; + for (let i = 1; i < N; i++) { + innerDiv.appendChild(item.cloneNode()); + } + return innerDiv; + } + + /** + * Get the entity with a certain ID from an array of entities. Returns null if no such entity + * is in the array. + * @param {HTMLElement[]} entities + * @param {Number} entity_id + * @return {HTMLElement} The entity with id=entity_id or null. + */ + this.getEntityById = function(entities, entity_id) { + if (entities == null) { + throw new Error("entities must not be null"); + } + if (entity_id == null || isNaN(entity_id)) { + throw new Error("entity_id is to be a number"); + } + for (let i = 0; i < entities.length; i++) { + let e = entities[i]; + if (getEntityId(e) === entity_id) { + return e; + } + } + return null; + } + + /** + * Create a div with class `item` which wraps the first argument. + * @param {HTMLElement} entity + * @returns {HTMLElement} + */ + this.createSlideItem = function(entity) { + let item = $('<div class="item"></div>')[0]; + item.appendChild(entity); + return item; + } + + /** + * Find the index of the div.item.active element withing the div.carouse-inner element. + * + * @param {HTMLElement} carousel + * @returns {Number} The (integer) index of the active slide item of a given carousel. + */ + this.getActiveSlideItemIndex = function(carousel) { + let active_item = carousel.getElementsByClassName("carousel-inner")[0].getElementsByClassName("active")[0]; + let i = 0; + while (active_item = active_item.previousElementSibling) { + ++i + } + return i; + } + + /** + * Remove the `active` class from all slide items selectors of a carousel and add it to the new + * slide item selector denoted by the index. + * + * @param {HTMLElement} carousel + * @param {Number} index - The (integer) index of the new active slide item selector. + * @returns {HTMLElement} The parameter `carousel`. + */ + this.setActiveSlideItemSelector = function(carousel, index) { + if (carousel == null) { + throw new Error("parameter `carousel` must not be null."); + } + if (index == null || isNaN(index) || index < 0) { + throw new Error("parameter `index` is to be a non-null positive integer."); + } + let nav = $(carousel).find('.' + preview.classNamePreviewCarouselNav); + nav.find('.active').toggleClass("active", false); + $(preview.getSlideItemSelector(carousel, index)).toggleClass("active", true); + + preview.scrollCarouselNavToActiveSelector(nav); + + return carousel; + } + + this.scrollCarouselNavToActiveSelector = function(nav) { + let selector = nav.find('.active'); + let selectorPos = selector.position().left; + + let scrollbar = nav.find('.caosdb-value-list').has('.btn-group'); + let scrollbarWidth = scrollbar.innerWidth(); + let currentScroll = scrollbar.scrollLeft(); + + let selectorPosInScrollBar = selectorPos - currentScroll; + let distanceToMiddleOfScrollBar = scrollbarWidth / 2 - selectorPosInScrollBar; + + scrollbar.animate({ + scrollLeft: currentScroll - distanceToMiddleOfScrollBar + }, 200); + return nav; + + } + + /** + * Get the slide item selector at postition `i`, starting with zero. + * + * @param {HTMLElement} carousel + * @param {Number} i - The (integer) index of the slide item selector. + * @returns {HTMLElement} The ith slide item selector. + */ + this.getSlideItemSelector = function(carousel, i) { + let items = $(carousel).find('.' + preview.classNamePreviewCarouselNav).find('[data-slide-to]'); + if (items.length <= i) { + throw new Error("Index out of bounds."); + } + return items[i]; + } + + /** + * Find the slideItemSelector which belongs to the next active slideItem and add the `active` class. + * Note: The function which is to be bound to bootstrap's `slid.bs.carousel` event which is triggered + * after the transition to a new slide is done. + * + * @return true. + */ + this.triggerUpdateActiveSlideItemSelector = function(e) { + let carousel = this; + let index_active = preview.getActiveSlideItemIndex(carousel); + preview.setActiveSlideItemSelector(carousel, index_active); + return true; + } + + /** + * Retrieve a list of entities from the server. + * + * @param {String[]} entityIds - The ids of the entities which are to be retrieved. + * @return {Promise for HTMLElement[]} A Promise for an array of entities. + */ + this.retrievePreviewEntities = async function _rPE(entityIds) { + try { + let xml = await connection.get(transaction.generateEntitiesUri(entityIds)); + return await preview.processPreviewResponse(xml); + } catch (err) { + if (err.message.startsWith("UriTooLongException")) { + let chunks = preview.halfArray(entityIds); + let first = await preview.retrievePreviewEntities(chunks[0]); + let second = await preview.retrievePreviewEntities(chunks[1]); + return first.concat(second); + } else { + throw err + } + } + } + + /** + * Split an array into two arrays. + */ + this.halfArray = function(array) { + if (array.length < 2) { + throw new Error("Could not cut this array in half. It has a length of " + array.length); + } + let half = Math.floor(array.length / 2) + return [array.slice(0, half), array.slice(half, array.length)] + } + + /** + * Transform the xml to an array of entities. + * + * @param {Promise XMLDocument} xml - The server response. + * @param {Promise XMLDocument} xsl - The xsl script. + * @return {Promise HTMLElement[]} A promise for an Array of HTMLElements. + */ + this.transformXmlToPreviews = async function _tXTP(xml, xsl) { + let html = await asyncXslt(xml, xsl); + let entities = []; + $(html).find('.caosdb-entity-panel').each((index, entity) => { + entities[index] = entity; + $(entity).toggleClass("caosdb-entity-panel", false).toggleClass(preview.classNameEntityPreview, true); + $(entity).find('.caosdb-entity-actions-panel').remove(); + }); + return entities; + } + + /** + * Get an array of entity ids from a container of entity links. + * + * @param {HTMLElement} refLinksContainer + * @return {String[]} An array of entity ids. + */ + this.getEntityIds = function(refLinksContainer) { + if (refLinksContainer == null) { + throw new Error("parameter refLinksContainer must not be null."); + } + + let entityIds = []; + preview.getReferenceLinks(refLinksContainer).each((index, link) => { + entityIds.push(getEntityId(link)); + }); + return entityIds; + } + + /** + * Get an array of all reference links. + * + * @param {HTMLElement} refLinksContainer - The original reference links. + * @return {jQuery} A collection of links. + */ + this.getReferenceLinks = function(refLinksContainer) { + return $(refLinksContainer).find('a').addBack('a').has('.caosdb-id'); + } +}; + + +$(document).ready(preview.init); \ No newline at end of file diff --git a/src/core/js/templates_ext.js b/src/core/js/templates_ext.js new file mode 100644 index 00000000..99667822 --- /dev/null +++ b/src/core/js/templates_ext.js @@ -0,0 +1,109 @@ +// Templates Extension for CaosDB WebUI +// A. Schlemmer, 11/2018 + + +var templates_ext = new function() { + + this.init = function() { + this.add_templates(); + this.add_user_templates(); + } + + this.generate_template = function(tempstring, tempquery) { + // Syntax: + // tempstring: Suche alle Kisten mit Name {text} + // tempquery: FIND Record Kiste with KistenName = "$1" + var re = /\{(.*?)\}/g; + var preparedstr = tempstring.replace(re, ' <input type="text"> </input>'); + var template1 = $('<div class="row"><div class="col-md-10"><div class="btn invisible" style="padding-left: 0px;">.</div>' + preparedstr + '</div><div class="col-md-2 text-right"><button class="btn btn-default caosdb-button-ok"> <span class="glyphicon glyphicon-wrench" aria-hidden="true"></span></button><button class="btn btn-default caosdb-button-search"> <span class="glyphicon glyphicon-search" aria-hidden="true"></span> </button></div></div>'); + var clickfun = (_) => { + var res = template1.find("input"); + var req = /\$([0-9]+)/g; + $("#caosdb-query-textarea").val(tempquery.replace(req, (match, p1) => res[p1 - 1].value)); + }; + var clickfun2 = (_) => { + clickfun(); + $(".caosdb-search-btn").find("a").click(); + }; + template1.find(".caosdb-button-ok").click(clickfun); + template1.find(".caosdb-button-search").click(clickfun2); + return template1; + } + + this.add_templates = function() { + var shortcuts_container = $('<div class="container caosdb-shortcuts-container"><span class="h3">Shortcuts</span></div>'); + $("#caosdb-query-panel").append(shortcuts_container); + + var temparray = [ + // {"help": "Suche alle Experimente der Experimentserie {text}", + // "query": "FIND Experiment with ExperimentSeries = \"$1\""}, + // {"help": "Tabelle mit Experimenten in Zeitraum {text}", + // "query": "SELECT date, species, vivoness FROM Experiment with date in $1"}, + + { + "help": "Show the box with number {text}", + "query": "FIND Record Box with Number = \"$1\"" + }, { + "help": "Generate table with boxes that have a content with {text}", + "query": "SELECT Number, Content from Box with Content like \"*$1*\"" + }, { + "help": "Find all boxes with name that contains {text}", + "query": "FIND Record Box with Number like \"*$1*\"" + }, { + "help": "Generate table with all boxes borrowed by {text} (last name)", + "query": "SELECT Number from Box with Loan with Borrower with LastName= \"$1\"" + }, + + // In the future add: , Kiste.KistenName + { + "help": "Generate Table with state of all lent boxes", + "query": "SELECT Borrower, expectedReturn, lent, returned, Box from Loan" + }, { + "help": "Show loan state for box with number {text}", + "query": "SELECT Borrower, expectedReturn, lent, returned from loan with (box with number = \"$1\")" + }, { + "help": "Show location of box in Fischereihafen Storage {text}", + "query": "SELECT Number, Site, Aisle, Level from Palette which is referenced by box with number= \"$1\"" + }, { + "help": "Find all fabrics from core {text} (name)", + "query": "find fabric which references section which references bag which references ppstrip which references core with name=\"$1\"" + }, { + "help": "Show open loan requests", + "query": "select Box, Borrower, expectedReturn, comment, destination, exhaustContents from Record Loan with (loanRequested and not loanAccepted)" + } + + + ]; + + for (var i = 0; i < temparray.length; i++) { + var tempel = temparray[i]; + shortcuts_container.append( + this.generate_template(tempel.help, tempel.query)); + } + + // templates_conf.load(); + } + + this.add_user_templates = async function() { + var temparray = await query("FIND Record UserTemplate"); + + for (var i = 0; i < temparray.length; i++) { + $(".caosdb-shortcuts-container").append( + this.generate_template(getProperty(temparray[i], "templateDescription"), + getProperty(temparray[i], "Query"))); + } + } + + this.retrieve_templates = function() { + $.ajax({ + url: "webinterface/conf/js/templates.js", + }).done(function (data) {console.log(JSON.parse(data))}); + + } +} + + + +$(document).ready(function() { + templates_ext.init(); +}); diff --git a/src/core/js/webcaosdb.js b/src/core/js/webcaosdb.js new file mode 100644 index 00000000..e9a293f6 --- /dev/null +++ b/src/core/js/webcaosdb.js @@ -0,0 +1,1137 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ +'use strict'; + +window.addEventListener('error', (e) => globalError(e.error)); + +var globalError = function(error) { + console.log(error); + var stack = error.stack; + var message = "Error! Please help to make CaosDB better! Copy this message and send it via email to t.fitschen@indiscale.com.\n\n"; + message += error.toString(); + + if (stack) { + message += '\n' + stack; + } + + window.alert(message); +} + +var globalClassNames = new function() { + this.NotificationArea = "caosdb-preview-notification-area"; + this.WaitingNotification = "caosdb-preview-waiting-notification"; + this.ErrorNotification = "caosdb-preview-error-notification"; +} + +/** + * connection module contains all ajax calls. + */ +this.connection = new function() { + this._init = function() { + /** + * Send a get request. + */ + this.get = async function _get(uri) { + try { + return await $.ajax({ + url: window.sessionStorage.caosdbBasePath + uri, + dataType: "xml", + }); + } catch (error) { + if (error.status == 414) { + throw new Error("UriTooLongException for GET " + uri); + } else if (error.status != null) { + throw new Error("GET " + uri + " returned with HTTP status " + error.status + " - " + error.statusText); + } else { + throw error; + } + } + } + + /** + * Send a put (i.e. update) request. + */ + this.put = async function _put(uri, data) { + try { + return await $.ajax({ + url: window.sessionStorage.caosdbBasePath + uri, + method: 'PUT', + dataType: "xml", + processData: false, + data: data, + contentType: 'text/xml', + }); + } catch (error) { + if (error.status != null) { + throw new Error("PUT " + uri + " returned with HTTP status " + error.status + " - " + error.statusText); + } else { + throw error; + } + } + } + + /** + * Run a script in the backend. + * @scriptname is the name of the scriptfile without leading scriptpath, e.g. test.py. + * @params is a associative array with keys being the options and values being the values. + */ + this.runScript = async function _runScript(scriptname, params) { + var pstring = "call=" + scriptname; + for (var key in params) { + pstring += "&O" + key + "=" + params[key]; + } + try { + return await $.ajax({ + url: window.sessionStorage.caosdbBasePath + "scripting", + method: 'POST', + dataType: "xml", + processData: false, + data: pstring, + contentType: 'application/x-www-form-urlencoded', + }); + } catch (error) { + if (error.status != null) { + throw new Error("POST scripting returned with HTTP status " + error.status + " - " + error.statusText); + } else { + throw error; + } + } + } + + /** + * Send a post (i.e. insert) request. + */ + this.post = async function _post(uri, data) { + try { + return await $.ajax({ + url: window.sessionStorage.caosdbBasePath + uri, + method: 'POST', + dataType: "xml", + processData: false, + data: data, + contentType: 'text/xml', + }); + } catch (error) { + if (error.status != null) { + throw new Error("POST " + uri + " returned with HTTP status " + error.status + " - " + error.statusText); + } else { + throw error; + } + } + } + + /** + * Send a delete request. + * idline: 124&256 + */ + this.deleteEntities = async function _deleteEntities(idline) { + try { + return await $.ajax({ + url: window.sessionStorage.caosdbBasePath + "Entity/" + idline, + method: 'DELETE', + dataType: "xml", + processData: false, + data: "", + contentType: 'text/xml', + }); + } catch (error) { + if (error.status != null) { + throw new Error("DELETE " + "Entity/" + idline + " returned with HTTP status " + error.status + " - " + error.statusText); + } else { + throw error; + } + } + } + + /** + * Return the base path of the server. + */ + this.getBasePath = function() { + let a = document.createElement('a'); + a.href = window.sessionStorage.caosdbBasePath; + return a.href; + } + } + this._init(); +} + +/** + * transformation module contains all code for tranforming xml into html via + * xslt. + */ +this.transformation = new function() { + /** + * remove all permission information from a server's response. + * + * @param {XMLDocument} xml + * @return {XMLDocument} without <Permissions> tags. + */ + this.removePermissions = function(xml) { + $(xml).find('Permissions').remove(); + return xml + } + + /** + * Retrieve a certain xslt script by name. + * + * @param {String} name + * @return {XMLDocument} xslt document. + */ + this.retrieveXsltScript = async function _rXS(name) { + return await connection.get("webinterface/xsl/" + name); + } + + /** + * Transform the server's response with multiple entities into their + * html representation. The parameter `xml` may also be a Promise. + * + * @param {XMLDocument} xml + * @return {HTMLElement[]} an array of HTMLElements. + */ + this.transformEntities = async function _tME(xml) { + let xsl = await this.retrieveEntityXsl(); + let html = await asyncXslt(xml, xsl); + return $(html).find('div.root > div.caosdb-entity-panel').toArray(); + } + + /** + * @param {XMLDocument} xml + * @return {HTMLElement[]} an array of HTMLElements. + */ + this.transformParent = async function _tME(xml) { + var xsl = await transformation.retrieveXsltScript("parent.xsl"); + insertParam(xsl, "entitypath", window.sessionStorage.caosdbBasePath + "Entity/"); + // TODO the following line should not have any effect. nevertheless: it + // does not work without + xsl = str2xml(xml2str(xsl)); + let html = await asyncXslt(xml, xsl); + return html; + } + + /** + * @param {XMLDocument} xml + * @return {HTMLElement[]} an array of HTMLElements. + */ + this.transformProperty = async function _tME(xml) { + var xsl = await transformation.retrieveXsltScript("property.xsl"); + insertParam(xsl, "filesystempath", window.sessionStorage.caosdbBasePath + "FileSystem/"); + insertParam(xsl, "entitypath", window.sessionStorage.caosdbBasePath + "Entity/"); + insertParam(xsl, "close-char", '×'); + var entityXsl = await transformation.retrieveXsltScript('entity.xsl'); + var messageXsl = await transformation.retrieveXsltScript('messages.xsl'); + var commonXsl = await transformation.retrieveXsltScript('common.xsl'); + var xslt = transformation.mergeXsltScripts(xsl, [messageXsl, commonXsl, entityXsl]); + let html = await asyncXslt(xml, xslt); + return html; + } + /** + * @param {XMLDocument} xml + * @return {HTMLElement[]} an array of HTMLElements. + */ + this.transformEntityPalette = async function _tME(xml) { + var xsl = await transformation.retrieveXsltScript("entity_palette.xsl"); + let html = await asyncXslt(xml, xsl); + return html; + } + + /** + * Retrieve the entity.xsl script and modify it such that we can use it + * without the context of the main.xsl. + * + * @return {XMLDocument} xslt script + */ + this.retrieveEntityXsl = async function _rEX() { + var entityXsl = await transformation.retrieveXsltScript("entity.xsl"); + insertParam(entityXsl, "filesystempath", window.sessionStorage.caosdbBasePath + "FileSystem/"); + insertParam(entityXsl, "entitypath", window.sessionStorage.caosdbBasePath + "Entity/"); + insertParam(entityXsl, "close-char", '×'); + var errorXsl = await transformation.retrieveXsltScript('messages.xsl'); + var xslt = transformation.mergeXsltScripts(entityXsl, [errorXsl]); + xslt = injectTemplate(xslt, '<xsl:template match="/"><div class="root"><xsl:apply-templates select="Response/*" mode="entities"/></div></xsl:template>'); + xslt = injectTemplate(xslt, '<xsl:template name="make-filesystem-link"> <xsl:param name="href" /> <xsl:param name="display" select="$href" /> <a> <xsl:attribute name="href"><xsl:value-of select="concat($filesystempath,$href)" /></xsl:attribute> <xsl:value-of select="$display" /> </a> </xsl:template> '); + return xslt; + } + + /** + * Merges several xsl style sheets into one. All template rules are + * appended to the main style sheet. + * + * @param {XMLDocument} xslMain, the main style sheet + * @param {XMLDocument[]} xslIncludes, array of style sheets which are to + * be included. + * @return {XMLDocument} a new style sheets with all template rules; + */ + this.mergeXsltScripts = function(xslMain, xslIncludes) { + let ret = getXSLScriptClone(xslMain); + for (var i = 0; i < xslIncludes.length; i++) { + $(xslIncludes[i].firstElementChild).find('xsl\\:template').each(function(index) { + $(ret.firstElementChild).append(this); + }); + } + return ret; + } +} + +/** + * transaction module contains all code for insertion, update and deletion of + * entities. Currently, only updates are implemented. + */ +this.transaction = new function() { + this.classNameUpdateForm = "caosdb-update-entity-form"; + + /** + * Retrieve a single entity and return its XML representation. + * + * @param {String} entityId + * @return {Element} an xml element. + */ + this.retrieveEntityById = async function _rEBI(entityId) { + let entities = await this.retrieveEntitiesById([entityId]); + return entities[0]; + } + + /** + * Retrieve multiple entities and return their XML representation. + * + * @param {String[]} entityIds, array of IDs. + * @return {Element[]} array of xml elements. + */ + this.retrieveEntitiesById = async function _rEBIs(entityIds) { + return $(await connection.get(this.generateEntitiesUri(entityIds))).find('Response [id]').toArray(); + } + + /** Sends a PUT request with an xml representation of entities and + * returns the xml document with the server response. + * + * @param {XMLDocument} xml, the entity elements surrounded by an + * <Update> tag. + * @return {XMLDocument} the server response. + */ + this.updateEntitiesXml = async function _uE(xml) { + return await connection.put("Entity/", xml); + } + + /** Sends a POST request with an xml representation of entities and + * returns the xml document with the server response. + * + * @param {XMLDocument} xml, the entity elements surrounded by an + * <Insert> tag. + * @return {XMLDocument} the server response. + */ + this.insertEntitiesXml = async function _iE(xml) { + return await connection.post("Entity/", xml); + } + + this.deleteEntities = async function _dE(idlist) { + return await connection.deleteEntities(idlist.join("&")); + } + + /** + * Generate the URI for the retrieval of a list of entities. + * + * @param {String[]} entityIds - An array of entity ids.. + * @return {String} The uri. + */ + this.generateEntitiesUri = function(entityIds) { + return "Entity/" + entityIds.join("&"); + } + + /** + * Submodule for update transactions. + */ + this.update = new function() { + /** + * Create a form for updating entities. It has only a textarea and a + * submit button. + * + * The 'entityXmlStr' contains the entity in it's current (soon-to-be + * old) version which can then be modified by the user. + * + * The 'putCallback' is a function which accepts one parameter, a + * string representation of an xml document. + * + * @param {String} entityXmlStr, the old entity + * @param {function} putCallback, the function which sends a put request. + */ + this.createUpdateForm = function(entityXmlStr, putCallback) { + // check the parameters + if (putCallback == null) { + throw new Error("putCallback function must not be null."); + } + if (typeof putCallback !== "function" || putCallback.length !== 1) { + throw new Error("putCallback is to be a function with one parameter."); + } + if (entityXmlStr == null) { + throw new Error("entityXmlStr must not be null"); + } + + // create the form element by element + let textarea = $('<div class="form-group"><textarea rows="8" style="width: 100%;" name="updateXml"/></div>'); + textarea.find('textarea').val(entityXmlStr); + let submitButton = $('<button class="btn btn-default" type="submit">Update</button>'); + let resetButton = $('<button class="btn btn-default" type="reset">Reset</button>'); + let form = $('<form class="panel-body"></form>'); + form.toggleClass(transaction.classNameUpdateForm, true); + form.append(textarea); + form.append(submitButton); + form.append(resetButton); + + // reset restores the original xmlStr + form.on('reset', function(e) { + textarea.find('textarea').val(entityXmlStr); + return false; + }); + + // submit calls the putCallback + form.submit(function(e) { + putCallback(e.target.updateXml.value); + return false; + }); + + return form[0]; + } + + /** + * Start the update of a single entity. Returns a state machine which + * 1) removes entity from page. show waiting notification instead. + * 2) retrieves the old version's xml + * 3) shows update form with old xml + * 4) submits new xml and handles success and failure of the update. + * + * @param {HTMLElement} entity, the div which represent the entity. + * @return {Object} a state machine. + */ + this.updateSingleEntity = function(entity) { + let updatePanel = transaction.update.createUpdateEntityPanel(transaction.update.createUpdateEntityHeading($(entity).find('.caosdb-entity-panel-heading')[0])); + var app = new StateMachine({ + transitions: [{ + name: "init", + from: 'none', + to: "waitRetrieveOld" + }, { + name: "openForm", + from: ['waitRetrieveOld', 'waitPutEntity'], + to: 'modifyEntity' + }, { + name: "submitForm", + from: 'modifyEntity', + to: 'waitPutEntity' + }, { + name: "showUpdatedEntity", + from: 'waitPutEntity', + to: 'final' + }, { + name: "resetApp", + from: '*', + to: 'final' + }, ], + }); + app.errorHandler = function(fn) { + try { + fn(); + } catch (e) { + setTimeout(() => app.resetApp(e), 1000); + } + } + app.onInit = function(e, entity) { + // remove entity + $(entity).hide(); + app.errorHandler(() => { + // show updatePanel instead + $(updatePanel).insertBefore(entity); + // create and add waiting notification + updatePanel.appendChild(transaction.update.createWaitRetrieveNotification()); + let entityId = getEntityId(entity); + transaction.update.retrieveOldEntityXmlString(entityId).then(xmlstr => { + app.openForm(xmlstr); + }, err => { + app.resetApp(err); + }); + }); + // retrieve old xml, trigger state change when response is ready + }; + app.onOpenForm = function(e, entityXmlStr) { + app.errorHandler(() => { + // create and show Form + let form = transaction.update.createUpdateForm(entityXmlStr, (xmlstr) => { + app.submitForm(xmlstr); + }); + updatePanel.append(form); + }); + }; + app.onResetApp = function(e, error) { + $(entity).show(); + $(updatePanel).remove(); + if (error != null) { + globalError(error); + } + }; + app.onShowUpdatedEntity = function(e, newentity) { + // remove updatePanel + updatePanel.remove(); + // show new version of entity + $(newentity).insertBefore(entity); + // remove old version + $(entity).remove(); + }; + app.onSubmitForm = function(e, xmlstr) { + // remove form + $(updatePanel).find('form').remove(); + + $(updatePanel).find('.' + globalClassNames.ErrorNotification).remove(); + + // send HTTP PUT to update entity + app.errorHandler(() => { + transaction.updateEntitiesXml(str2xml('<Update>' + xmlstr + '</Update>')).then( + xml => { + if ($(xml.firstElementChild).find('Error').length) { + // if there is an <Error> tag in the response, show + // the response in a new form. + app.openForm(xml2str(xml)); + transaction.update.addErrorNotification($(updatePanel).find('.panel-heading'), transaction.update.createErrorInUpdatedEntityNotification()); + } else { + // if there are no errors show the XSL-transformed + // updated entity. + transformation.transformEntities(xml).then(entities => { + app.showUpdatedEntity(entities[0]); + }, err => { + // errors from the XSL transformation + app.resetApp(err); + }); + } + }, err => { + // errors from the HTTP PUT request + app.resetApp(err); + } + ); + }); + }; + app.onLeaveWaitPutEntity = function() { + // remove waiting notifications + removeAllWaitingNotifications(updatePanel); + }; + app.onLeaveWaitRetrieveOld = app.onLeaveWaitPutEntity; + + app.init(entity); + app.updatePanel = updatePanel; + + let closeButton = transaction.update.createCloseButton('.panel', () => { + app.resetApp(); + }); + $(updatePanel).find('.panel-heading').prepend(closeButton); + return app; + } + + /** + * Retrieves an entity and returns it's untransformed entity id. + * + * @param {String} entityId, an entity id. + * @return {String} the xml of the entity. + */ + this.retrieveOldEntityXmlString = async function _retrieveOEXS(entityId) { + let xml = await transaction.retrieveEntityById(entityId);; + return xml2str(transformation.removePermissions(xml)); + } + + this.createWaitRetrieveNotification = function() { + return createWaitingNotification("Retrieving xml and loading form. Please wait."); + } + + this.createWaitUpdateNotification = function() { + return createWaitingNotification("Sending update to the server. Please wait."); + } + + this.createErrorInUpdatedEntityNotification = function() { + return createErrorNotification("The update was not successful."); + } + + this.addErrorNotification = function(elem, err) { + $(elem).append(err); + return elem; + } + + /** + * Create a panel where the waiting notifications and the update form + * is to be shown. This panel will be inserted before the old entity. + * + * @param {HTMLElement} heading, the heading of the panel. + * @return {HTMLElement} A div. + */ + this.createUpdateEntityPanel = function(heading) { + let panel = $('<div class="panel panel-default" style="border-color: blue;"/>'); + panel.append(heading); + return panel[0]; + }; + + /** + * Create a heading for the update panel from the heading of an entity. + * Basically, this copies the heading, removes everything but the first + * element, and adds a span that indicates that one is currently + * updating something. + * + * @param {HTMLElement} entityHeading, the heading of the entity. + * @return {HTMLElement} the heading for the update panel. + */ + this.createUpdateEntityHeading = function(entityHeading) { + let heading = entityHeading.cloneNode(true); + let update = $('<span><h3>Update</h3></span>')[0]; + $(heading).children().slice(1).remove(); + $(heading).prepend(update); + $(heading).find('.caosdb-backref-link').remove(); + return heading; + } + + /** + * Get the heading from an entity panel. + * + * @param {HTMLElement} entityPanel, the entity panel. + * @return {HTMLElement} the heading. + */ + this.getEntityHeading = function(entityPanel) { + return $(entityPanel).find('.caosdb-entity-panel-heading')[0]; + } + + this.initUpdate = function(button) { + transaction.update.updateSingleEntity( + $(button).closest('.caosdb-entity-panel')[0] + ); + } + + this.createCloseButton = function(close, callback) { + let button = $('<button title="Cancel update" class="btn btn-link close" aria-label="Cancel update">×</button>'); + button.bind('click', function() { + $(this).closest(close).hide(); + callback(); + }); + return button[0]; + } + } +} + + +var paging = new function() { + /** + * It reads the current page from the current uri's query segment (e.g. + * '?P=0L10') and sets the correct links of the PREV/NEXT buttons or + * hides them. + * + * This is called in main.xsl + * + * @param totalEntities, + * the total number of entities in a response (which is >= + * the number of entities which are currently shown on this + * page. + */ + this.initPaging = function(href, totalEntities) { + + if (totalEntities == null) { + return false; + } + + // get links for the next/prev page + var oldPage = paging.getPSegmentFromUri(href); + if (oldPage == null) { + // no paging is to be done -> this is a success after all. + return true; + } + var nextHref = paging.getPageHref(href, paging.getNextPage(oldPage, totalEntities)); + var prevHref = paging.getPageHref(href, paging.getPrevPage(oldPage)); + + if (nextHref != null) { + // set href and show next button + $('.caosdb-next-button').attr("href", nextHref); + $('.caosdb-next-button').show(); + $('.caosdb-paging-panel').show(); + } else { + $('.caosdb-next-button').hide(); + } + if (prevHref != null) { + // set href and show prev button + $('.caosdb-prev-button').attr("href", prevHref); + $('.caosdb-prev-button').show(); + $('.caosdb-paging-panel').show(); + } else { + if (prevHref == nextHref) { + $('.caosdb-paging-panel').hide(); + } + $('.caosdb-prev-button').hide(); + } + + return true; + + } + + /** + * Replace the old page string in the given uri or concat it if there was no + * page string. If page is null return null. + * + * @param uri_old, + * the old uri + * @param page, + * the page the new uri shall point to + * @return a string uri which points to the page denotes by the parameter + */ + this.getPageHref = function(uri_old, page) { + if (uri_old == null) { + throw new Error("uri was null."); + } + if (page == null) { + return null; + } + + var pattern = /(\?(.*&)?)P=([^&]*)/; + if (pattern.test(uri_old)) { + // replace existing P=... + return uri_old.replace(pattern, "$1P=" + page); + } else if (/\?/.test(uri_old)) { + // append P=... to existing query segment + return uri_old + "&P=" + page; + } else { + // no query segment so far + return uri_old + "?P=" + page; + } + } + + /** + * @param uri + * @return The first part of the query segment which begins with 'P=' + */ + this.getPSegmentFromUri = function(uri) { + if (uri == null) { + throw new Error("uri was null."); + } + var pattern = /\?(.*&)?P=([^&]*)/; + var ret = pattern.exec(uri); + return (ret != null ? ret[2] : null); + } + + /** + * Construct a page string for the previous page. Returns null, if there is + * no previous page. Throws exceptions if P is null or has a wrong format. + * + * @param P, + * a paging string + * @return a String + */ + this.getPrevPage = function(P) { + if (P == null) { + throw new Error("P was null"); + } + + var pattern = /^([0-9]+)L([0-9]+)$/ + var match = pattern.exec(P); + if (match == null) { + throw new Error("P didn't match " + pattern.toString()); + } + + var index_old = parseInt(match[1]); + if (index_old == 0) { + // no need for a prev button + return null; + } + + var length = parseInt(match[2]); + var index = Math.max(index_old - length, 0); + + return index + "L" + length; + } + + /** + * Construct a page string for the next page. Returns null if there is no + * next page. Throws exceptions if n or P is null or if n isn't an integer >= + * 0 or if P doesn't have the correct format. + * + * @param P, + * a paging string + * @param n, + * total numbers of entities. + * @return a String + */ + this.getNextPage = function(P, n) { + // check n and P for null values and correct formatting + if (n == null) { + throw new Error("n was null"); + } + if (P == null) { + throw new Error("P was null"); + } + n = parseInt(n); + if (isNaN(n) || !(typeof n === 'number' && (n % 1) === 0 && n >= 0)) { + throw new Error("n is to be an integer >= 0!"); + } + var pattern = /^([0-9]+)L([0-9]+)$/ + var match = pattern.exec(P); + if (match == null) { + throw new Error("P didn't match " + pattern.toString()); + } + + var index_old = parseInt(match[1]); + var length = parseInt(match[2]); + var index = index_old + length; + + if (index >= n) { + // no need for a next button + return null; + } + return index + "L" + length; + } +}; + +var queryForm = new function() { + this.init = function(form) { + this.restoreLastQuery(form, () => window.sessionStorage.lastQuery); + this.bindOnClick(form, (set) => { + window.sessionStorage.lastQuery = set; + return null; + }); + }; + + this.restoreLastQuery = function(form, getter) { + if (form == null) { + throw new Error("form was null"); + } + if (getter()) { + form.query.value = getter(); + } + }; + + this.bindOnClick = function(form, setter) { + if (setter == null || typeof(setter) !== 'function' || setter.length !== 1) { + throw new Error("setter must be a function with one param"); + } + form.getElementsByClassName("caosdb-search-btn")[0].onclick = function() { + // store current query + var queryField = form.query; + var value = queryField.value.toUpperCase(); + if (typeof value == "undefined" || value.length == 0) { + return; + } + if (!(value.startsWith("FIND") || value.startsWith("COUNT") || value.startsWith("SELECT"))) { + queryField.value = "FIND ENTITY WHICH HAS A PROPERTY LIKE '*" + queryField.value + "*'"; + } + setter(queryField.value); + + // remove paging for select queries + if (queryForm.isSelectQuery(queryField.value)) { + queryForm.removePagingField(form); + } + + form.submit(); + }; + }; + + /** + * Is the query a SELECT field,... FROM entity query? + * + * @param {HTMLElement} query, the query to be tested. + * @return {Boolean} + */ + this.isSelectQuery = function(query) { + return query.toUpperCase().startsWith("SELECT"); + } + + /** + * Remove the (hidden) paging input from the query form. + * The form is changed in-place without copying it. + * + * @param {HTMLElement} form, the query form. + * @return {HTMLElement} the form without the paging input. + */ + this.removePagingField = function(form) { + $(form.P).remove(); + return form; + } +}; + + +this.markdown = new function() { + this.toHtml = function(textElement) { + if ($(textElement).hasClass('markdowned')) { + return textElement; + } + $(textElement).toggleClass('markdowned', true); + + $(textElement).find(".caosdb-comment-annotation-text").each(function() { + let converter = new showdown.Converter(); + let text = $(this).html(); + let html = converter.makeHtml(text.trim()); + $(this).html(html); + }); + $(textElement).find(".media-body:not(:has(.media-heading))").each(function() { + $('<h4 class="media-heading">You<small><i> just posted</i></small></h4>').prependTo($(this)); + }); + return textElement; + + } +} + +var hintMessages = new function() { + this.init = function() { + for (var entity of $('.caosdb-entity-panel')) { + this.hintMessages(entity); + } + } + + this.removeMessages = function(entity) { + $(entity).find(".alert").remove(); + } + + this.unhintMessages = function(entity) { + $(entity).find(".caosdb-f-message-badge").remove(); + $(entity).find(".alert").show(); + } + + this.hintMessages = function(entity) { + this.unhintMessages(entity); + var messageType = { + "info": "info", + "warning": "warning", + "danger": "error" + }; + for (let alrt in messageType) { + $(entity).find(".alert.alert-" + alrt).each(function(index) { + var messageElem = $(this); + if (messageElem.parent().find(".caosdb-f-message-badge.alert-" + alrt).length == 0) { + messageElem.parent('.caosdb-messages, .caosdb-property-row').prepend('<button title="Click here to show the ' + messageType[alrt] + ' messages of the last transaction." class="btn caosdb-f-message-badge badge alert-' + alrt + '">' + messageType[alrt] + '</button>'); + messageElem.parent().find(".caosdb-f-message-badge.alert-" + alrt).on("click", function(e) { + $(this).hide(); + console.log(this); + console.log(alrt); + messageElem.parent().find('.alert.alert-' + alrt).show() + }); + } + messageElem.hide(); + }); + } + + if ($(entity).find(".caosdb-messages > .caosdb-f-message-badge").length > 0) { + var div = $('<div class="text-right" style="padding: 5px 16px;"/>'); + div.prependTo($(entity).find(".caosdb-messages")); + var messageBadges = $(entity).find(".caosdb-messages > .caosdb-f-message-badge"); + messageBadges.detach(); + div.append(messageBadges); + } + $(entity).find(".caosdb-property-row > .caosdb-f-message-badge").addClass("pull-right"); + } +} + + +function createErrorNotification(msg) { + return $('<div class="' + globalClassNames.ErrorNotification + '">' + msg + '<div>')[0]; +} + +/** + * Create a waiting notification with a informative message for the waiting user. + * + * @param {String} info, a message for the user + * @return {HTMLElement} A div with class `caosdb-preview-waiting-notification`. + */ +function createWaitingNotification(info) { + return $('<div class="' + globalClassNames.WaitingNotification + '">' + info + '</div>')[0]; +} + +/** + * Remove all waiting notifications from an element. + * + * @param {HTMLElement} elem + * @return {HTMLElement} The parameter `elem`. + */ +function removeAllWaitingNotifications(elem) { + $(elem.getElementsByClassName(globalClassNames.WaitingNotification)).remove(); + return elem; +} + +/** + * Extract the ID of an entity by parsing the textContent of the first occuring element with + * class `caosdb-id`. + * + * @param {HTMLElement} entity + * @returns {Number} ID of entity. + */ +function getEntityId(entity) { + let id = Number.parseInt(entity.getElementsByClassName("caosdb-id")[0].textContent); + if (isNaN(id)) throw new Error("id was NaN"); + return id; +} + +/** + * Post an xml document to basepath/Entity + * + * @param xml, + * XML document + * @param basepath, + * string + * @param querySegment, + * string + * @param timeout, + * integer (in milliseconds) + * @return Promise object + */ +function postXml(xml, basepath, querySegment, timeout) { + return $.ajax({ + type: 'POST', + contentType: 'text/xml', + url: basepath + "Entity/" + (querySegment == null ? "" : querySegment), + processData: false, + data: xml, + dataType: 'xml', + timeout: timeout, + statusCode: { + 401: function() { + throw new Error("unauthorized"); + }, + }, + }); +} + +/** + * Serialize an xml document into plain string. + * + * @param xml + * @return string representation of xml + */ +function xml2str(xml) { + return new XMLSerializer().serializeToString(xml); +} + +/** + * Convert a string into an xml document. + * + * @param s, + * a string representation of an xml document. + * @return an xml document. + */ +function str2xml(s) { + var parser = new DOMParser(); + return parser.parseFromString(s, "text/xml"); +} + +/** + * Asynchronously transform an xml into html via xslt. + * + * @param xmlPromise, + * resolves to an input xml document. + * @param xslPromise, + * resolves to a xsl script. + * @param paramsPromise, + * resolves to parameters for the xsl script. + * @return A promise which resolves to a generated HTML document. + */ +async function asyncXslt(xmlPromise, xslPromise, paramsPromise) { + var xml, xsl, params; + [xml, xsl, params] = await Promise.all([xmlPromise, xslPromise, paramsPromise]); + return xslt(xml, xsl, params); +} + +/** + * transform a xml into html via xslt + * + * @param xml, + * the input xml document + * @param xsl, + * the transformation script + * @param params, + * xsl parameter to be set (optionally). + * @return html + */ +function xslt(xml, xsl, params) { + var xsltProcessor = new XSLTProcessor(); + if (params) { + for (var k in params) { + if (params.hasOwnProperty(k)) { + xsltProcessor.setParameter(null, k, params[k]); + } + } + } + if (typeof xsltProcessor.transformDocument == 'function') { + // old FFs + var retDoc = document.implementation.createDocument("", "", null); + xsltProcessor.transformDocument(xml, xsl, retDoc, null); + return retDoc.documentElement; + } else { + // modern browsers + xsltProcessor.importStylesheet(xsl); + return xsltProcessor.transformToFragment(xml, document); + } +} + +/** + * TODO + */ +function getXSLScriptClone(source) { + return str2xml(xml2str(source)) +} + +/** + * TODO + */ +function injectTemplate(orig_xsl, template) { + var xsl = getXSLScriptClone(orig_xsl); + var entry_t = xsl.createElement("xsl:template"); + xsl.firstElementChild.appendChild(entry_t); + entry_t.outerHTML = template; + return xsl; +} + +/** + * TODO + */ +function insertParam(xsl, name, value = null) { + var param = xsl.createElement("xsl:param"); + param.setAttribute("name", name); + if (value != null) { + param.setAttribute("select", "'" + value + "'"); + } + xsl.firstElementChild.append(param); +} + +/** + * When the page is scrolled down 100 pixels, the scroll-back button appears. + * + * @return + */ + +/** + * Every initial function calling is done here. + * + * @return + */ +function initOnDocumentReady() { + hintMessages.init(); + + // init query form + var form = document.getElementById("caosdb-query-form"); + if (form != null) { + queryForm.init(form); + } + + // show image 100% width + $(".entity-image-preview").click(function() { + $(this).css('width', '100%'); + $(this).css('max-width', ""); + $(this).css('max-height', ""); + }); + +} + +$(document).ready(initOnDocumentReady); diff --git a/src/core/owner.css b/src/core/owner.css new file mode 100644 index 00000000..1a6126b9 --- /dev/null +++ b/src/core/owner.css @@ -0,0 +1,35 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ + + +.owners { + border: 1px solid lightgray; + background-color: lightgray; + padding: 1px; +} + +.owner { + display: inline; + background-color: white; + padding: 1px; +} diff --git a/src/core/owner.xsl b/src/core/owner.xsl new file mode 100644 index 00000000..2eb2136b --- /dev/null +++ b/src/core/owner.xsl @@ -0,0 +1,73 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header +--> +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + <xsl:output method="html" /> + + <xsl:variable name="base_uri"> + <xsl:call-template name="uri_ends_with_slash"> + <xsl:with-param name="uri" select="/Response/@baseuri" /> + </xsl:call-template> + </xsl:variable> + + <xsl:template match="/"> + <html> + <head> + <link rel="stylesheet" type="text/css"> + <xsl:attribute name="href"> + <xsl:value-of + select="concat($base_uri, 'webinterface/owner.css')" /> + </xsl:attribute> + </link> + + </head> + <body> + <xsl:apply-templates select="Response/*" /> + </body> + </html> + </xsl:template> + + <xsl:template match="Entity|Property|Record|RecordType"> + <div class="owners"> + Owners: + <xsl:for-each select="Owner"> + <div class="owner"><xsl:value-of select="@role"></xsl:value-of></div> + </xsl:for-each> + </div> + </xsl:template> + + <!-- assure that this uri ends with a '/' --> + <xsl:template name="uri_ends_with_slash"> + <xsl:param name="uri" /> + <xsl:choose> + <xsl:when test="substring($uri,string-length($uri),1)!='/'"> + <xsl:value-of select="concat($uri,'/')" /> + </xsl:when> + <xsl:otherwise> + <xsl:value-of select="$uri" /> + </xsl:otherwise> + </xsl:choose> + </xsl:template> +</xsl:stylesheet> + diff --git a/src/core/permissions.css b/src/core/permissions.css new file mode 100644 index 00000000..bca557c0 --- /dev/null +++ b/src/core/permissions.css @@ -0,0 +1,44 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ + + +.entity_acl { + border: 1px solid #000000; +} + +.Grant, .Deny { + border: 1px solid lightgray; + display: table-row; +} + +.role, .permission { + display: table-cell; +} + +.Grant td { + background-color: lightgreen; +} + +.Deny td { + background-color: lightcoral; +} diff --git a/src/core/permissions.xsl b/src/core/permissions.xsl new file mode 100644 index 00000000..25668e5d --- /dev/null +++ b/src/core/permissions.xsl @@ -0,0 +1,166 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header +--> +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + <xsl:output method="html" /> + + <xsl:variable name="base_uri"> + <xsl:call-template name="uri_ends_with_slash"> + <xsl:with-param name="uri" select="/Response/@baseuri" /> + </xsl:call-template> + </xsl:variable> + + <xsl:template match="/"> + <html> + <head> + <link rel="stylesheet" type="text/css"> + <xsl:attribute name="href"> + <xsl:value-of + select="concat($base_uri, 'webinterface/permissions.css')" /> + </xsl:attribute> + </link> + + </head> + <body> + <xsl:apply-templates select="Response/*" /> + </body> + </html> + </xsl:template> + + <xsl:template match="Property|Record|RecordType"> + <xsl:call-template name="perm_table"> + <xsl:with-param name="for"> + <xsl:value-of select="name()" /> + <xsl:text> </xsl:text> + <xsl:value-of select="@id"></xsl:value-of> + </xsl:with-param> + <xsl:with-param name="entity_id" select="@id" /> + </xsl:call-template> + </xsl:template> + + <xsl:template match="EntityPermissions"> + <xsl:call-template name="perm_table"> + <xsl:with-param name="for"> + all entities + </xsl:with-param> + </xsl:call-template> + </xsl:template> + + <xsl:template name="perm_table"> + <xsl:param name="for"></xsl:param> + <xsl:param name="entity_id"></xsl:param> + <table class="entity_acl"> + <caption> + Permissions for + <xsl:value-of select="$for" /> + </caption> + <tr> + <th /> + <th class="role">Role</th> + <xsl:for-each select="//EntityPermissions/Permission"> + <th class="permission"> + <xsl:value-of select="@name" /> + </th> + </xsl:for-each> + </tr> + <xsl:apply-templates select="EntityACL/Grant|EntityACL/Deny" + mode="table_row"> + <xsl:with-param name="entity_id" select="$entity_id" /> + </xsl:apply-templates> + </table> + </xsl:template> + + <xsl:template match="Grant|Deny" mode="table_row"> + <xsl:param name="entity_id" /> + <xsl:param name="role" select="@role" /> + <xsl:param name="g_or_d" select="name()" /> + + <tr> + <xsl:attribute name="class"> + <xsl:value-of select="name()" /> + <xsl:if test="@priority='true'"> + <xsl:text> priority</xsl:text> + </xsl:if> + </xsl:attribute> + <td> + <xsl:value-of select="name()" /> + </td> + <td class="role"> + <xsl:value-of select="$role" /> + </td> + <xsl:for-each select="//EntityPermissions/Permission"> + <td class="permission"> + <input type="checkbox"> + <xsl:call-template name="is_checked"> + <xsl:with-param name="pname" + select="@name" /> + <xsl:with-param name="entity_id" + select="$entity_id" /> + <xsl:with-param name="role" + select="$role" /> + <xsl:with-param name="g_or_d" + select="$g_or_d" /> + </xsl:call-template> + </input> + </td> + </xsl:for-each> + + </tr> + </xsl:template> + + <xsl:template name="is_checked"> + <xsl:param name="pname" /> + <xsl:param name="entity_id" /> + <xsl:param name="role" /> + <xsl:param name="g_or_d" /> + <xsl:choose> + <xsl:when test="$entity_id"> + <xsl:if + test="/Response/*[@id=$entity_id]/EntityACL/*[name()=$g_or_d][@role=$role]/Permission[@name=$pname]"> + <xsl:attribute name="checked"></xsl:attribute> + </xsl:if> + </xsl:when> + <xsl:otherwise> + <xsl:if + test="/Response/EntityPermissions/EntityACL/*[name()=$g_or_d][@role=$role]/Permission[@name=$pname]"> + <xsl:attribute name="checked"></xsl:attribute> + </xsl:if> + </xsl:otherwise> + </xsl:choose> + </xsl:template> + + <!-- assure that this uri ends with a '/' --> + <xsl:template name="uri_ends_with_slash"> + <xsl:param name="uri" /> + <xsl:choose> + <xsl:when test="substring($uri,string-length($uri),1)!='/'"> + <xsl:value-of select="concat($uri,'/')" /> + </xsl:when> + <xsl:otherwise> + <xsl:value-of select="$uri" /> + </xsl:otherwise> + </xsl:choose> + </xsl:template> +</xsl:stylesheet> + diff --git a/src/core/pics/caosdb_logo_42.png b/src/core/pics/caosdb_logo_42.png new file mode 100644 index 0000000000000000000000000000000000000000..e0c925f41c405c049c0af2e29a782df69e7105f2 GIT binary patch literal 2310 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 literal 0 HcmV?d00001 diff --git a/src/core/pics/caosdb_logo_medium.png b/src/core/pics/caosdb_logo_medium.png new file mode 100644 index 0000000000000000000000000000000000000000..34f1f632116748818058e3afee64cea4520c17f9 GIT binary patch literal 1940 zcmeAS@N?(olHy`uVBq!ia0y~yVDM#NV6fp}V_;xVE;qlzz`(##?Bp53!NI{%!;#X# zz`(#+;1OBOz`*qbgc;M+Z2mGZFi4iTMwA5Sr<If^7Ns(jmzV2h=4BTrCl;jY<rk&T zerF@az`!o(>EaktG3V{=%AUEQl58LD>*%KD?DARObUC$4By*!ItH>I!-Wy`#Ar~B) zeu)e9?rz($;BR#D6bBPgUl&!wMLQJSdO0JeZ+jrLHr8>;#-Of*OV`9Ab0Ysd{+aiF z?(4bd_Le`}R=(`Biu&bwziXdYpReBby!M=>Pm<8&87_xsR4w9_;FCT6Q2O#liO(OJ zy7vFI;5GiBWA!m6@p5qm(|PSv8c(Osvu<(!Xz;IduV}sef%>S(g&WImCGBB3-MvM` z|6$$A2hJ5sUcDFgty#|gxzI|i<FmL&@*a-M1^U}WU+Ukunlkqv|Kidp`M7q|3e~di z#^|Fr_CGqIvHv2^vV=~PI|^}Kj5&wq?%@7COLvdt^0UW7D|Yit-Oc4}_OM`cn+*G$ zrtrkACSNxHV9cAEcvmQ<Xa0llKQ{1me(bsYQSc9A`Gc*=kw3Wph}}P~eQeVMvqM}j zOZOjZU99;cq-LeYi#->kO#MC>y;Lyw`YY`uZ}#Z1QT|+?7e~}LFc!b6&CL3E=yp`f zm5PI+mtHRm`F|nw@xy-3z&~Eyy;<dNj?6q{{a*6r!?lMlsUP&oS$S^Fy!BqT?*mSs zm6W&8u#+-UT0Qq0&#rEh&NJzAA5CAp!gTrGfTovcj~`r7c6_($on6IC*ng+QN#|^J z&Fw2b%D?yPz7I)n4y+1`yz(rf`R$@hCM~B`8UAl#+hcpqko~pG%$;j4P7gmlXB*3P zj`W>kn~z;MP<BYCv##t>*2?c`ADv?@oVRTF*R^Nnk0m@e*FXAWX!OnHam~u}e1%b} z_HV`Q=LGsYZhvt0@Skg{hrg9&PL=*3W$M&xc2s2JceaiH%U1g9iaGxIu=drND5WQU zbAsC$Z7Xc=DHWYk7FwNqXnv2Aq^$E9!PaM2cX&McFsbevx3c$J*+>0Lug;wMLTG1I zr7)Y`0k1%J_SRi*CWdB+|7&FLE?Lm|sr~SqN4J?>t%Of^?fEOjmMbFo=%CuE`)Wl$ z1Dm2(FaPVO^vBk>W`PuU%RZfBhq_F5|M<HhB&Yu4duP#~{ipLnGPU#DJg#j{HPwGA zACT8$`(yT(zBHMTUd9@Ct3!cTif&EabZ1{OkDZm!GA-+c%Qq~)#`QP<Y3|-hlcV=0 zUT%E<$n4LCdE)!}&Rg02Ui7wuuSPCD`^i$xO(OT7JyG1IdS>F^rqVV+J-11l(od`W zvOBDQTA20jj|LqH<F5bLWik(2zExYulby2snNDP=y_QS4QsS}|S7*1LSa(1*=>P0j zx=*-nI;?e*UpIN`%d{A++y5U6UjCrA@;G0O*IVBy(MRQ#AIUA`I=nFYN`L7N{`-fk zTLreiK6Fu$<*RMd``S(Hn@(+Hz23TGcUa=oj45}@9y}GA71Of$>4TGLid%0#6*+Hw zSo8u@3FpCBJ(-U&nHPSi?)%02Pvf}IE63O;-{O2<c!)lot1Rev^2O#4Tjoe<30`QQ z^7C4q;tGGR@2g6ER=Ib)HHkdyeP><xxoJ96E*kn*ontn7v#oQ&j^Mw3tJa<83r$Sb zU4B=|U7+sP5%)Id?JL;0H+m!;xYuRkt@gxkq4njZsk>E-G-TIUD$JHuO@BXmM)`J= z6LFafHXe%ZF8S)ge{E8mpW7L6d!1F^TSJYu&%LYEd`s3_<--0Q7sd6b&v^e=&2FRa z!ss0@94GFPI%G7HLI3dAO}F+q{toK8xJWo(MD{~>(EGQWCR}Z`lRP~&y29J#;GHJ^ zSr&l@eQG?yZsdE}dQ0eh`u$4tMb%A}wXM&Sa|3Uj?|S~sEd4#_;h38`zjj_pUVCM= z*t&z_tBl(x?`H4aQ=&ilal~{k-x~@=-|rqed5iV0lF~Yno4-3Qtdu_~U?aBc=*Htu zGGh-tlfD}}KTu*z&^PX9oSk>oiXY#f`t;p@vxqWV3s;K=4FStiPfhwB7r5BGBkA&s zhx2wRHR%TZY~~e<>-~A^7(d5-NrA233Kf_Ao}%1zyzBeCDZANKeS+;ySvq$VtY^7V zE2Jwk&FyCFk{}a<Tfw(Cf6(SU+P7%h<#kgHQr~LWSLl3^kP`fIN<d%NV0qW(c|z(! zzdmnS`0)C^uKUrA=TDxUA?;pO(8aPfeA|T?9~)LLnSQnQ&6;qvudnCW9NE^QR}*D* z?oAXw&+<F(#IKgElj+>Qfc4l`WxbHHYpQM=?W-BDJwER0DEzjgX89bE9nAYpjy(?v z<H)|Jc(;7dYK6Z)dagW|mfF#K*+<8c`~4y7)JZY=$F<F+yhLyDUUrF%Qx!ke_u#f` zR!3Z~8N+*Bo(SD1-xB{s{+##JPyJciy%1;7C!e1%-E_-*@YN{ayme!kuGq=c)`LpA zqIsdaRHAn=##DN4n;CuJ@>Knk=fd}GyrVxoW%ts9+r%=vC)_tY_;Es2Rm$SUCCocE zZCc*Ryi3(*<(^5gRt&+FqPxAN9vc2R9q`;<YvFCZvv<Sh31r;v{y%xf5tUnF|Cuw^ X*6mJO8<54oz`)??>gTe~DWM4fx*Nh~ literal 0 HcmV?d00001 diff --git a/src/core/pics/caosdb_logo_small.png b/src/core/pics/caosdb_logo_small.png new file mode 100644 index 0000000000000000000000000000000000000000..186a1b2e302e64c072a1b0e0c599cfeaf7a3c0e4 GIT binary patch literal 583 zcmeAS@N?(olHy`uVBq!ia0y~yV31;9U=ZP8V_;yI?4Y}kfq{Xg*vT`5gM)*kh9jke zfq{Xuz$3Dlfq{7i2s3&`i8C=UFi4iTMwA5Sr<If^7Ns(jmzV2h=4BTrCl;jY<rk&T zerF@az`%IK)5S5wqx0xhfB)b>0oD)k*-M)O1G)~mr7nzJ;cfEIJ7$IF!6W_)?qA%H zkh{q3;M`3N=C8P1d<})xG?W%TU%GxtZk)(h75(>T_f)5!n_K-Z=AWP!^FP_6u08F4 zh1);czEQs6X!+)R#Idl6F3Sy?%X}(&^;tdVA9<<}aY)Imig(W8txb;I52}v5Ib<ti ze?Udjt#G<uE&s#U@g7?`z3v&dwo6zYP;stk@w7^sanS6X$Li>VukzJ)G+2H6z<4;~ z)U?pUO!r!9<u*@RVm#%dlkpqFkm{buqc!KQpV++Ou^Z#I&^aB;qZ0m{?YO%&(9~wa zmj6Lfzv{L;l5F8xm!R~fw`NhMP2c;%I}^WYX`in0xp66W*F+8r$$dovHLcGdW+$lX zx2fIxmRK3}N=n&6Y+wGQhHD$IrN_lu&G(L6q<QDKyUo;J*XA#1i~cz8c+|8NiYwQt zZ0)=FVfzQZ_Zx4kXRtmm2y<8Py}LB}#J3Fdru;`Y_bFW7p<DXScJAiu)4mm|pRjr9 zn6^gzprz-d=5>9GG(0cOJ8S)E#lx%(cc-oE^k;4Me=z9^=c$T`^Q}AnZYcS9wIn6_ pVE>Q#`l;4Udaq-8{<H6A_!b`BxMZ5@Y6b=d22WQ%mvv4FO#lUk2f6?N literal 0 HcmV?d00001 diff --git a/src/core/pics/caosdb_no_undertitle.png b/src/core/pics/caosdb_no_undertitle.png new file mode 100644 index 0000000000000000000000000000000000000000..d324bbe1740ac768611d081a9608ec9b39718bae GIT binary patch literal 11918 zcmeAS@N?(olHy`uVBq!ia0y~yU|ht&z~IBd#=yX!Z?svHfq{Xg*vT`5gM)*kh9jke zfq{Xuz$3Dlfr0lr2s1iq%&uTyV2~_vjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw z{mw>;fkA=6)5S5QV$R#Sm2+ZJr+(kReA>HLs&iW>*iL9sSYpk+=_;$EgGu6K)v(iM zE1EgZyx!OI$eK?{ODk`|ipM=?R%<AI;ce;QNSwM=&sy;%11G2AlF0&-{FYB&^={p+ z=YJosbl|yKn!WStUC&p)e_m0ayU*YD`N~(Ts^>kOb6+rYsZ%SLs8)!C7L&7W1BkU^ zLw<0fi>Ow}ihx9ql@^P@Vg^e>WC9@ygPfdg85*XAt>oeozH2g>(;zZd%~Qd|RHc-? zljWF35OY*>7R#0>)!5$GVLO+sR*(&1TIaDs-(dm&#JGzNK^$LJP2kb6)yqqtHP1A2 zd%f)`wiez=%X$T*XGw3(UJ+m4$|d@ZH|O{-p@OnLHSVN~75Niu>!o&RxK9Rgq77@O z&vq<z*}g|B*;(^hmyx+bsXHsvLgkpZb5#P?N6qaDd3!%vde)+?q3mt14qa9<eW)2E zD&y;*#G>kC5a1l3#KI)lwCVHntdQTHyH9#VO<rQ!_*%^~d(xKK3Tr)s^3*&3nN|I| z{<ZA;+PB_I?y1bY6*IMH_o}zoUZr>_<!kPlQdWK>G-4XxhBtehBiVnac`SSz@tZNZ zA!-2|2eYRF;}+)GNf#@sUbsapJS2De-~mp{b?n|Rs&~2HH&y)-8+Phh$G;suuT~wN zBXHqD`=Q1v?YG}dTRBXwv@QGAv(8e<V%f75vnEd5d8Iu)KzN>yhJjMg&WBSczdFOr z5MJ&1CBNg#m-Y4w7MK5IeA}6Bze)FFNq+eK)e~LU&f;TpTVHhdNwB8*&jP7;*7e)+ z^gjP!6TQ7r>44yYjSZ}O_-C+PskSlAwZ5jdK(s=zf~~?ptT$iteuj3)iVfQzEmjta zymBfrGqo;s!NWPT5?*xdy6|OQr?TIASy4+J6D_0ZZLi#zg5H)+mNsDu@{PU9^J-Pv zhWfjl457~!mVDhS_Rd3Z_Ga6kf8O-Yt+y|)e$Q>%9o`o+``WytdMo!|5C8aMrSaAk zs$T+<tiyB**e>wQX^Z?S;w3O+PGybCXX~x9*^&k91<^WE@h=Y-fSkl1ut_<q((@Th z^ThL76F)mQdSrP0)%211c=6W9Qu$?8ChHFNJM!5%)n4^^wK~mcXW~MW-rsq(r`|qU z`>s7L@71p7-MQBPe&yTN&OX!~Zg%JX^Rj<QslwlDrAynZuI$_(o6Yp0^1=Ovi>U^R z2lkwKtaW?iJf?NbQArmoR`Gn(3R$sXdcYLrU9Tfo`d%x@^Kh%N6j@b!e@meF=`R<j zhxB%x`CMIdxGf-oWwP6xlS>5C+xX}IcwONC-|AZVpQlqoqw5O~&AHb1PVCTrFZSU5 z*6#&m!`d6SC0}Ly&sozMyG@n7adD$Pv;7v?*I5tvMcYFbI<@AAi0rHVoRt}IC91kM zKlkcw<<)$@pC$^|TRuPId~aRd|0D75uKH<6Z+*yYnexV*DP87M+TB9-dzN`Ye_w{T z1V1b-Uz4-r_qC@*7XteE50tGjZk*njZ?MBJBb<++j(yDr+1JW(y`aE-Kk<9li)~Lo zeVmnWHvibQ+68I%e$2nt({o1m`HpW2Erq4(#_QfanU#`dygu*m$M*2ty@^K`U3(>e zHSBAwvB-0eZON?-@{IA4Ikzn<<TqR?TVsE0(MNxtuL)WqD-3Erl(^`gPN~wcd9-ZS zhabxP^S{JTJgHpNdf@$S7pKQFt^cV%e7RWf;md8_`>tL5edws}+D|8+xz!eIIF?|2 zO(TJ`poa6VO5^p$UI%`)9P6IWWzwuNQ+{rjILE5hx5BBF>-~hwf_Hbt@Nu<g&7WuZ zY*X9K-}UMeJR-sgTi+Wl`W0Yj@^|B{ud>^(*<CLCc=ub;P6e~*eEsdQw>KVR@^1Vt z{^QDa!*j0_9~36!>PVfR={$8~sYHQHLC~}NT|w@xT%ufQ4(=ttjb;XkuBzQrc&FY> z#^_sD(4;lrBA4~qO!ufr`nveF*q@tmdg;f~USBG@d1_6I9NR3eid%mV_&5CK_6_KD zeVm^@uijqGE6=Dvu|U#b@q~S#6qP@@l<oE<t*<Rv^Ji?l|1??4Z-vRX^?wzP`^1LG za)jyUWllYODp9#u?TXVszYNKO9383u9y(I{uk%lBi7?rxDApU_ZOLt=pS&1UY}=fC z)!|~fc-^api=U7E-*U%`{Wpv7YyZa&{_cyBUGk1Q)##(~gTs|uf*)i(=!h`+mtx5# z)P3yo#*9B<4+<XqXSjVTak*0~S2cg6Y@kC{a<F?`fccH)o3m%yBqcIj-0X1Vd|mGG zs0PoFuZyHDjKzBEs|`L?8u7g}Kd|t?-KX2a7do}tPG8gKVOsj?h~V<yRfq3fmkcge zQCfH_I{ZQOo!xIElnpJd;=VAXA6fKq_n%F3^*3hJc<V^n%T5l6_RLrtu<%gziG8Nx zE2^w7?oZrsd{g-3qTPj;I|Cd(1Y3NyJ|NO>#x+O2`<VQ<i+Kl+Ec*D`@K%|1TagT? zzN+KdpyYLLSK@Km+Gg7sduN`F_2y)I6_=ZL#kcNDy7je?2cK+a{j(73)&F=nZRL?g zAAdLS^L@1dCC@!4r1rgC7F@pPW@dez-zzDlg`CZ{%l7hJHCes=?gXEgeY0!deDm6q zS{--n#Tj1dQ!7vDDhW46Iw%(?7$i-1=L+`De4ebmtmf$FRaR@VwL3jJ%2`v>8G;R0 zz1-?xpZ5A|ZUXPK{C6v|eqZSMwztEBLD;ua;@O!3kqZq!cV9^oS|&W>`bEw)?AKD| z)~|duVeO*a<6O+@Nt1S{r^-EUh&vN;+u&)yyl&UW`urCSY7dvKk$NEE!}9L80n<rk zgZ0~aCqKEzW+T~sEc|z>`D*2?d5it;-HZCYyFJeS-y?qi`dwx+I%b{wm^99BiM?}s zUd6Ac9*6dx>z6ZbeO(lqbm!#rq>De|c5V`#sGw%_{>Yif5naL6FGMCDZ)|U{U%}Tu zD_ALrc@Ap~Lk)+9N`;5(6V(T156-XJwT;L5OlQXxMsCCTmmh2F)0o|L;<SU!=N;~$ z_dldhxw%o+yMaI9)1z69e?QgSlRCh*PJZ3gfXx#t_byK?i||uBzdzJ1@qgNbYY`@K zm%gVR@cGf&+o`Ye+w9)I53kj~FS+wBDCf;(oBc1l|GqB2vv1zLb(7CoI91oY`Zl>@ zeVbgL>tp^ODftl!N{WJ!3QGHrugRFlwC-q4XV+ZaUIxE|8K3v+DeV`V__D5NyM)UU zKbLg}Jaq0d)l4}wM_|Sznc~LxjH_Lr=qnlBzTU`}60yY5rH(I$Deusg)mlr9RwRV& z{Ll4AEJ9tWQ1IuhjvkH51G^Kgul;VQXXsDLJ5y=7kjJ=TJ#%*M@m=T2bnYL2ZoVzK zR-l4$2Y2xDH52w<y?Eg3f!EuTV<(<W*Xfd(q_gU@)NcEmSHGWXu8%i0-n#K~{?S`E z)L*`eK6^OZzaTz;|Hrjk%4F-mCGNU={GI(|_u2BlGV)IdPF!sm&GbD%=Hm&Y8J!8Q zO!sOj*`LVK<dss|#1O&vhhg*I$Lx$f`|f|9d0ph=%KP6Hl%6xLPf$`h9m!zK^+)3I zf|`S6YbLynxPK%>M$dJ*pikpjeWtbBlBeDZkK7=uond|LtAy#aAmvKs8Ryp|TZaXD z{<hqo(W@=NYjU-1`3qxd{fC`j(=T7%zfQvXH&3Sd!B5-QJ^6RNPf1{fSor1Sfd4A_ zM#kHmZD;Lmyg29f><ErG_8!}u4u~EQ64b0NbQk38IAJQGZoSqgt!&>ib(bd$`>n+% z<|l>~RNq;+>21W-88O?l4a`IL&tSTfZXLF{zjm6>GOy1<54dNDE{~Vtvq`nS=FVOF zsG)B~YO6^LQ)$19!KZk&xi*hqepqQ7zf{krxjOv()1$ueOE-U?I>~L%_KVZ+9b1)q zz3}}jw_C#JKTT|&_q3T^Z|Y+8`ys+pPF$KM`Y8P0h0Mw=JI{J=+2Ry8hwq5%k@Z(k zRtrotcoy?;_R~!prQfs96Z`n~b4N#w^pAD%e|gnrCbYQjiT-pie^sMB|MaMsZON<# z!fiG0l#dFqMdciy6@Oy#rgty5u9>hZV8OqCoL`rpvq-v>zv^I!#$5A<>1QNrHCJ!D zdw@B6aZlHo<YO<qXWsc>*z`2|BY*b2ca=TYxBZ>A?cC9x)z&kW)&814etrM%8DH6- zg+Y1gZ$Hnr``<rJ^wYm*YPEY^CNF<>F|DxE>|06Dfp2FP%L+%DaS1bSv2P7ko5y-b zMQQn}RqM~$3KtYg2V~?<6?yMw(0ky_9`4AUz07g~6DNn1RyJ<?&nalhxntFV<)uuk z4J}hS&%9oFe9ttt1S7Rs>(->4Gx^svv*3xh{QG_1ZyU~E-Z^9H1BYF66wjRfx#E%g zYw7C@=~Y{9O7iXy{Csz2LuGIB=PiM}I@@GlZ>jk#&i>13UXZBFWr?{(#VOyX-tKti zY+3V&_l$JCZhpy~L#x(a|M=&azQNXqF={;#2X^e@7v$pkZM<typzlW0hZ}ZgYvz~T zT5@Twk9;ZT@-sCvXMK3G=KM^%Uzab<t6BE%>$m&$tea)@Sl9Cxn?Dn%k5}xx`Mi#; z_{A@#4QUF?Cw977Gi@*s%=|g!Oawy>Z?WD(_514e>RZH$FYjTESh(ulI&s$&p8qnb z&rA22itPw?;S0ao*W~th-C3;%*DFnYlaAH#e^{~VS&zo)IcH^A&K%77KBtj0_WSJo zq-`~O3mW$)Djk`2WYNbX?|5^LOZ9ecxGkx=!&<rVKWmtX?X62c89uycj=A0UGv}^M z_c8hN*AA4eac-zTxaSzNZO&DO?wsSZM7xjO|0Fdd<@Y{N+1%dz{r92P;$!~}^q;={ z_M~{r+a1lmvEDc5)lB@G)t^>#x8~UO^XVmLC!UatTefWVdsDM=-PGXgLEFCAc$5~{ z2)}x6TzKSV&-&T@GjBXg^FFU^v+dm%-{)su&53wpk#l@j+fK!&kM>2F#9i1_;qTxw z?en@_73ONEo|iqy3w^7#@cv@MiRJ1QCU4pAtzWh8j)%(=kuy76uFKUj{SYg-`|g~& z`+axD{l}jKS0=8`3QU(!Dl#ls?7rEaF+Tii@6j7Kj$SkAxx7!}@dAcLtD>$fIbF7< zxpDf3KD$`v8Y8h@`!I+5&l6?7ywI7xoiVw{X0M)DuefMzw)M5|4gXJEOi{R9&N%&v zbW^T%7|Z5Ozl841zaO1+@ds;ME#HHVtZnlR-ZoDBeN}Gu`5pf{_kA(D_o}YJH}?Cc zo!Ody-|@M*@+GHyjd62*W@Kd4Kk=$B&vB*dRhd@5<CZG_RJuL+^u3?vOXq^J6X(~@ zlJVyAcda_KxH|N*PF-b6{;9WiYj%2ed(~`~eJyk5thu;-r|V<pvsK?BjQTQcK5#!^ zvlljVb!pH~mI+!Hd9-&`RVV+qR|dw(>m#3@KHwg@^}!KG!Ij5auEkAr|GYW$>p7*I zKfQO-dBaz}D(8A@&c9ndanYrHt5=;DyWIXZ;(KyYTG7Yw2Tc(saUWccm#y)f(do8O zduNf=qMRB_v0m}1_cN@owHrF}$0^=ler(al>LSw<`%Kjv?`_$a{&l8!=s`oD%#1Xz z=9wv_-`6Hqzc;cHI(_3xDgXMMz0=n1+j^xn-C$Rm+}4LLue7KI3eR&<G3lIjK}G3d zXR2j!knd!le6^eR-IT7nE!y9kvZ{C9zpo`K@rxFhvxXkLDA{h=zEb&o^0yz^5|Osn z>qGthZZ9b?6+Ms>EOPo_<rc|POY1Fna&7+gQCs}AtkLZXd*O*^XWW_IIPHUY#|iV0 z((F#P{f*m&tAuB)U-i1%aKWm*N}uv)Y!+Mp<avC@iMy+A&CmZoGviRqQulYqr^tMi z4Xx#v+z=zOEqQMLcHt_K9~C-M`mcUVbsxK(bKHuxf=Oc29uxk5n<La&1iFvC-!A() z<^f-k|AV?@>uU=)X6z|nz!oVxG3(U54apyuK7Qq=XXTiwUK(n=V)d+pPXyoRwJyIa zXH_+Qv+Yl%yS1w`{q3Bi!=+FD>s1NL{j%J81y|oPHJi?Y1QW-ViV`bSMOG*(n6@x& zomrpF;lZ+We#ox&EovUSIX|!ew&!#4skbT@_fNccC5@?3>+u^W;oisG{z)Mza_oJ( zIwnrs8N&YAwqLtv`!0DUjjQ=}YYcx@ao(-qoWrz6YvKRI?p3#%6CS4tS_%}GP41O? zAmsBYJ1`~hLEQ6}y9b(*=3ej%2{pc*E`0K>2_J)9<9D;$>&_f{a+>Fe{$IX#A11D^ zh^gldHd;ExQ+Z=XO|V+ko61wO!$W!g>x=c)ufO7#bn!=g?(tt-X<uyxE*qp5Zs)n8 zu>0DaZ+AD=9bY?tmhrz?0hUbsO^s2<uE?C7F>CSWIRD+YDnZho%Wl2!&ac0|bH{G$ z8F?{xyF6wrxDp_H$Nuq_ERHmpFoOVRjR1cYlSYpWq0oalx3@h_&;8w7o9TNeD#`q( z#iB&Tz|HKRgCAVxiFx;@#;d?Z!u{FQp5L~ixw;3BS+B_Qa?v~bV&T(UX+NY|Ef;;1 zyDqP}V7=qvvNeYrm2GF$nu_)Qp41@MeJuQT-`857L(dBq%6|U-ty|i_^I7}zjAG4J zE2Reqr`-PA?|yx5)gjKyaw~W{u2;_6&%(U*O1P{`?N!I-tnD#tDqf$ua%|o1?K^J8 zueugvcd|*;J%nkd?%MfG`=u6~%n@7}d^M?bz1_pYzo9-yWA6uC-KzA-xu`u-UQ6lt zimZK8tBY&JCa>D3)%oM(Qn~B$J+6;g=TEQvP<)0ZWyhTh>$+SY?+f+obA7z-ZNz-` zO>VbSEiRU=`F?cK$MDUuXLgC-yR#(k)?3T!>QAl9=SS=}Fg$a#v)y!k-rt$OPhFc| zvgGc($FpX8iCy{9^`Rj7VnuqVnbr*FSB)HxWS!=V3T7Vp{o$AOswlpNx3UctckMjk z?oqD3(<xC|$!KN6KDO4LMGfIB)8BXQoVj`8L#3Ttd4KfYpL=Z4M|B4M+|!KAwQPS3 zOwBXnPrbD(_5QA`{Vu*Z<^Q)Q-ADTh=P%K}d+xHdo}d2xKk~7e=7n?kJJ&qWwGX;9 zuZ-3CS&mxxlBg{w)`?fdykF#V^2DlH2Q)06JetC^>kHS76Z_uVZINmAoF&^~ZrHT^ z>49TYf+VuUjxPGRr#Xl#<-pqQJj~0cKAHSr@!prZzb@Q*T$y-z`ipXHR<q*8YjXd` zzl*Hph!*zeSdp}9$*;!n87W5vf4Z;RGd-sI;m#AH1`Gw&mW!CmANtE(U*7GpZ&yd$ z>fiO}TPE<U?@<$|*PLz=_Uz2dcST=f73wvU?{823^p-7Y+uMl9sZWYC&$c=@?%Mr* zwQbtBO!oC_qYR2IZU5#MoL}V}x%ch#FUz~Vj@`=+^xe3y*pPpn<+RY->z?0OSzd7J z%yj17$Lpj7d9K)9+vK#2?~uVC5raF|Pfq2H^T=$7{TQWsCopCI&rclx)~3umc;YV4 zk&Op>gR<^5@2}XqEhc(XT|xNF-`^KWFLnwNOS)KLtrk<O&?FH$`Mbe{YjawdO1s^{ z&Ue;5Xizm}<P4U!Ye-&x@7%I=)s>~M{VOv+o6o%Tp^#6iSTZ@Q>+Z@MpI^R=KR)Z* z{f||@{|oW;r97K)-Z1$~>iZ|MvR2=^98SHJ*_!qGYyb8&6AsUw{pGFDnM&32x3in- zBLya&l<?guxZC#AUc+azEJS3gG?j!uTzjRZWoaC5^zhF4Ny*(Ti=!S`W`|vs6Rczk zo>HKo$I>S@QJ&FUbkgz=wLN(ur%&b7uQ+c$4HTPKEWI977OkEl<2$Y6RO6y&eq3x> z{{J3qS!%jleVX5j_yaA!SWCXIP4xZ!|HBXIbU{wvgj*}>Q=R^5_N=g5@*;f0@(CVW zH|~6Gcdh)+soS>w20I@nM+E1m=H!}nYR<5I_j}>Fd*R2om+JjAYE8HMKRL7i_2FFY zlmLN=7bO&rD;t_$+9~o-hikzMrJA^{#oMOLGM}P;e&TJx%Bd1Jr^ZjdE!gOJ=%4I| zT*05h1^OS3Bv`B7-l#Ug`cnShc*Wb68Ao4S<DL5KCU<kf;nO$M?p{0hs)H$LYeClG zouNC6I$o_xGf4l#pCMIlnmgA!;(np_+URv^A`*#93SK=2ja(_7zWs6XmfMG?&2ztT zXX})+vEMIV-9BsM*5igNAL^{ptXG>K^y;#E{jRLy>$T5=XDNTOvgf$bcU7SK*!N$} zd(QkkY&m1kyx<daV_upcwNTRUJ@~v^(nGM~rl!iRv&UX+J^L}MBQmkh`11>)iR}h{ zFBKD>DyZzQy1Gfrv(-jE>0-s9vNhLF_$w}znNod}{a&9&w&&C}eI6=TY<50;vnNE9 zNhr<h@bMX!J{XEjf7|)|#+_RE^|`-I{d0e?@P=-dkeqq?goj)9x+=Y^3lFZ!53{fi zmA9I7`o_GNW8a=|Z&9C@=JIu6&-(uypVj{KY}|TWkVho!Z$->q&n2doOH*5AZroXN zG)(-?lzz>gJ^$jmJLdFy+I(I6AfZuY^3%6dnZqV&WH9D@*fFm>arc?9`>ivI8=a;w zzcN(4P%ajbEk04*Fxrft`TVM^RSo|>9n|iu*zoDSnQ=*<#{A#zD=YT)N2(XzoZGwW zs-tGiln@SsUC%t+uFITUn7n^xq^HfPZ%;mhT$ua2x8&Pir_utQ#Y&$gBxicZR<M~q zJSn+%=gE-vV^d?-?$*ESr?j=vrT*E(D1Ub&Wy>r3+h$!5adUfSpk*|DX0qM!9lL~A z@=Xns_T><qFyVDpexlr)`IW}~X2K_AtM(k9bkcVxXa3v`oA{S@Jg->0h%NimgVf~< zuFhMvPeti@@+&RT-#6uY1^w@8F1V$t#GibqQT(@v=ZmKuZKr?lUw-@BrirE9ISrxx zcWz$W`Q*^wtbR?+N2(GlYM=XLs;~RDX>qx#?8(%UFZ+&6o7e7<;^HxRW$%t#{=OMX zrVCGAX;ByE>r>MZY-;3iG`v-0UC+m+#Ii&x{CA<L=Plo~B@L6F{hv8MI4|ApNeOdj zi1fN|f}FkQGtNn}TdaGh!gud|ch8!6&K57e&eWZ=wo{!UQ$MWI+S#rzLv6zLl)v+u zI(!c1JU%1yYNJT!b>FHDhXVHIiLkQoS@9}X=@X;Rk9S*=*tSZ4ogw9to-ikf`-5}n z*;!NPtBbmuaBNGySTX%=#LjaEKb5;=`szg8|2VyM^YPmC_ujua-4J4|RV1-ksXcY? zz4`b4^#yx<$yb<J^;&lMq@vxPufs0oDSh6pvhH@bhsI)k@sk!>pNm5acn${eK76pE zC)v1fxz(2c3$A-a-7k#xGQAp<Wd4(5mRY6w&CMq)7S;#c35&5#v(8dhOEhV;%=S*; z>tj!1Y?ZxJ{jjuIUdNS3-`dqRZq=*LN=EF;8o%EEQ2!Lk|3~`+r{GS`9YLX=XHIzA zH(gmu@BROK3A-mZ-haPlzx)fC7xV6YnReAbcgKqhIiXRfJuNJ{3JOGcul~I9<(6?p z?Qi*#+~2)-Gku>0tvK6W!?)#h;K`yDKXrdvE@ast@_38f(GVluRr5U64xRRQe^UC) zFyf$CTv?ah?2{rId!k(GoR3vLxPL@r#>>w-f;&AzxqbP*2AN8L-C!O5CGXn)!wyl! zwVK<1AK7+J_4T~lKW-Mih<j_iXXoK(JGY${&h(vg<<64zQ!j<<NX55b>^gOH<F@2h z?~TTD1ST4vjIH7Ine^;>qvoorZL9vpgwEHz#+j<IljT%iMeou7e;>Rtsff{!oa-FA zeZ{M-tE{AhyZ+i6++J1Zai@pzozTSdqCafbuk~O5{ir_M>2niqoV!z9w06~Iu`Oqo znqHq+w7Wlg```Xt|26S9=KJu51)RUvxccv&Ki~MjtTfm6T4rjwf2ZePuJd2DdSi}% zxbjnLhR@S0^Q%p&87+cB>({K>r^z*W_nczG3fUd})1?zPu6h(1cvU>)t5oP;foC-j zQXbqsP&;Gor6hp|Rf3kw8=hA@<=~zEz21NO+f?P(_Z6QzdS(mX4$8YOEN%V%-TvyU zjw?f>*I(`u+HRg-YJcE<xa^#molk$qX8NvM+#kdjS+1Ub@-9znjbpp9`^0MRcexKl zLgQId%a7-~Pw_V5=sNAI#inl^_gF_N)OW?J=2cZ&SDicG^OVEq=KKA@H@Y+#{=~U_ zni~BnN|5(jSILGqJX3S^rCZl_Fr?`|zkc9MO6dC|TV`9-{fxdJRr@~M*EV;q_X>+8 z-#6L*wDDhKum1DKF5|K<U)Jw_Gbb{Zmy18(=w99PUGefw`=4KY7IS;!H@3Sw`>SI9 zugW^P)RgT|zWbA{OAR6>sl>IfZ=RUV)Uaw<=vT|ozpbn0wRu|YZTKU)LApWvQ_kzB z9c<d$(=`^`u|GEcX4n6^)pF4^(NDpR{0eib9{Sw9cxTDSDYtD`uP*;8dTzFO-jCPX z!a~~jcAfJ5QvLM!+q+Bd<Xzi#|HP%M#k=Gqiha&6J6HYBdhN-~v%Xcr2aeDFG3S7h z(x12M(z`v*-Rkv8Tsc+l>@tSs)3UN93f%g#ts8>0@1?oAxFwuQ|9$ec+;wT!6a&7= zd#4<Zmv1plaSgQ&{VEn}E2w<^$&-&;&Ubj&J+I845WQsU#j-V<+AiJdGo5-c`e$0Q z0n^MqIVrt=&i*>4pEXtEkn2H(&$FfP-!0G2yHv79^Xuv2v{Jv9zWZx`t&Xn$c71oa z@B8|ayDK}lowN4bc{K0myT~24&QF`?Ua{@%rhlsX@9%$q^lZJ|^^3dz6oprRkG?VE zM^f?RcaOe?Zk_7SeAiac?)_zff7bnG@!wBg3z(E$R=R!FwAK4gUwoq9H%ov0glHxE zp6^*l`8TbSe}Blk*?`SI<ZEP4#j@)&Uw9n&^h!T?j^6Y?KD<xk*FI5h4e{Ib{0z^! z-ZhiE*Ys(;Et;m$Eq?A2<96X$v%f4XPJ3%{`|3Z>*}JF3{k+oVzi-ozrx#!TzAkfh zXVK~JTWvq`%rf>(KEG&pKJWV6-@N_ntd~}QJk|E^__etkGh>g>|EImSvoc+N*WZq3 zYJY=^C#UsVWaVAkqOo7}q0aoa&+K~^&-U9Oo4rDmqf2eG*2%3+F~M%1kN>)xxa`%c z>t5d~_P>4C)zNb}x~6o6=%V|I#uqynqitr@8JySK8y0c=$xZ)_Q+C(uFT2_Iv}(t_ z4Gavt|3Pa3mR3e+#jQ7deI$utt>w9phyR}?n+9KdpnkwVD(pnv+s<^mJ&U)0U95e7 z-!l=#0P}V4mRzaZdi+{%&zY%#SBqD2`k!2WTXCuV-nno0o_^u6&!hU$%_qy&EnT_q z%e8=jEV;Mk*6$eJ<>vW_lqj#^Dvr<Zn*BaP?eV#pt5sz=jxYLHc6;MLhL~p;zp&lj z`0VJSkG6&^_q{)_nR6?F`}CFfhb2-!H16Q|kvcPnd5-tHsc}tzwjE~U4UN@TGG~rI zbx|jNVc7bsSAWj%jN56kzNhwiGsD>f`ll}Ld>aw?;DC(pwj+x^ZoR3Oaz9#h8OwqM z&tK2f?snbWzF%}fYOBe%(tlr%oQ?f1FUS!hKeuc*Kc9!F@~U*z<5y&2jjf6*re2(B zTx1vOxc_`RyV}#T$#-18Emz)Yvsd?HNV)r`qY3d<(JCS<;@7|UthxO%qa6Dq`Rq5k zRyW_Dao(~ywY6l4lh2{~CziZtoUc^=>BhnX<&F7263&a~9G@lKee81}<F`q<`rZa@ z9djIhZZmktzHh?gUx7+S-l2QL{EmLom%A>n{kSY$)8={M-PPwV`xS}iGXL1{HsW#8 z#U1Y_Hg8LoJ^Nw8a;ttLOQk)sZznUyo_N&}^0PY1a;nhMm$PSY5e;r;Hemf8^LGBd zc@=4S)sJTGeCXgT!YT74>;8@{vn3=aObBB=u9od2d>OiWK{@oGA>(4r@9Xx(7$`qZ zu1nwY?9!}@(~E_dlwX`%Z0cEG)&A|v{)um1#$UgG`Tw#n%kHo8+ugZpfy*wY##g!u zuY>cRUtaZ6+#u+s{(Rfjhp&eoS*!i@1H+E9z5jA*k1YCVo_qWk@6PlthW~li*B)=o zsOgk1^FCO%=5nL`q<EKE;eG)-RqpJMSXOt?Vv%3b>hgLq4U@>r!h)7k@BeIat-Jk| zbA6|G-1Ewf8GlS~Z<Jxvn;_1j$5t0{;Krw?myEtd@=SXh(YrBY&(+<T|5Cgf=D)v~ zb0FyJ&DVkl=W5lk?pW5cac}!Gwa_#7{z)Z#Tz5Sx%;Un<e;-d?Id<~om1CZ7Pd-;` z_TlsT8Zj|ouEz$xC1De!&fIi#xtMijt(yw#BoP%)H*cRp;Z=G4lU@~VUGHrASM5UE zMcLa7TXb`Lcy7JXe^In;wGLza>vb`$H=WOKy{~ydcU$sYIq-~P*}arS6ZiE+9(^kP z{r%Rj&#Eu2>(E)yV;Qk(mcT^U8B^al#HcCl4-37YCo=QM18JLCswWp4y%u;Vnj6*e z?au!RyMtbfZcDx^(S2<1xuu_um905!@X6&c<H0EbAFWq?n|<Ji%=PUI<$NWEzq>qk za6h%yUHfV8Gq=BCoy=#tpQ-sK%edZ7xFPNH=BZJ9>v_)x{+~DVb)?>JkbNz*XL3w- z<L;c}R{CPS_n$tO*t0?QHG|K~wz*H6KNq>Ix}~WU%cC>zr=y_c+EtOCg}79MpMG_- z|Mls?{u6IIoFq&;SuJmgoKl_f<U!1xHLIGJGQ?@@Ja=`0f%_NnGrz4btSV7Sv;O^w z>DQM>;ykgT*K+10SzpWCn6W2d@4huR`b=ei%yQps|HNwbxpk)3SHyD2JMMh3?dhY` z1x|c#!Yt!jqXqRc@<Q%=e+3O;o_yc1n=c~C`dT@gbN9KjH5=YW#2?Q&`)pO>{O*{i zI;#}-H%RFl{j{!UQTu%{UN7V-quiGavv`+%Z`*AJE!lRQTRfBd&x!+e=k*<~E(o!E zT4?+JeW;Gqd$zo98D%TqM(kq|yU|x_n%dW<GwFV+jE85+wkLVY-y^psg)L$>+Hfsq z@jgM04~c^Nver+K-)5t^RHWp#`p(>)H8E3FHKv=JXW3n{Ir>TT%+F?VrWj6}eCunT zPbEB#G1dxA?*F9R!>luHsoeGb6FBuWqy$$oPGeX;{gQv<ANHNz0SqyrEgPQOD=PIK zh^@%ay}M%7&nXf&r|L82cbw37UGhZwmP5x0@e^~^w{QygMsVC$Ezgx>%-baUTAB6R zZ~q&8Un3vbd%s-V<HPc9tL*9*UGujkw;DWs)c+;p*Zr9;e;7~hjQMjzCsvf>gJY`( z@0M*k(+~dH^lqb0lqUQ3H4~~`e=r{Ud3N(w9WDmzPS?kpJ`vBlZ028LvB|K$ws>3e z+*IqZlahb8CGS07wq~`#^ZNbA<t-=fPfQDD4rJ94n<(7<GREauVqQ{RUUTK5RfT87 z=8EqZm>A#bad2y`%ah<48UE`JOxUhrw`!A^aKuJ0mmI-J4JFsB#;4jkYGm^--j3!L zJ<#L&IJaW`vcm`FZcDD+7qC<3_QqAY$9GNp<huOl(SrvbZ%Y=PW1en(&7`N`t)`ss z?GukbtIm3UbLU+x@l~IMA8?wVFh6g*Wl2_NwQ7Bxr&~L74d=sdjqBytKQ{aOo}Tw4 ziZAE5RClE7p8W@wmnG!=;!n<tTb*?AM}ro_O#Nq1dgclncAffps{PB+MIU2M|EPCz z$vbRuGiJwu^$qf?SEb)->^O7e%nN<>>LXWY&+u5=c*=iC$kruq&-N5o&ia!jxAw3` z*!_j8n9n_NRbiOZqUkfezc}<kk5ble1tq?l%x+eO+CI`1n*wwl1UZwB{mb2xe(QGQ z%~eqw&PAv-JS+SZka}Rsh96-%Qs*WA&5cylVOXcS+slpr1INz%d!-vQ_N=({Vq0=8 z|K?kA9j*tCZ~AkObshV<^HXM>OiecV;Q1h{$i4nuN}`U`{==&S@AYv!cy{%YfM3y4 zmK`ilD_j1rT%UAtrEz=eyMqfZews9A!J%}cQ=Hy{`!3#DvAR7;XIpabp*tL2H>Z~w zM$64MFrTJ(MzPA)O*`A_-fdgvz!l0_hb&ra_^L0@_&oi1!_yg7uADwAzOy_~=$Y{9 zjZEzhiQ`qA1rHf=y~<oZoXf7B^k?6NgexnjC*A9x8=CH(ut;rr@|K@mC+GZMw<<2u z;dl9ohaF|<%pY{rL{o&-bn2y#b%rF`1wS^>eqkx$J}ZA`lmE)Bi<~RAC7UJ|b${Br zUF>l}c%qv2ZG(wP!3Mi|&OY(e{$Z3R`l-HXi{^6WS<QuR%%16*$1MNLE<XER&23ti z(KA!I{+V;0?r#2cJi_E(XN{EDla6~YYiC|@Wt!7oU}@m|P_6a;{M$<oyU8uNvt`RE z#vOCI3pfL`54~s7%!{$uzQX9(zDYA@{^ckxGnw;(eO<5XV^-Ist`k!al%FzQ^8G_) z=w8L(g<IG3aX7g29k2Q5ZRxGu7QgycdUtIRW8aFHJ1bUApK{AHE&5g5AMIQ_0bPmI zZfOZCcN1ab#+sf_*VUqUOw#*!Z8Wu(d|0&e)?J(E{G7Z?iwkDTH|%22$&FOZ{l@jP z)_ZkK!EqzaUjjK6Mb|e6Wp6*VQg>}p>}jsZ&P_rOEn3%KJhdfaab3EFg`z>a+s2GP zs}DrZn0dC~f#8D=PICnsR>kq&dGvq6y~E|9SGDgS3lI4kc&5zn<|~HOhwf7pGs^^i zZj|@qP}qF;Ti_n;7t;<YA5c%axMRL$i>qMwvG_wd=cj%?@gVQPotn7<KLYr6R`%I( z$8Z(hTolIVt^L{cXNlH{UR~`J)kA+5*(^V_>F@nVSB3qrEJ`h6n>;Pd(qU@lqBE?1 zDu)U+^tE+FA8!5{YV+^hw3N#i`brg&F8&a@z46`gD>*;4j^;0)dZ>8D->)^VG@d?6 zQfR%H{CRfWgUzdE86Vjav*Oibr6QFN=?``o&B|i`=eu>!LF3dE_7A+FXVot#=O<nK zad=zu+_sZ{zFW8a_j~Xs!o)5k{b(F$5l-&$S^YN;ec!EfbbiBmkyZJTSFXj_{VAMz zCA8hraf+~^`RAPnB&LXRoEDkZ`9VqZTABHVLIeJ<<(Dog9sCk=to^c2Xlmc;Gomxv z4aAE({!ZdJ9W(v-<%0V^H=hn`S^MTj-&XlF?+3**Y=0I#N@|i{*X{Z^&eE7wQEC3M zC%!+Y`hAco$Xxd3z`8QF)a*R-*-e(Sf9dVqe1-ey(I=j@v0F_J-ksZVB2vP0I_s4E z$NE0K`*Hf4>=xN<6}JO7`>%#EP1gGOIl|<hU&#Eo5&M%kwm(a{_#=Dwmidf-^p8IO z`PNIipi)Qbf3~5&-fZPl$8^`9x)IQ|bJ3a7sXLX<I9dp#>Rjl%)Fx5N8fxa%VAfa` z?%6d-#lb0N(n|%;ODaryCreEChUz^mu@^M=XkjotXd=ifaB!(xD@TC-A(zsbDu+~B zJT)d8#DCK}v+xdAW2%;pH+w|cy@hK&NA{&hBv^-;K6`et=jNmjtBZ2%pFOzC7L|0d z;-*$@*9ny~Kd+~X?BD*M=h(u#b9DcdYsg&G+m>y({;iPL>cA_FmUH%ng~oqO&e=U# z)2m#^&FGnh&tB#IYMl8All=cjxGCQFK0$MS5vy9kq6@o1JbRkH_ixhl<yUu~QrVYq z#A|<B`NbnSds=@=$I5me+b;gGCSii?MTae0yg~zJyjV8Vr{Ux)qfUm&fojS<E}aaf z8iFkxlYUAn&FHx&lEya4#bc4mg)`@$eZQ-;Wv7=k(vslP`zxMJZ}kZ{EYtJtTHuU~ zS(y@%e`B7va~hqK4&MJv^TEUj6T21fQ{ARL=kekA$$5Zh)9Rw#GX!3Q`GnlvyJ}Yc zxvLW<b*_nSsI2{L5be&KyE6Z%ONnh8r)6EjhO1kQ`!eO!bgIOjHT+5NsceWb|62DT z)u-g{=Nad(Otxw<*SNj$Sh89Ux8eF^rT4Y(FVDEM^SQxphI&x4YTA2P==R3G13rHC zEtUl*rZr#d{Q1tl({g)l#&yy7jXd1$pe3DLqTgcMCJV%a7f;_{$F`svvZhf~i%0Xn ayyEZBrE5+9?_ywJVDNPHb6Mw<&;$T*nS_b} literal 0 HcmV?d00001 diff --git a/src/core/pics/caosdb_no_undertitle_622.png b/src/core/pics/caosdb_no_undertitle_622.png new file mode 100644 index 0000000000000000000000000000000000000000..5be79fc860bd770cec13185e6077bb0d851bcc80 GIT binary patch literal 17962 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 literal 0 HcmV?d00001 diff --git a/src/core/webcaosdb.xsl b/src/core/webcaosdb.xsl new file mode 100644 index 00000000..60b4463e --- /dev/null +++ b/src/core/webcaosdb.xsl @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header +--> +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + <xsl:output method="html" /> + + <xsl:include href="xsl/main.xsl" /> + <xsl:include href="xsl/navbar.xsl" /> + <xsl:include href="xsl/messages.xsl" /> + <xsl:include href="xsl/query.xsl" /> + <xsl:include href="xsl/entity.xsl" /> + <xsl:include href="xsl/filesystem.xsl" /> + + <xsl:template match="/"> + <html lang="en"> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <xsl:call-template name="caosdb-head-title" /> + <xsl:call-template name="caosdb-head-css" /> + <xsl:call-template name="caosdb-head-js" /> + </head> + <body> + <xsl:call-template name="caosdb-top-navbar" /> + <xsl:call-template name="paging-panel"/> + <xsl:call-template name="caosdb-data-container" /> + <xsl:call-template name="paging-panel"/> + </body> + </html> + </xsl:template> + +</xsl:stylesheet> \ No newline at end of file diff --git a/src/core/xsl/annotation.xsl b/src/core/xsl/annotation.xsl new file mode 100644 index 00000000..f41f6cbb --- /dev/null +++ b/src/core/xsl/annotation.xsl @@ -0,0 +1,79 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header +--> +<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + <xsl:output method="html"/> + <xsl:template match="History" mode="comment-annotation-header"> + <h4 class="media-heading"> + <xsl:value-of select="@username"/> + <small> + <i> + <xsl:text> posted on </xsl:text> + <xsl:value-of select="@datetime"/> + </i> + </small> + </h4> + </xsl:template> + <xsl:template match="Property" mode="comment-annotation-text"> + <p class="caosdb-comment-annotation-text"> + <xsl:value-of select="text()"/> + </p> + </xsl:template> + <xsl:template match="Record" mode="comment-annotation"> + <div class="media"> + <div class="media-left"> + <h3> + <xsl:text>»</xsl:text> + </h3> + </div> + <div class="media-body"> + <xsl:apply-templates mode="comment-annotation-header" select="History[translate(@transaction,'insert','INSERT')='INSERT']"/> + <xsl:apply-templates mode="comment-annotation-text" select="Property[@name='comment']"/> + </div> + </div> + </xsl:template> + <xsl:template match="Record"> + <li class="list-group-item"> + <xsl:apply-templates mode="comment-annotation" select="."/> + </li> + </xsl:template> + <xsl:template match="Record" mode="error"> + <div class="alert alert-danger caosdb-new-comment-error alert-dismissable"> + <button class="close" data-dismiss="alert" aria-label="close">×</button> + <strong>Error!</strong> + This comment has not been inserted. + <p class="small"><pre><code><xsl:copy-of select="."/></code></pre></p></div> + </xsl:template> + <xsl:template match="Response"> + <div> + <xsl:choose> + <xsl:when test="Error[@code='12']"> + <xsl:apply-templates mode="error" select="Record[Property[@name='annotationOf']]"/> + </xsl:when> + <xsl:otherwise> + <xsl:apply-templates select="Record[Property[@name='annotationOf']]"/> + </xsl:otherwise> + </xsl:choose> + </div> + </xsl:template> +</xsl:stylesheet> diff --git a/src/core/xsl/common.xsl b/src/core/xsl/common.xsl new file mode 100644 index 00000000..e166cf91 --- /dev/null +++ b/src/core/xsl/common.xsl @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header +--> +<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + <xsl:output method="html"/> + <xsl:template name="make-filesystem-link"> + <xsl:param name="href"/> + <xsl:param name="display" select="$href"/> + <a> + <xsl:attribute name="href"> + <xsl:value-of select="concat($filesystempath,$href)"/> + </xsl:attribute> + <xsl:value-of select="$display"/> + </a> + </xsl:template> +</xsl:stylesheet> diff --git a/src/core/xsl/entity.xsl b/src/core/xsl/entity.xsl new file mode 100644 index 00000000..32f56333 --- /dev/null +++ b/src/core/xsl/entity.xsl @@ -0,0 +1,462 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header +--><xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + <xsl:output method="html"/> + <!-- These little colored Rs, RTs, Ps, and Fs which hilite the beginning + of a new entity. --> + <xsl:template match="Property" mode="entity-heading-label"> + <span class="label caosdb-f-entity-role caosdb-label-property" data-entity-role="Property">P</span> + </xsl:template> + <xsl:template match="Record" mode="entity-heading-label"> + <span class="label caosdb-f-entity-role caosdb-label-record" data-entity-role="Record">R</span> + </xsl:template> + <xsl:template match="RecordType" mode="entity-heading-label"> + <span class="label caosdb-f-entity-role caosdb-label-recordtype" data-entity-role="RecordType">RT</span> + </xsl:template> + <xsl:template match="File" mode="entity-heading-label"> + <span class="label caosdb-f-entity-role caosdb-label-file" data-entity-role="File">F</span> + </xsl:template> + <xsl:template match="@id" mode="backreference-link"> + <a class="caosdb-backref-link label caosdb-id-button" title="Find all referencing entities."> + <xsl:attribute name="href"> + <xsl:value-of select="concat($entitypath, '?P=0L10&query=FIND+Entity+which+references+', current())"/> + </xsl:attribute> + <span class="glyphicon glyphicon-share-alt flipped-horiz-icon"/> Backref + </a> + <span class="spacer"/> + </xsl:template> + <!-- special entity properties like type, checksum, path... --> + <xsl:template match="@datatype" mode="entity-heading-attributes-datatype"> + <p class="caosdb-entity-heading-attr small text-justify"> + <em class="caosdb-entity-heading-attr-name">data type:</em> + <xsl:value-of select="."/> + </p> + </xsl:template> + <xsl:template match="@checksum" mode="entity-heading-attributes-checksum"> + <p class="caosdb-entity-heading-attr caosdb-overflow-box small text-justify"> + <em class="caosdb-entity-heading-attr-name"> + <xsl:value-of select="concat(name(),':')"/> + </em> + <span class="caosdb-checksum caosdb-overflow-content"> + <xsl:value-of select="."/> + </span> + </p> + </xsl:template> + <xsl:template match="@path" mode="entity-heading-attributes-path"> + <p class="caosdb-entity-heading-attr small text-justify"> + <em class="caosdb-entity-heading-attr-name"> + <xsl:value-of select="concat(name(),':')"/> + </em> + <xsl:call-template name="make-filesystem-link"> + <xsl:with-param name="href" select="."/> + </xsl:call-template> + </p> + </xsl:template> + <!-- Any further entity attributes --> + <xsl:template match="@*" mode="entity-heading-attributes"> + <p class="caosdb-entity-heading-attr small text-justify"> + <em class="caosdb-entity-heading-attr-name"> + <xsl:value-of select="concat(name(),':')"/> + </em> + <xsl:value-of select="."/> + </p> + </xsl:template> + <xsl:template match="*" mode="entity-action-panel"> + <div class="caosdb-entity-actions-panel text-right btn-group-xs"></div> + </xsl:template> + <!-- Main entry for ENTITIES --> + <xsl:template match="Property|Record|RecordType|File" mode="entities"> + <div class="panel panel-default caosdb-entity-panel"> + <xsl:attribute name="id"> + <xsl:value-of select="@id"/> + </xsl:attribute> + <xsl:attribute name="data-entity-id"> + <xsl:value-of select="@id"/> + </xsl:attribute> + <div class="panel-heading caosdb-entity-panel-heading"> + <xsl:attribute name="data-entity-datatype"> + <xsl:value-of select="@datatype"/> + </xsl:attribute> + <div class="row"> + <div class="col-sm-8"> + <h5> + <xsl:apply-templates mode="entity-heading-label" select="."/> + <!-- Parents --> + <span class="caosdb-f-parent-list"> + <xsl:if test="Parent"> + <!-- <xsl:apply-templates select="Parent" mode="entity-body" /> --> + <xsl:for-each select="Parent"> + <span class="caosdb-parent-item small"> + <!-- TODO lots of code duplication with parent.xsl --> + <xsl:attribute name="id"> + <xsl:value-of select="generate-id()"/> + </xsl:attribute> + <span class="caosdb-f-parent-actions-panel"></span> + <a class="caosdb-parent-name"> + <xsl:attribute name="href"> + <xsl:value-of select="concat($entitypath, @id)"/> + </xsl:attribute> + <xsl:value-of select="@name"/> + </a> + </span> + </xsl:for-each> + </xsl:if> + </span> + <strong class="caosdb-label-name"> + <xsl:value-of select="@name"/> + </strong> + </h5> + </div> + <div class="col-sm-4 text-right"> + <h5> + <xsl:apply-templates mode="backreference-link" select="@id"/> + <span class="label caosdb-id caosdb-id-button"> + <xsl:value-of select="@id"/> + </span> + </h5> + </div> + </div> + <xsl:apply-templates mode="entity-heading-attributes" select="@description"/> + <xsl:apply-templates mode="entity-heading-attributes-datatype" select="@datatype"/> + <xsl:apply-templates mode="entity-heading-attributes-path" select="@path"/> + <xsl:apply-templates mode="entity-heading-attributes" select="@*[not(contains('+checksum+cuid+id+name+description+datatype+path+',concat('+',name(),'+')))]"/> + <xsl:apply-templates mode="entity-heading-attributes-checksum" select="@checksum"/> + </div> + <xsl:apply-templates mode="entity-action-panel" select="."/> + <div class="panel-body caosdb-entity-panel-body"> + <!-- Messages --> + <div class="caosdb-messages"> + <xsl:apply-templates select="Error"> + <xsl:with-param name="class" select="'alert-danger'"/> + </xsl:apply-templates> + <xsl:apply-templates select="Warning"> + <xsl:with-param name="class" select="'alert-warning'"/> + </xsl:apply-templates> + <xsl:apply-templates select="Info"> + <xsl:with-param name="class" select="'alert-info'"/> + </xsl:apply-templates> + </div> + <!-- Properties --> + <ul class="list-group caosdb-properties"> + <xsl:if test="Property"> + <li class="list-group-item caosdb-properties-heading"> + <strong class="small">Properties</strong> + </li> + <xsl:apply-templates mode="entity-body" select="Property"/> + </xsl:if> + </ul> + <!-- Thumbnail --> + <xsl:if test="@path"> + <xsl:call-template name="entity-body-thumbnail"> + <xsl:with-param name="path" select="@path"/> + </xsl:call-template> + <xsl:call-template name="entity-body-video"> + <xsl:with-param name="path" select="@path"/> + </xsl:call-template> + </xsl:if> + <!-- Annotations --> + <xsl:call-template name="annotation-section"> + <xsl:with-param name="entityId" select="@id"/> + </xsl:call-template> + </div> + </div> + </xsl:template> + <!-- Thumbnails of images --> + <xsl:template name="entity-body-thumbnail"> + <xsl:param name="path"/> + <xsl:if test="contains('.jpg.gif.png.svg',translate(substring($path, string-length($path) - 3), 'JPGIFNSV', 'jpgifnsv'))"> + <div class="row"> + <div class="col-sm-12"> + <img class="entity-image-preview" style="max-width: 200px; max-height: 140px;"> + <xsl:attribute name="src"> + <xsl:value-of select="concat($filesystempath,$path)"/> + </xsl:attribute> + </img> + </div> + </div> + </xsl:if> + </xsl:template> + <!-- Video display --> + <xsl:template name="entity-body-video"> + <xsl:param name="path"/> + <xsl:if test="contains('.mp4.mov.webm',translate(substring($path, string-length($path) - 3), 'MPOVWEB', 'mpovweb'))"> + <div class="row"> + <div class="col-sm-12"> + <video controls="controls"> + <source> + <xsl:attribute name="src"> + <xsl:value-of select="concat($filesystempath,$path)"/> + </xsl:attribute> + </source> + </video> + </div> + </div> + </xsl:if> + </xsl:template> + <!-- PROPERTIES --> + <xsl:template match="Property" mode="entity-body"> + <li class="list-group-item caosdb-property-row"> + <xsl:attribute name="id"> + <xsl:value-of select="generate-id()"/> + </xsl:attribute> + <xsl:variable name="collapseid" select="concat('collapseid-',generate-id())"/> + <!-- property heading and value --> + <xsl:apply-templates mode="property-collapsed" select="."> + <xsl:with-param name="collapseid" select="$collapseid"/> + </xsl:apply-templates> + <!-- messages --> + <xsl:apply-templates select="Error"> + <xsl:with-param name="class" select="'alert-danger'"/> + </xsl:apply-templates> + <xsl:apply-templates select="Warning"> + <xsl:with-param name="class" select="'alert-warning'"/> + </xsl:apply-templates> + <xsl:apply-templates select="Info"> + <xsl:with-param name="class" select="'alert-info'"/> + </xsl:apply-templates> + <!-- collapsed data --> + <div class="collapse"> + <xsl:attribute name="id"> + <xsl:value-of select="$collapseid"/> + </xsl:attribute> + <hr class="caosdb-subproperty-divider"/> + <!-- <li> --> + <!-- <a class="caosdb-property-name"> --> + <!-- <xsl:attribute name="href"> --> + <!-- <xsl:value-of select="concat($entitypath,@id)" /></xsl:attribute> --> + <!-- </a> --> + <!-- </li> --> + + <!-- property attributes --> + <xsl:apply-templates mode="property-attributes" select="@description"/> + <xsl:apply-templates mode="property-attributes-id" select="@id"/> + <xsl:apply-templates mode="property-attributes-type" select="@datatype"/> + <xsl:apply-templates mode="property-attributes" select="@*[not(contains('+cuid+id+name+description+datatype+',concat('+',name(),'+')))]"/> + </div> + </li> + </xsl:template> + <xsl:template match="Property" mode="property-collapsed"> + <xsl:param name="collapseid"/> + <div class="row"> + <div class="col-sm-4"> + <h5> + <xsl:if test="@*[not(contains('+cuid+id+name+',concat('+',name(),'+')))]"> + <span class="glyphicon glyphicon-collapse-down" data-toggle="collapse" style="margin-right: 10px;"> + <xsl:attribute name="data-target"> + <xsl:value-of select="concat('#',$collapseid)"/> + </xsl:attribute> + </span> + </xsl:if> + <strong class="caosdb-property-name"> <xsl:value-of select="@name"/></strong> + </h5> + </div> + <!-- property value --> + <div class="col-sm-6 caosdb-property-value"> + <xsl:apply-templates mode="property-value" select="."/> + </div> + <div class="col-sm-6 caosdb-property-edit-value" style="display: none;"></div> + <div class="col-sm-2 caosdb-property-edit" style="text-align: right;"></div> + </div> + </xsl:template> + <xsl:template name="single-value"> + <xsl:param name="value"/> + <xsl:param name="reference"/> + <xsl:param name="boolean"/> + <xsl:if test="normalize-space($value)!=''"> + <xsl:choose> + <xsl:when test="$reference='true' and normalize-space($value)!=''"> + <!-- this is a reference --> + <a class="btn btn-default btn-sm caosdb-resolvable-reference"> + <xsl:attribute name="href"> + <xsl:value-of select="concat($entitypath,normalize-space($value))"/> + </xsl:attribute> + <span class="caosdb-id caosdb-id-button"> + <xsl:value-of select="normalize-space($value)"/> + </span> + <span class="caosdb-resolve-reference-target"/> + </a> + </xsl:when> + <xsl:when test="$boolean='true'"> + <xsl:element name="span"> + <xsl:attribute name="class"> + <xsl:value-of select="concat('caosdb-boolean-',normalize-space($value)='TRUE')"/> + </xsl:attribute> + <xsl:value-of select="normalize-space($value)"/> + </xsl:element> + </xsl:when> + <xsl:otherwise> + <span class="caosdb-property-text-value"> + <xsl:value-of select="$value"/> + </span> + </xsl:otherwise> + </xsl:choose> + </xsl:if> + </xsl:template> + <xsl:template match="Property" mode="property-reference-value-list"> + <xsl:param name="reference"/> + <div class="caosdb-value-list"> + <xsl:element name="div"> + <xsl:attribute name="class">btn-group btn-group-sm caosdb-overflow-content</xsl:attribute> + <xsl:for-each select="Value"> + <xsl:call-template name="single-value"> + <xsl:with-param name="reference"> + <xsl:value-of select="'true'"/> + </xsl:with-param> + <xsl:with-param name="value"> + <xsl:value-of select="text()"/> + </xsl:with-param> + <xsl:with-param name="boolean"> + <xsl:value-of select="'false'"/> + </xsl:with-param> + </xsl:call-template> + </xsl:for-each> + </xsl:element> + </div> + </xsl:template> + <xsl:template match="Property" mode="property-value-list"> + <xsl:param name="reference"/> + <div class="caosdb-value-list"> + <xsl:element name="ol"> + <xsl:attribute name="class">list-group list-inline</xsl:attribute> + <xsl:for-each select="Value"> + <xsl:element name="li"> + <xsl:attribute name="class">list-group-item</xsl:attribute> + <xsl:call-template name="single-value"> + <xsl:with-param name="reference"> + <xsl:value-of select="'false'"/> + </xsl:with-param> + <xsl:with-param name="value"> + <xsl:value-of select="text()"/> + </xsl:with-param> + <xsl:with-param name="boolean"> + <xsl:value-of select="../@datatype='LIST<BOOLEAN>'"/> + </xsl:with-param> + </xsl:call-template> + </xsl:element> + </xsl:for-each> + </xsl:element> + </div> + </xsl:template> + <xsl:template match="Property" mode="property-value"> + <xsl:choose> + <!-- filter out collection data types --> + <xsl:when test="contains(concat('<',@datatype),'<LIST<')"> + <!-- list --> + <xsl:choose> + <xsl:when test="translate(normalize-space(text()),'0123456789','')='' and not(contains('+LIST<INTEGER>+LIST<DOUBLE>+LIST<TEXT>+LIST<BOOLEAN>+LIST<DATETIME>+',concat('+',@datatype,'+')))"> + <xsl:apply-templates mode="property-reference-value-list" select="."/> + </xsl:when> + <xsl:otherwise> + <xsl:apply-templates mode="property-value-list" select="."/> + </xsl:otherwise> + </xsl:choose> + </xsl:when> + <!-- hence, this is no collection --> + <xsl:otherwise> + <xsl:call-template name="single-value"> + <xsl:with-param name="value"> + <xsl:value-of select="text()"/> + </xsl:with-param> + <xsl:with-param name="reference"> + <xsl:value-of select="translate(normalize-space(text()),'0123456789','')='' and not(contains('+INTEGER+DOUBLE+TEXT+BOOLEAN+DATETIME+',concat('+',@datatype,'+')))"/> + </xsl:with-param> + <xsl:with-param name="boolean"> + <xsl:value-of select="@datatype='BOOLEAN'"/> + </xsl:with-param> + </xsl:call-template> + </xsl:otherwise> + </xsl:choose> + <!-- unit --> + <xsl:if test="@unit"> + <span class="caosdb-unit"> + <xsl:value-of select="@unit"/> + </span> + </xsl:if> + </xsl:template> + <xsl:template match="Property" mode="property-value-plain"> + <xsl:choose> + <!-- filter out collection data types --> + <xsl:when test="contains(concat('<',@datatype),'<LIST<')"> + <!-- ignore for now --> + </xsl:when> + <!-- hence, this is no collection --> + <xsl:otherwise> + <xsl:value-of select="text()"/> + </xsl:otherwise> + </xsl:choose> + </xsl:template> + <xsl:template match="@*" mode="property-attributes"> + <div class="row"> + <div class="col-sm-3 col-sm-offset-1"> + <strong> + <xsl:value-of select="name()"/> + </strong> + </div> + <div class="col-sm-8"> + <xsl:value-of select="."/> + </div> + </div> + </xsl:template> + <xsl:template match="@datatype" mode="property-attributes-type"> + <div class="row"> + <div class="col-sm-3 col-sm-offset-1"> + <strong>data type</strong> + </div> + <div class="col-sm-8 caosdb-property-datatype"> + <xsl:value-of select="."/> + </div> + </div> + </xsl:template> + <xsl:template match="@id" mode="property-attributes-id"> + <div class="row"> + <div class="col-sm-3 col-sm-offset-1"> + <strong> + <xsl:value-of select="name()"/> + </strong> + </div> + <div class="col-sm-8"> + <a class="caosdb-property-id"> + <xsl:attribute name="href"> + <xsl:value-of select="concat($entitypath,.)"/> + </xsl:attribute> + <xsl:value-of select="."/> + </a> + </div> + </div> + </xsl:template> + <!-- ANNOTATIONS --> + <xsl:template name="annotation-section"> + <xsl:param name="entityId"/> + <ul class="list-group caosdb-annotation-section"> + <xsl:attribute name="data-entity-id"> + <xsl:value-of select="$entityId"/> + </xsl:attribute> + <li class="list-group-item caosdb-comments-heading"> + <strong class="small">Comments</strong> + <button class="btn btn-link btn-xs pull-right caosdb-new-comment-button"> + <strong>add new comment</strong> + </button> + </li> + </ul> + </xsl:template> +</xsl:stylesheet> diff --git a/src/core/xsl/entity_palette.xsl b/src/core/xsl/entity_palette.xsl new file mode 100644 index 00000000..bbe3a7dc --- /dev/null +++ b/src/core/xsl/entity_palette.xsl @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Simplified interface for editing data models --><!-- A. Schlemmer, 01/2019 --><xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + <xsl:output method="html"/> + + <xsl:template match="/Response"> + <div class="btn-group-vertical"> + <button type="button" class="btn btn-default caosdb-f-edit-panel-new-button new-property">Create new Property</button> + <button type="button" class="btn btn-default caosdb-f-edit-panel-new-button new-recordtype">Create new RecordType</button> + </div> + <div title="Drag and drop Properties from this panel to the Entities on the left." class="panel panel-default"> + <div class="panel-heading"> + <h5>Existing Properties</h5> + </div> + <div class="panel-body"> + <div class="input-group" style="width: 100%;"> + <input class="form-control" placeholder="filter..." title="Type a name (full or partial)." oninput="edit_mode.filter('properties');" id="caosdb-f-filter-properties" type="text"/> + <span class="input-group-btn"> + <button class="btn btn-default caosdb-f-edit-panel-new-button new-property caosdb-f-hide-on-empty-input" title="Create this Property." ><span class="glyphicon glyphicon-plus"></span></button> + </span> + </div> + <ul class="caosdb-v-edit-list"> + <xsl:apply-templates select="./Property"/> + </ul> + </div> + </div> + <div title="Drag and drop RecordTypes from this panel to the Entities on the left." class="panel panel-default"> + <div class="panel-heading"> + <h5>Existing RecordTypes</h5> + </div> + <div class="panel-body"> + <div class="input-group" style="width: 100%;"> + <input class="form-control" placeholder="filter..." title="Type a name (full or partial)." oninput="edit_mode.filter('recordtypes');" id="caosdb-f-filter-recordtypes" type="text"/> + <span class="input-group-btn"> + <button class="btn btn-default caosdb-f-edit-panel-new-button new-recordtype caosdb-f-hide-on-empty-input" title="Create this RecordType"><span class="glyphicon glyphicon-plus"></span></button> + </span> + </div> + <ul class="caosdb-v-edit-list"> + <xsl:apply-templates select="./RecordType"/> + </ul> + </div> + </div> + </xsl:template> + + <xsl:template match="RecordType"> + <xsl:if test="string-length(@name)>0"> + <li class="caosdb-f-edit-drag list-group-item caosdb-v-edit-drag"> + <xsl:attribute name="id">caosdb-f-edit-rt-<xsl:value-of select="@id"/></xsl:attribute> + <xsl:value-of select="@name"/> + </li> + </xsl:if> + </xsl:template> + + <xsl:template match="Property"> + <li class="caosdb-f-edit-drag list-group-item caosdb-v-edit-drag"> + <xsl:attribute name="id">caosdb-f-edit-p-<xsl:value-of select="@id"/></xsl:attribute> + <xsl:value-of select="@name"/> + </li> + </xsl:template> +</xsl:stylesheet> diff --git a/src/core/xsl/filesystem.xsl b/src/core/xsl/filesystem.xsl new file mode 100644 index 00000000..35ce1d71 --- /dev/null +++ b/src/core/xsl/filesystem.xsl @@ -0,0 +1,122 @@ +<!-- + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header +--> +<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + <xsl:output method="html"/> + <xsl:template name="filesystem-cwd"> + <xsl:param name="pardir" select="concat($filesystempath, '/')"/> + <xsl:param name="curdir-name" select="substring-before(substring-after(/Response/dir/@url,$pardir),'/')"/> + <xsl:param name="curdir" select="concat($pardir, $curdir-name, '/')"/> + <xsl:if test="string-length($curdir-name)"> + <xsl:choose> + <xsl:when test="$curdir-name!=/Response/dir/@name"> + <a class="caosdb-fs-cwd" title="Go to this directory."> + <xsl:attribute name="href"> + <xsl:value-of select="$curdir"/> + </xsl:attribute> + <xsl:value-of select="$curdir-name"/> + </a> + <xsl:call-template name="filesystem-cwd"> + <xsl:with-param name="pardir" select="$curdir"/> + </xsl:call-template> + </xsl:when> + <xsl:otherwise> + <span class="caosdb-fs-cwd"> + <xsl:value-of select="$curdir-name"/> + </span> + </xsl:otherwise> + </xsl:choose> + </xsl:if> + </xsl:template> + <xsl:template match="dir" mode="filesystem-item"> + <li class="list-group-item"> + <a class="caosdb-fs-dir"> + <xsl:attribute name="href"> + <xsl:value-of select="concat(/Response/dir/@url, @name)"/> + </xsl:attribute> + <span class="glyphicon"></span> + <xsl:value-of select="@name"/> + </a> + </li> + </xsl:template> + <xsl:template match="file" mode="filesystem-item"> + <xsl:param name="file-uri" select="concat(/Response/dir/@url, @name)"/> + <li class="list-group-item"> + <div class="row"> + <div class="col-sm-6"> + <a class="caosdb-fs-file"> + <xsl:attribute name="href"> + <xsl:value-of select="$file-uri"/> + </xsl:attribute> + <span class="glyphicon"></span> + <xsl:value-of select="@name"/> + </a> + </div> + <div class="col-sm-6 text-right"> + <a class="btn caosdb-fs-btn-file"> + <xsl:attribute name="href"> + <xsl:value-of select="concat($entitypath, @id)"/> + </xsl:attribute> + <span class="label caosdb-label-file">F</span> + <span class="label caosdb-id"> + <xsl:value-of select="@id"/> + </span> + </a> + </div> + </div> + <xsl:call-template name="entity-body-thumbnail"> + <xsl:with-param name="path" select="substring-after($file-uri,$filesystempath)"/> + </xsl:call-template> + </li> + </xsl:template> + <xsl:template match="/Response/dir" mode="top-level-data"> + <div class="container"> + <div class="panel-group"> + <div class="panel panel-default"> + <div class="panel-heading"> + <div class="row"> + <div class="col-sm-8"> + <a title="Go back to the root of the file system."> + <xsl:attribute name="href"> + <xsl:value-of select="concat($filesystempath, '/')"/> + </xsl:attribute> + <strong>File System</strong> + </a> + <xsl:call-template name="filesystem-cwd"/> + </div> + <div class="col-sm-4 text-right"> + <xsl:value-of select="count(dir)"/> Directories and + <xsl:value-of select="count(file)"/> Files + </div> + </div> + </div> + <div class="panel-body"> + <ul class="list-group"> + <xsl:apply-templates mode="filesystem-item" select="dir"/> + <xsl:apply-templates mode="filesystem-item" select="file"/> + </ul> + </div> + </div> + </div> + </div> + </xsl:template> +</xsl:stylesheet> diff --git a/src/core/xsl/main.xsl b/src/core/xsl/main.xsl new file mode 100644 index 00000000..90467f18 --- /dev/null +++ b/src/core/xsl/main.xsl @@ -0,0 +1,162 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header +--> +<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + <xsl:output method="html"/> + <xsl:include href="common.xsl"/> + <xsl:variable name="basepath"> + <xsl:call-template name="uri_ends_with_slash"> + <xsl:with-param name="uri"> + <xsl:value-of select="/Response/@baseuri"/> + </xsl:with-param> + </xsl:call-template> + </xsl:variable> + <xsl:variable name="entitypath" select="concat($basepath,'Entity/')"/> + <xsl:variable name="filesystempath" select="concat($basepath,'FileSystem')"/> + <xsl:variable name="lowercase" select="'abcdefghijklmnopqrstuvwxyz'"/> + <xsl:variable name="uppercase" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/> + <xsl:template name="make-filesystem-link"> + <xsl:param name="href"/> + <xsl:param name="display" select="$href"/> + <a> + <xsl:attribute name="href"> + <xsl:value-of select="concat($filesystempath,$href)"/> + </xsl:attribute> + <xsl:value-of select="$display"/> + </a> + </xsl:template> + <xsl:template name="caosdb-head-title"> + <title>CaosDB</title> + </xsl:template> + <xsl:template name="caosdb-head-css"> + <xsl:element name="link"> + <xsl:attribute name="rel">stylesheet</xsl:attribute> + <xsl:attribute name="href"> + <xsl:value-of select="concat($basepath,'webinterface/css/bootstrap.css')"/> + </xsl:attribute> + </xsl:element> + <xsl:element name="link"> + <xsl:attribute name="rel">stylesheet</xsl:attribute> + <xsl:attribute name="href"> + <xsl:value-of select="concat($basepath,'webinterface/css/webcaosdb.css')"/> + </xsl:attribute> + </xsl:element> + </xsl:template> + <xsl:template name="caosdb-head-js"> + <xsl:element name="script"> + <xsl:attribute name="src"> + <xsl:value-of select="concat($basepath,'webinterface/js/jquery.js')"/> + </xsl:attribute> + </xsl:element> + <xsl:element name="script"> + <xsl:attribute name="src"> + <xsl:value-of select="concat($basepath,'webinterface/js/bootstrap.js')"/> + </xsl:attribute> + </xsl:element> + <xsl:element name="script"> + <xsl:attribute name="src"> + <xsl:value-of select="concat($basepath,'webinterface/js/state-machine.js')"/> + </xsl:attribute> + </xsl:element> + <xsl:element name="script"> + <xsl:attribute name="src"> + <xsl:value-of select="concat($basepath,'webinterface/js/showdown.js')"/> + </xsl:attribute> + </xsl:element> + <xsl:element name="script"> + <xsl:attribute name="src"> + <xsl:value-of select="concat($basepath,'webinterface/js/webcaosdb.js')"/> + </xsl:attribute> + </xsl:element> + <xsl:element name="script"> + <xsl:attribute name="src"> + <xsl:value-of select="concat($basepath,'webinterface/js/caosdb.js')"/> + </xsl:attribute> + </xsl:element> + <xsl:element name="script"> + <xsl:attribute name="src"> + <xsl:value-of select="concat($basepath,'webinterface/js/preview.js')"/> + </xsl:attribute> + </xsl:element> + <xsl:element name="script"> + <xsl:attribute name="src"> + <xsl:value-of select="concat($basepath,'webinterface/js/ext_references.js')"/> + </xsl:attribute> + </xsl:element> + <xsl:element name="script"> + <xsl:attribute name="src"> + <xsl:value-of select="concat($basepath,'webinterface/js/templates_ext.js')"/> + </xsl:attribute> + </xsl:element> + <xsl:element name="script"> + <xsl:attribute name="src"> + <xsl:value-of select="concat($basepath,'webinterface/js/annotation.js')"/> + </xsl:attribute> + </xsl:element> + <xsl:element name="script"> + <xsl:attribute name="src"> + <xsl:value-of select="concat($basepath,'webinterface/js/edit_mode.js')"/> + </xsl:attribute> + </xsl:element> + <!--EXTENSIONS--> + <script> + window.sessionStorage.caosdbBasePath = "<xsl:value-of select="$basepath"/>"; + $(document).ready(() => paging.initPaging(window.location.href, <xsl:value-of select="/Response/@count"/> )); + </script> + </xsl:template> + <xsl:template name="caosdb-data-container"> + <div class="container caosdb-f-main"> + <div class="row"> + <div class="panel-group caosdb-f-main-entities"> + <xsl:apply-templates select="/Response/UserInfo"/> + <xsl:apply-templates mode="top-level-data" select="/Response/*"/> + <xsl:apply-templates mode="query-results" select="/Response/Query"/> + <xsl:if test="not(/Response/Query/Selection)"> + <xsl:apply-templates mode="entities" select="/Response/*"/> + </xsl:if> + </div> + <div class="panel panel-warning caosdb-f-edit caosdb-v-edit-panel caosdb-v-edit-panel hidden"> + <div class="panel-heading"> + <h3 class="panel-title">Edit Mode Toolbox</h3> + </div> + <div class="caosdb-f-edit-panel-body panel-body"></div> + </div> + </div> + </div> + </xsl:template> + <xsl:template match="*" mode="entities"/> + <xsl:template match="*" mode="top-level-data"/> + <xsl:variable name="close-char" select="'×'"/> + <!-- assure that this uri ends with a '/' --> + <xsl:template name="uri_ends_with_slash"> + <xsl:param name="uri"/> + <xsl:choose> + <xsl:when test="substring($uri,string-length($uri),1)!='/'"> + <xsl:value-of select="concat($uri,'/')"/> + </xsl:when> + <xsl:otherwise> + <xsl:value-of select="$uri"/> + </xsl:otherwise> + </xsl:choose> + </xsl:template> +</xsl:stylesheet> diff --git a/src/core/xsl/messages.xsl b/src/core/xsl/messages.xsl new file mode 100644 index 00000000..eab7d2ce --- /dev/null +++ b/src/core/xsl/messages.xsl @@ -0,0 +1,75 @@ +<!-- + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header +--> +<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + <xsl:output method="html"/> + <xsl:template match="Error|Warning|Info"> + <xsl:param name="class"/> + <div> + <xsl:attribute name="class">alert + <xsl:value-of select="$class"/> alert-dismissable fade in</xsl:attribute> + <a class="close" data-dismiss="alert" href="#"> + <xsl:value-of select="$close-char"/> + </a> + <strong> + <xsl:value-of select="name()"/> + </strong> + <xsl:value-of select="@description"/> + <div class="small"> + <xsl:value-of select="text()"/> + </div> + </div> + </xsl:template> + <xsl:template match="script" mode="entities"> + <div class="container" id="caosdb-container-script"> + <div class="panel panel-default"> + <div class="panel-heading"> + <div class="row"> + <div class="col-sm-8" id="caosdb-caption-script"> + Output of the Script + </div> + <div class="col-sm-4 text-right"> + Code: <span id="caosdb-return-code"><xsl:value-of select="@code"/></span></div> + </div> + </div> + </div> + <xsl:apply-templates select="stderr"/> + <xsl:apply-templates select="stdout"/> + </div> + </xsl:template> + <xsl:template match="stderr"> + <div class="panel panel-default" id="caosdb-container-stderr"> + <div class="panel-heading" id="caosdb-caption-stderr">Errors:</div> + <div class="alert" id="caosdb-stderr"> + <xsl:value-of select="text()"/> + </div> + </div> + </xsl:template> + <xsl:template match="stdout"> + <div class="panel panel-default" id="caosdb-container-stdout"> + <div class="panel-heading" id="caosdb-caption-stdout">Standard Messages:</div> + <div id="caosdb-stdout"> + <xsl:value-of select="text()"/> + </div> + </div> + </xsl:template> +</xsl:stylesheet> diff --git a/src/core/xsl/navbar.xsl b/src/core/xsl/navbar.xsl new file mode 100644 index 00000000..0619becc --- /dev/null +++ b/src/core/xsl/navbar.xsl @@ -0,0 +1,212 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header +--> +<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + <xsl:output method="html"/> + <xsl:template name="make-retrieve-all-link"> + <xsl:param name="entity"/> + <xsl:param name="display"/> + <xsl:param name="paging" select="'0L10'"/> + <li> + <a> + <xsl:attribute name="href"> + <xsl:value-of select="concat($entitypath, '?all=', normalize-space($entity))"/> + <xsl:if test="$paging"> + <xsl:value-of select="concat('&P=',$paging)"/> + </xsl:if> + </xsl:attribute> + <xsl:value-of select="$display"/> + </a> + </li> + </xsl:template> + <xsl:template name="caosdb-top-navbar"> + <nav class="navbar navbar-default navbar-fixed-top"> + <div class="container-fluid"> + <div class="navbar-header"> + <button class="navbar-toggle" data-target="#top-navbar" data-toggle="collapse" type="button"> + <span class="icon-bar"></span> + <span class="icon-bar"></span> + <span class="icon-bar"></span> + </button> + <a class="navbar-brand"> + <xsl:element name="img"> + <xsl:attribute name="class">caosdb-logo</xsl:attribute> + <xsl:attribute name="src"> + <xsl:value-of select="concat($basepath,'webinterface/pics/caosdb_logo_medium.png')"/> + </xsl:attribute> + </xsl:element> + CaosDB + </a> + </div> + <div class="collapse navbar-collapse" id="top-navbar"> + <xsl:if test="/Response/UserInfo"> + <ul class="nav navbar-nav caosdb-navbar"> + <li class="dropdown" id="caosdb-navbar-entities"> + <a class="dropdown-toggle" data-toggle="dropdown" href="#"> + Entities + <span class="caret"></span></a> + <ul class="dropdown-menu"> + <li class="dropdown-header">Retrieve all:</li> + <xsl:call-template name="make-retrieve-all-link"> + <xsl:with-param name="entity"> + Entity + </xsl:with-param> + <xsl:with-param name="display"> + Entities + </xsl:with-param> + </xsl:call-template> + <xsl:call-template name="make-retrieve-all-link"> + <xsl:with-param name="entity"> + Record + </xsl:with-param> + <xsl:with-param name="display"> + Records + </xsl:with-param> + </xsl:call-template> + <xsl:call-template name="make-retrieve-all-link"> + <xsl:with-param name="entity"> + RecordType + </xsl:with-param> + <xsl:with-param name="display"> + RecordTypes + </xsl:with-param> + </xsl:call-template> + <xsl:call-template name="make-retrieve-all-link"> + <xsl:with-param name="entity"> + Property + </xsl:with-param> + <xsl:with-param name="display"> + Properties + </xsl:with-param> + </xsl:call-template> + <xsl:call-template name="make-retrieve-all-link"> + <xsl:with-param name="entity"> + File + </xsl:with-param> + <xsl:with-param name="display"> + Files + </xsl:with-param> + </xsl:call-template> + </ul> + </li> + <li id="caosdb-navbar-filesystem"> + <xsl:call-template name="make-filesystem-link"> + <xsl:with-param name="href" select="'/'"/> + <xsl:with-param name="display" select="'File System'"/> + </xsl:call-template> + </li> + <li id="caosdb-navbar-query"> + <button class="navbar-btn btn btn-link" data-target="#caosdb-query-panel" data-toggle="collapse"> + Query + </button> + </li> + </ul> + </xsl:if> + <ul class="nav navbar-nav navbar-right"> + <xsl:call-template name="caosdb-user-menu"/> + </ul> + </div> + <!-- query panel --> + <div class="collapse" id="caosdb-query-panel"> + <xsl:call-template name="caosdb-query-panel"/> + </div> + </div> + <xsl:apply-templates select="/Response/Error"> + <xsl:with-param name="class" select="'alert-danger'"/> + </xsl:apply-templates> + <xsl:apply-templates select="/Response/Warning"> + <xsl:with-param name="class" select="'alert-warning'"/> + </xsl:apply-templates> + <xsl:apply-templates select="/Response/Info"> + <xsl:with-param name="class" select="'alert-info'"/> + </xsl:apply-templates> + </nav> + <div class="container" id="subnav"/> + </xsl:template> + <xsl:template match="Role" name="caosdb-user-roles"> + <div class="caosdb-user-role"> + <xsl:value-of select="text()"/> + </div> + </xsl:template> + <xsl:template match="UserInfo" name="caosdb-user-info"> + <div class="caosdb-user-info" style="display: none;"> + <div class="caosdb-user-name"> + <xsl:value-of select="@username"/> + </div> + <div class="caosdb-user-realm"> + <xsl:value-of select="@realm"/> + </div> + <xsl:apply-templates select="Roles/Role"/> + </div> + </xsl:template> + <xsl:template name="caosdb-user-menu"> + <xsl:choose> + <xsl:when test="/Response/@username"> + <li class="dropdown" id="user-menu"> + <a class="dropdown-toggle" data-toggle="dropdown" href="#"> + <xsl:value-of select="concat(/Response/@username,' ')"/> + <span class="glyphicon glyphicon-user"/> + <span class="caret"></span> + </a> + <ul class="dropdown-menu"> + <li> + <a title="Click to logout."> + <xsl:attribute name="href"> + <xsl:value-of select="concat($basepath, 'logout')"/> + </xsl:attribute> + Logout + <span class="glyphicon glyphicon-log-out"/></a> + </li> + </ul> + </li> + </xsl:when> + <xsl:otherwise> + <li id="user-menu"> + <form class="navbar-form" method="POST"> + <xsl:attribute name="action"> + <xsl:value-of select="concat($basepath, 'login')"/> + </xsl:attribute> + <input class="form-control" id="username" name="username" placeholder="username" type="text"/> + <input class="form-control" id="password" name="password" placeholder="password" type="password"/> + <button class="btn btn-default" type="submit"> + <span class="glyphicon glyphicon-log-in"></span> + Login + </button> + </form> + </li> + </xsl:otherwise> + </xsl:choose> + </xsl:template> + <xsl:template name="paging-panel"> + <div class="container caosdb-paging-panel" style="display: none"> + <ul class="pager"> + <li class="previous"> + <a class="caosdb-prev-button">Previous Page</a> + </li> + <li class="next"> + <a class="caosdb-next-button">Next Page</a> + </li> + </ul> + </div> + </xsl:template> +</xsl:stylesheet> diff --git a/src/core/xsl/parent.xsl b/src/core/xsl/parent.xsl new file mode 100644 index 00000000..12345d58 --- /dev/null +++ b/src/core/xsl/parent.xsl @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf8"?> +<!-- + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header +--> +<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + <xsl:output method="html"/> + <xsl:template match="/Response"> + <xsl:apply-templates select="./RecordType"/> + </xsl:template> + <xsl:template match="RecordType"> + <span class="caosdb-parent-item small"> + <xsl:attribute name="id"> + <xsl:value-of select="generate-id()"/> + </xsl:attribute> + <span class="caosdb-f-parent-actions-panel"> + </span> + <a class="caosdb-parent-name"> + <xsl:attribute name="href"> + <xsl:value-of select="concat($entitypath, @id)"/> + </xsl:attribute> + <xsl:value-of select="@name"/> + </a> + </span> + </xsl:template> +</xsl:stylesheet> diff --git a/src/core/xsl/property.xsl b/src/core/xsl/property.xsl new file mode 100644 index 00000000..d3524fbd --- /dev/null +++ b/src/core/xsl/property.xsl @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf8"?> +<!-- + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header +--> +<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + <xsl:output method="html"/> + <xsl:template match="/Response"> + <xsl:apply-templates mode="entity-body" select="./Property"/> + </xsl:template> +</xsl:stylesheet> diff --git a/src/core/xsl/query.xsl b/src/core/xsl/query.xsl new file mode 100644 index 00000000..9375354a --- /dev/null +++ b/src/core/xsl/query.xsl @@ -0,0 +1,232 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header +--> +<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + <xsl:output method="html"/> + <xsl:param name="lowercase" select="'abcdefghijklmnopqrstuvwxyz'"/> + <xsl:param name="uppercase" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/> + <xsl:template match="ParseTree" mode="query-results"> + <xsl:apply-templates select="ParsingError"/> + </xsl:template> + <xsl:template match="ParseTree/ParsingError" mode="query-results"> + <div class="panel-body"> + <div class="caosdb-overflow-box"> + <div class="caosdb-overflow-content"> + <span>ParseTree:</span> + <xsl:value-of select="text()"/> + </div> + <div class="caosdb-overflow-contents"> + <span> + <xsl:value-of select="@type"/> + <xsl:text>, line </xsl:text> + <xsl:value-of select="@line"/> + <xsl:text>, character </xsl:text> + <xsl:value-of select="@character"/> + <xsl:text>:</xsl:text> + </span> + <xsl:value-of select="text()"/> + </div> + </div> + </div> + </xsl:template> + <xsl:template match="Query" mode="query-results"> + <div class="panel panel-default caosdb-query-response"> + <div class="panel-heading caosdb-query-response-heading"> + <div class="row"> + <div class="col-sm-10 caosdb-overflow-box"> + <div class="caosdb-overflow-content"> + <span>Query: </span> + <xsl:value-of select="@string"/> + </div> + </div> + <div class="col-sm-2 text-right"> + <span>Results: </span> + <span class="caosdb-query-response-results"> + <xsl:value-of select="@results"/> + </span> + </div> + </div> + </div> + <xsl:if test="@results=0"> + <div class="panel panel-default caosdb-no-results"> + <div class="alert alert-warning" role="alert"> + There were no results for this query. + </div> + </div> + </xsl:if> + <xsl:apply-templates mode="query-results" select="./ParseTree"/> + </div> + <xsl:apply-templates mode="select-table" select="./Selection"/> + </xsl:template> + <xsl:template match="Selection" mode="select-table"> + <div class="panel panel-default caosdb-select-table"> + <div class="panel-heading"> + <div class="container-fluid panel-container"> + <div class="col-xs-6"> + <h5>Table of selected fields</h5> + </div> + <div class="col-xs-6 text-right"> + <!-- Trigger the modal with a button --> + <button class="btn btn-info btn-sm" data-target="#downloadModal" data-toggle="modal" type="button">Download this table</button> + <!-- Modal --> + <div class="modal fade text-left" id="downloadModal" role="dialog"> + <div class="modal-dialog"> + <!-- Modal content--> + <div class="modal-content"> + <div class="modal-header"> + <button class="close" data-dismiss="modal" type="button">×</button> + <h4 class="modal-title">Download this table</h4> + </div> + <div class="modal-body"> + <p> + <a> + <xsl:attribute name="href"> + data:text/csv;charset=utf-8,<xsl:for-each select="Selector"><xsl:value-of select="@name"/>%09</xsl:for-each><xsl:for-each select="/Response/*[@id]"><xsl:call-template name="select-table-row-plain"><xsl:with-param name="entity-id" select="@id"/></xsl:call-template></xsl:for-each></xsl:attribute> + Download TSV File + </a> + </p> + <hr/> + <p> + <small>Download this dataset in Python with:</small> + </p> + <p> + <code> + data = caosdb.execute_query('<xsl:value-of select="//Response/Query/@string"/>') + </code> + </p> + </div> + <div class="modal-footer"> + <button class="btn btn-default" data-dismiss="modal" type="button">Close</button> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + <div class="caosdb-select-table-actions-panel text-right btn-group-xs"></div> + <div class="panel-body"> + <div class="table-responsive"> + <table class="table table-hover"> + <thead> + <tr> + <th></th> + <xsl:for-each select="Selector[@name!='id']"> + <th> + <xsl:value-of select="@name"/> + </th> + </xsl:for-each> + </tr> + </thead> + <tbody> + <xsl:for-each select="/Response/*[@id]"> + <xsl:call-template name="select-table-row"> + <xsl:with-param name="entity-id" select="@id"/> + </xsl:call-template> + </xsl:for-each> + </tbody> + </table> + </div> + </div> + </div> + </xsl:template> + <xsl:template name="entity-link"> + <xsl:param name="entity-id"/> + <a class="btn btn-default btn-sm caosdb-select-id"> + <xsl:attribute name="href"> + <xsl:value-of select="concat($entitypath, $entity-id)"/> + </xsl:attribute> + <!-- <xsl:value-of select="$entity-id" /> --> + <span class="caosdb-select-id-target"> + <span class="glyphicon glyphicon-new-window"/> + </span> + </a> + </xsl:template> + <xsl:template name="select-table-row"> + <xsl:param name="entity-id"/> + <tr> + <td> + <xsl:call-template name="entity-link"> + <xsl:with-param name="entity-id" select="$entity-id"/> + </xsl:call-template> + </td> + <xsl:for-each select="/Response/Query/Selection/Selector[@name!='id']"> + <xsl:call-template name="select-table-cell"> + <xsl:with-param name="entity-id" select="$entity-id"/> + <xsl:with-param name="field-name" select="translate(@name, $uppercase, $lowercase)"/> + </xsl:call-template> + </xsl:for-each> + </tr> + </xsl:template> + <xsl:template name="select-table-cell"> + <xsl:param name="entity-id"/> + <xsl:param name="field-name"/> + <td> + <div class="caosdb-v-property-value"> + <xsl:choose> + <xsl:when test="/Response/*[@id=$entity-id]/@*[translate(name(),$uppercase, $lowercase)=$field-name]"> + <xsl:value-of select="/Response/*[@id=$entity-id]/@*[translate(name(), $uppercase, $lowercase)=$field-name]"/> + </xsl:when> + <xsl:when test="/Response/*[@id=$entity-id]/Property[translate(@name, $uppercase, $lowercase)=$field-name]"> + <xsl:apply-templates mode="property-value" select="/Response/*[@id=$entity-id]/Property[translate(@name, $uppercase, $lowercase)=$field-name]"/> + </xsl:when> + </xsl:choose> + </div> + </td> + </xsl:template> + <!-- For CSV download --> + <!-- This block is responsible for generating the downloadable TSV. --> + <xsl:template name="select-table-row-plain"> + <xsl:param name="entity-id"/> +%0A<xsl:for-each select="/Response/Query/Selection/Selector"><xsl:call-template name="select-table-cell-plain"><xsl:with-param name="entity-id" select="$entity-id"/><xsl:with-param name="field-name" select="translate(@name, $uppercase, $lowercase)"/></xsl:call-template><xsl:if test="position()!=last()">%09</xsl:if></xsl:for-each></xsl:template> + <xsl:template name="select-table-cell-plain"> + <xsl:param name="entity-id"/> + <xsl:param name="field-name"/> + <xsl:choose> + <xsl:when test="/Response/*[@id=$entity-id]/@*[translate(name(),$uppercase, $lowercase)=$field-name]"> + <xsl:value-of select="/Response/*[@id=$entity-id]/@*[translate(name(), $uppercase, $lowercase)=$field-name]"/> + </xsl:when> + <xsl:when test="/Response/*[@id=$entity-id]/Property[translate(@name, $uppercase, $lowercase)=$field-name]"> + <xsl:apply-templates mode="property-value-plain" select="/Response/*[@id=$entity-id]/Property[translate(@name, $uppercase, $lowercase)=$field-name]"></xsl:apply-templates> + </xsl:when> + </xsl:choose> + </xsl:template> + <xsl:template name="caosdb-query-panel"> + <div class="container caosdb-query-panel"> + <form class="panel" id="caosdb-query-form" method="GET"> + <xsl:attribute name="action"> + <xsl:value-of select="$entitypath"/> + </xsl:attribute> + <input id="caosdb-query-paging-input" name="P" type="hidden" value="0L10"/> + <div class="input-group"> + <input class="form-control" id="caosdb-query-textarea" name="query" placeholder="E.g. 'FIND Experiment'" rows="1" style="resize: vertical;" type="text"></input> + <span class="input-group-addon btn btn-default caosdb-search-btn"> + <a href="#" title="Click to execute the query."> + <span class="glyphicon glyphicon-search"></span> + </a> + </span> + </div> + </form> + </div> + </xsl:template> +</xsl:stylesheet> diff --git a/src/core/xsl/welcome.xsl b/src/core/xsl/welcome.xsl new file mode 100644 index 00000000..9369dfcc --- /dev/null +++ b/src/core/xsl/welcome.xsl @@ -0,0 +1,40 @@ +<!-- + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header +--> +<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + <xsl:output method="html"/> + <xsl:template name="squarefield"> + <xsl:param name="content"/> + <xsl:element name="div"> + <xsl:attribute name="class">caosdb-square</xsl:attribute> + <xsl:element name="div"> + <xsl:attribute name="class">caosdb-square-content</xsl:attribute> + <xsl:value-of select="$content"/> + </xsl:element> + </xsl:element> + </xsl:template> + <xsl:template name="welc_filesystem_button"> + <xsl:param name="baseloc"> + <xsl:value-of select="$filesystempath"/> + </xsl:param> + </xsl:template> +</xsl:stylesheet> diff --git a/src/ext/README b/src/ext/README new file mode 100644 index 00000000..9f476656 --- /dev/null +++ b/src/ext/README @@ -0,0 +1 @@ +This directory should not contain any files. It is a placeholder for the extensions which are copied to this directory from other repositories. diff --git a/test/core/index.html b/test/core/index.html new file mode 100644 index 00000000..e9833d56 --- /dev/null +++ b/test/core/index.html @@ -0,0 +1,56 @@ +<!DOCTYPE html> +<!-- + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header +--> +<html> +<head> + <meta charset="utf-8"/> + <title>WebCaosDB Unit Tests</title> + <link rel="stylesheet" href="css/qunit.css"/> + <link rel="stylesheet" href="css/webcaosdb.css"/> +</head> +<body> + <div id="qunit"></div> + <div id="qunit-fixture"></div> + <script src="js/jquery.js"></script> + <script src="js/webcaosdb.js"></script> + <script src="js/caosdb.js"></script> + <script src="js/state-machine.js"></script> + <script src="js/showdown.js"></script> + <script src="js/qunit.js"></script> + <script src="js/setup.js"></script> + <script src="js/preview.js"></script> + <script src="js/annotation.js"></script> + <script src="js/edit_mode.js"></script> + <script src="js/templates_ext.js"></script> + <script src="js/ext_references.js"></script> + <!--EXTENSIONS--> + <script src="js/modules/webcaosdb.js.js"></script> + <script src="js/modules/caosdb.js.js"></script> + <script src="js/modules/webcaosdb.css.js"></script> + <script src="js/modules/entity.xsl.js"></script> + <script src="js/modules/welcome.xsl.js"></script> + <script src="js/modules/query.xsl.js"></script> + <script src="js/modules/annotation.xsl.js"></script> + <script src="js/modules/navbar.xsl.js"></script> +</body> +</html> diff --git a/test/core/js/modules/annotation.xsl.js b/test/core/js/modules/annotation.xsl.js new file mode 100644 index 00000000..20815816 --- /dev/null +++ b/test/core/js/modules/annotation.xsl.js @@ -0,0 +1,102 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ +/* Testing the construction of the html elements for annotations of entities via xsl transformation */ + +/* SETUP */ +QUnit.module("annotation.xsl", { + before : function(assert) { + // load entity.xsl + var done = assert.async(); + var qunit_obj = this; + annotation.loadAnnotationXsl("../../").then(function(data){ + qunit_obj.annotationXSL = data; + done(); + }); + + + this.testCases = []; + this.testCases[0] = '<Response><Record><Property name="annotationOf"/><History transaction="INSERT" datetime="2015-12-24T20:15:00" username="someuser"/><Property name="comment">This is a comment</Property></Record><Record><Property name="annotationOf"/></Record></Response>'; + assert.ok(str2xml(this.testCases[0])); + } +}); + +QUnit.test("Root rule returns DIV element", function(assert){ + var xml_str = this.testCases[0]; + var xml = str2xml(xml_str); + + assert.equal(xslt(xml, this.annotationXSL).firstChild.tagName, "DIV"); +}); + +QUnit.test("Record rule returns li elements", function(assert){ + var xml_str = this.testCases[0]; + var xml = str2xml(xml_str); + + var annos = xslt(xml, this.annotationXSL).firstChild.children; +// assert.equal(annossec.length, 1, "one child"); +// assert.equal(annossec[0].tagName, "UL", "annossec is ul"); +// var annos = annossec[0].children; + assert.equal(annos.length, 2, "two li"); + assert.equal(annos[0].tagName, "LI", "1st"); + assert.equal(annos[0].className, "list-group-item", "heading found"); + assert.equal(annos[0].children.length, 1, "1st has one child."); + assert.equal(annos[1].tagName, "LI", "2nd"); + + var media = annos[0].children[0]; + assert.equal(media.tagName, "DIV", "is DIV"); + assert.equal(media.className, "media", "className is media"); + assert.equal(media.children.length, 2, "media has two children"); + assert.equal(media.children[0].className, "media-left"); + assert.equal(media.children[1].className, "media-body"); + +}); + +QUnit.test("History element", function(assert){ + var xml_str = this.testCases[0]; + var xml = str2xml(xml_str); + + var html = xslt(xml, this.annotationXSL); + var mediaBody = html.firstChild.getElementsByClassName("media-body")[0]; + assert.ok(mediaBody, "media-body is there"); + assert.ok(mediaBody.children.length>0,"media-body has children"); + var mediaHeading = mediaBody.getElementsByClassName("media-heading")[0]; + assert.ok(mediaHeading, "media-heading is there"); + assert.equal(mediaHeading.parentNode, mediaBody, "media-heading is child of media-body"); + + assert.ok(xml2str(mediaHeading).indexOf("someuser")!==-1, "username is there"); + assert.ok(xml2str(mediaHeading).indexOf("2015-12-24T20:15:00")!==-1, "datetime is there"); +}); + +QUnit.test("Comment text", function(assert){ + var xml_str = this.testCases[0]; + var xml = str2xml(xml_str); + + var html = xslt(xml, this.annotationXSL); + + var mediaBody = html.firstChild.getElementsByClassName("media-body")[0]; + assert.ok(mediaBody, "media-body is there"); + assert.ok(mediaBody.children.length>0,"media-body has children"); + var commentText = mediaBody.getElementsByClassName("caosdb-comment-annotation-text")[0]; + assert.ok(commentText, "comment-text element is there"); + assert.equal(commentText.children.length + commentText.childNodes.length, 1, "comment-text element has one child."); + assert.equal(commentText.innerHTML, "This is a comment", "comment text is correct."); +}); diff --git a/test/core/js/modules/caosdb.js.js b/test/core/js/modules/caosdb.js.js new file mode 100644 index 00000000..9de2e659 --- /dev/null +++ b/test/core/js/modules/caosdb.js.js @@ -0,0 +1,394 @@ +/** + * Tests for the high level java script client. + * @author Alexander Schlemmer + * Started in 08/2018 + **/ + + + +// Module initialization +QUnit.module("caosdb.js", { + before: function(assert) { + var done = assert.async(); + let string_test_document = ` +<Response> + <Record name="nameofrecord"> + <Parent name="bla" /> + <Property name="A" datatype="TEXT">245</Property> + </Record> + + <Record> + <Parent name="bla" /> + + </Record> + + + <Record name="nameofrec" id="17"> + <Parent name="bla" id="244" /> + <Parent name="bla2" id="217" /> + + <Property name="B" datatype="TEXT">245</Property> + <Property name="A" datatype="DOUBLE">245.0</Property> + <Property name="A" datatype="TEXT">245</Property> + </Record> + +<Record description="This record has no name."> + <Parent name="bla" /> + + <Property name="B" datatype="Uboot">245</Property> + <Property name="A" datatype="INTEGER">245</Property> + <Property name="A" datatype="LIST<INTEGER>"><Value>245</Value></Property> + <Property name="A" datatype="LIST<INTEGER>"></Property> + + <Property name="A" datatype="LIST<INTEGER>"> + <Value>245</Value> + <Value>245</Value> + </Property> + + <Property name="A" datatype="LIST<Uboot>"> + <Value>245</Value> + <Value>247</Value> + <Value> + 299 + </Value> + </Property> + </Record> + +</Response> +`; + var donecounter = 0; + var donemax = 2; + + doneinc = function() { + donecounter++; + if (donecounter == donemax) { + done(); + } + }; + + this.xml_test_document = str2xml(string_test_document); + + transformation.transformEntities(this.xml_test_document).then(x => { + this.x = x; + doneinc(); + }); + + + let string_test_document2 = ` +<Response> + <UserInfo username="max" realm="PAM"> + <Roles> + <Role>administration</Role> + </Roles> + </UserInfo> + + <Record name="nameofrecord"> + <Parent name="bla" /> + <Property name="A" datatype="TEXT">245</Property> + </Record> + +</Response> + + `; + this.xml_test_document2 = str2xml(string_test_document2); + + transformation.transformEntities(this.xml_test_document2).then(x => { + this.userInfoTest = x; + doneinc(); + }); + } +}); + +/** + * @author Alexander Schlemmer + * Test user info functions in client. + * + * TODO: Not possible right now, because transformEntities does not transform UserInfo. + */ +// QUnit.test("userInfo", function(assert) { +// assert.equal(getUserName(), "max"); +// }); + +/** + * @author Alexander Schlemmer + * Test whether properties are parsed correctly from the document tree. + */ +QUnit.test("getProperties", function(assert) { + try { + ps = getProperties(); + } + catch (e) { + assert.equal(e.message, "element is undefined"); + } + try { + ps = getProperties(undefined); + } + catch (e) { + assert.equal(e.message, "element is undefined"); + } + + assert.equal(this.x.length, 4); + + let ps = getProperties(this.x[0]); + assert.equal(ps.length, 1); + assert.equal(ps[0].name, "A"); + assert.equal(ps[0].datatype, "TEXT"); + assert.equal(ps[0].value, 245); + + ps = getProperties(this.x[1]); + assert.equal(ps.length, 0); + + ps = getProperties(this.x[2]); + assert.equal(ps.length, 3); + assert.equal(ps[1].name, ps[2].name); + assert.notEqual(ps[0].name, ps[2].name); + assert.notEqual(ps[1].duplicateIndex, ps[2].duplicateIndex); + assert.equal(ps[0].duplicateIndex, 0); + assert.notEqual(ps[1].datatype, ps[2].datatype); + assert.equal(ps[0].datatype, "TEXT"); +}); + +/** + * @author Alexander Schlemmer + * Test whether parents are retrieved correctly. + */ +QUnit.test("getParents", function(assert) { + par1 = getParents(this.x[1]) + par2 = getParents(this.x[2]) + + assert.equal(par1.length, 1); + assert.equal(par2.length, 2); + + assert.equal(par1[0].name, "bla") + assert.equal(par1[0].id, undefined) + + assert.equal(par2[0].name, "bla") + assert.equal(par2[0].id, "244") + assert.equal(par2[1].name, "bla2") + assert.equal(par2[1].id, "217") +}); + +/** + * @author Alexander Schlemmer + * Test whether lists and references are parsed correctly. + */ +QUnit.test("listProperties", function(assert) { + console.log(this.x[3]); + assert.equal(getPropertyElements(this.x[3]).length, 6); + ps = getProperties(this.x[3]); + assert.equal(ps.length, 6); + + assert.equal(ps[0].datatype, "Uboot"); + assert.equal(ps[0].reference, true); + assert.equal(ps[0].list, false); + + assert.equal(ps[1].datatype, "INTEGER"); + assert.equal(ps[1].reference, false); + assert.equal(ps[1].list, false); + + console.log(ps[2]); + assert.equal(ps[2].datatype, "LIST<INTEGER>"); + assert.equal(ps[2].reference, false); + assert.equal(ps[2].list, true); + assert.deepEqual(ps[2].value, ["245"]); + + console.log(ps[3]); + assert.equal(ps[3].datatype, "LIST<INTEGER>"); + assert.equal(ps[3].reference, false); + assert.equal(ps[3].list, true); + assert.deepEqual(ps[3].value, []); + + assert.equal(ps[4].datatype, "LIST<INTEGER>"); + assert.equal(ps[4].reference, false); + assert.equal(ps[4].list, true); + assert.deepEqual(ps[4].value, ["245", "245"]); + + assert.equal(ps[5].datatype, "LIST<Uboot>"); + assert.equal(ps[5].reference, true); + assert.equal(ps[5].list, true); + +}); + +/** + * @author Alexander Schlemmer + * Test setting of properties. + */ +QUnit.test("setProperties", function(assert) { + var newdoc = []; + for (var i=0; i<this.x.length; i++) { + newdoc.push(this.x[i].cloneNode(true)); + } + + // Set one property: + setProperty(newdoc[2], {name: "B", value: 246}); + ps = getProperties(newdoc[2]); + assert.equal(ps[0].name, "B"); + assert.equal(ps[0].value, "246"); + + // Ambiguity: + setProperty(newdoc[2], {name: "A", value: 246}); + ps = getProperties(newdoc[2]); + assert.equal(ps[1].name, "A"); + assert.equal(ps[2].name, "A"); + assert.equal(ps[1].value, "246"); + assert.equal(ps[2].value, "246"); + + // Better: + setProperty(newdoc[2], {name: "A", value: 247, duplicateIndex: 0}); + setProperty(newdoc[2], {name: "A", value: -247, duplicateIndex: 1}); + ps = getProperties(newdoc[2]); + assert.equal(ps[1].name, "A"); + assert.equal(ps[2].name, "A"); + assert.equal(ps[1].value, "247"); + assert.equal(ps[2].value, "-247"); +}); + +/** + * @author Alexander Schlemmer + * Test creating XML representations. + */ +QUnit.test("createXML", function(assert) { + var done = assert.async(); + let doc = createResponse( + createEntityXML("Record", "bla", undefined, + [{name: "blubb", value: 779}, {name: "zuzuz", value: 42}])); + transformation.transformEntities(doc).then (x => { + ps = getProperties(x[0]); + assert.equal(ps[0].name, "blubb"); + assert.equal(ps[1].value, 42); + done(); + }); +}); + + + +/** + * @author Alexander Schlemmer + * Test obtaining names and IDs. + */ +QUnit.test("namesAndIDs", function(assert) { + assert.equal(getEntityName(this.x[0]), "nameofrecord"); + assert.equal(getEntityID(this.x[0]), ""); + assert.equal(getEntityName(this.x[2]), "nameofrec"); + assert.equal(getEntityID(this.x[2]), "17"); + assert.equal(getEntityName(this.x[3]), ""); + assert.equal(getEntityID(this.x[3]), ""); +}); + + +/** + * @author Alexander Schlemmer + * Test heading attributes and descriptions. + */ +QUnit.test("headingAttributes", function(assert) { + assert.equal(getEntityDescription(this.x[0]), undefined); + assert.equal(getEntityDescription(this.x[1]), undefined); + assert.equal(getEntityDescription(this.x[2]), undefined); + assert.equal(getEntityDescription(this.x[3]), "This record has no name."); +}); + + +/** + * @author Alexander Schlemmer + * Test replication of entities. + */ +QUnit.test("replicationOfEntities", function(assert) { + var done = assert.async(); + + var reptest = function(ent, respxml) { + var oldprops = getProperties(ent); + var oldpars = getParents(ent); + var doc = createResponse( + createEntityXML(getEntityRole(ent), getEntityName(ent), getEntityID(ent), + getProperties(ent), getParents(ent))); + assert.equal(xml2str(doc), respxml); + + + doc = createResponse( + createEntityXML(getEntityRole(ent), getEntityName(ent), getEntityID(ent), + getProperties(ent), getParents(ent), true)); + transformation.transformEntities(doc).then (x => { + ps = getProperties(x[0]); + pars = getParents(x[0]); + + assert.equal(getEntityRole(ent), getEntityRole(x[0])); + assert.equal(getEntityName(ent), getEntityName(x[0])); + assert.equal(getEntityID(ent), getEntityID(x[0])); + assert.equal(ps.length, oldprops.length); + for (var i=0; i<ps.length; i++) { + assert.equal(ps[i].name, oldprops[i].name); + assert.deepEqual(ps[i].value, oldprops[i].value); + assert.equal(ps[i].datatype, oldprops[i].datatype); + assert.equal(ps[i].list, oldprops[i].list); + assert.equal(ps[i].reference, oldprops[i].reference); + } + assert.equal(pars.length, oldpars.length); + for (var i=0; i<pars.length; i++) { + assert.equal(pars[i].name, oldpars[i].name); + assert.equal(pars[i].id, oldpars[i].id); + } + funj += 1; + console.log(funj, maxfunccall); + if (funj == maxfunccall) { + done(); + } + }); + }; + + var respxmls = [ + '<Response><Record name="nameofrecord"><Parent name="bla"/><Property name="A">245</Property></Record></Response>', + '<Response><Record><Parent name="bla"/></Record></Response>', + '<Response><Record id="17" name="nameofrec"><Parent id="244" name="bla"/><Parent id="217" name="bla2"/><Property name="B">245</Property><Property name="A">245.0</Property><Property name="A">245</Property></Record></Response>', + '<Response><Record><Parent name="bla"/><Property name="B">245</Property><Property name="A">245</Property><Property name="A"><Value>245</Value></Property><Property name="A"/><Property name="A"><Value>245</Value><Value>245</Value></Property><Property name="A"><Value>245</Value><Value>247</Value><Value>299</Value></Property></Record></Response>']; + + var funj = 0; + var maxfunccall = this.x.length; + for (var i=0; i<this.x.length; i++) { + reptest(this.x[i], respxmls[i]); + } +}); + + +/** + * @author Alexander Schlemmer + * Test replication of entities. + * This test uses createEntityXML with the append_datatype option disabled. + * This causes the function to create XML without datatype attributes. + * The generated XML should be valid, but the XSLT is currently not able + * to generate valid HTML for list properties. See bug: #3 + */ +QUnit.skip("replicationOfEntitiesNoDatatype", function(assert) { + var done = assert.async(); + + var reptest = function(ent) { + var oldprops = getProperties(ent); + var oldpars = getParents(ent); + var doc = createResponse( + createEntityXML(getEntityRole(ent), getEntityName(ent), getEntityID(ent), + getProperties(ent), getParents(ent))); + transformation.transformEntities(doc).then (x => { + ps = getProperties(x[0]); + pars = getParents(x[0]); + + assert.equal(getEntityRole(ent), getEntityRole(x[0])); + assert.equal(getEntityName(ent), getEntityName(x[0])); + assert.equal(getEntityID(ent), getEntityID(x[0])); + assert.equal(ps.length, oldprops.length); + for (var i=0; i<ps.length; i++) { + assert.equal(ps[i].name, oldprops[i].name); + assert.deepEqual(ps[i].value, oldprops[i].value); + } + assert.equal(pars.length, oldpars.length); + for (var i=0; i<pars.length; i++) { + assert.equal(pars[i].name, oldpars[i].name); + assert.equal(pars[i].id, oldpars[i].id); + } + done(); + }); + + }; + + reptest(this.x[3]); +}); + + diff --git a/test/core/js/modules/entity.xsl.js b/test/core/js/modules/entity.xsl.js new file mode 100644 index 00000000..d042665d --- /dev/null +++ b/test/core/js/modules/entity.xsl.js @@ -0,0 +1,210 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ +/* Testing the construction of the html elements for entities via xsl transformation */ + +/* SETUP */ +QUnit.module("entity.xsl", { + before: function(assert) { + // load entity.xsl + var done = assert.async(); + var qunit_obj = this; + connection.get("webinterface/xsl/entity.xsl").then(function(data) { + insertParam(data, "entitypath", "/entitypath/"); + insertParam(data, "filesystempath", "/filesystempath/"); + qunit_obj.entityXSL = injectTemplate(data, '<xsl:template name="make-filesystem-link"></xsl:template>'); + done(); + }); + } +}); + +/* TESTS */ +QUnit.test("availability", function(assert) { + assert.ok(this.entityXSL); +}); + +QUnit.test("Property names are not links anymore", function(assert) { + var xsl = injectTemplate(this.entityXSL, '<xsl:template match="/"><xsl:apply-templates select="Property" mode="property-collapsed"><xsl:with-param name="collapseid" select="5678"/></xsl:apply-templates></xsl:template>'); + var xml_str = '<Property name="pname" id="2345" datatype="TEXT">pvalue</Property>'; + var xml = str2xml(xml_str); + var html = xslt(xml, xsl); + assert.equal(html.firstElementChild.getElementsByClassName("caosdb-property-name")[0].outerHTML, '<strong class=\"caosdb-property-name\">pname</strong>', "link there"); +}); + +// QUnit.test("parent name is bold link", function(assert) { +// // make this xsl sheet accessible +// let html = applyTemplates(str2xml('<Parent name="TestParent" id="1234" description="DESC"/>'), this.entityXSL, 'entity-body'); +// assert.ok(html); + +// var name_e = html.firstElementChild.getElementsByClassName("caosdb-parent-name")[0]; +// assert.ok(name_e, "element is there"); +// assert.equal(name_e.tagName, "A", "is link"); +// assert.equal(name_e.getAttribute("href"), "/entitypath/1234", "href location"); +// assert.equal(window.getComputedStyle(name_e)["font-weight"], "700", "font is bold"); +// }); + +QUnit.test("TestRecordType data type is recognized as a reference", function(assert) { + // inject an entrance rule + var xsl = getXSLScriptClone(this.entityXSL); + var entry_t = xsl.createElement("xsl:template"); + xsl.firstElementChild.appendChild(entry_t); + entry_t.outerHTML = '<xsl:template match="/"><xsl:apply-templates select="Property" mode="property-value"/></xsl:template>'; + + var xml_str = '<Property name="TestProperty" id="1234" description="DESC" type="TestRecordType">5678</Property>'; + var xml = str2xml(xml_str); + var params = { + entitypath: "/entitypath/" + }; + html = xslt(xml, xsl, params); + assert.ok(html, "html is ok."); + + var link_e = html.firstElementChild; + assert.equal(link_e.tagName, "A", "<a> tag is there."); + assert.equal(link_e.getAttribute("href"), "/entitypath/5678", "href location"); +}); + +QUnit.test("Property with Permissions tag and type='ExperimentSeries' is recognized as a reference", function(assert) { + // inject an entrance rule + var xsl = injectTemplate(this.entityXSL, '<xsl:template match="/"><xsl:apply-templates select="Property" mode="property-value"/></xsl:template>'); + + var xml_str = '<Property id="129954" name="ExperimentSeries" description="A collection of [Experiment] records which have the same [aim]." type="ExperimentSeries" importance="FIX">' + + '\n129950' + + '\n<Permissions>' + + '\n<Permission name="USE:AS_REFERENCE" />' + + '\n<Permission name="UPDATE:QUERY_TEMPLATE_DEFINITION" />' + + '\n</Permissions>' + + '\n</Property>'; + var xml = str2xml(xml_str); + var params = { + entitypath: "/entitypath/" + }; + var html = xslt(xml, xsl, params); + assert.ok(html, "html is ok."); + + var link_e = html.firstElementChild; + assert.equal(link_e.tagName, "A", "<a> tag is there."); + assert.equal(link_e.getAttribute("href"), "/entitypath/129950", "href location"); +}); + +QUnit.test("File path is not converted to lower case.", function(assert) { + let xml_str = '<File path="UPPERCASE.JPG" id="1234"></File>'; + let xml = str2xml(xml_str); + let html = applyTemplates(xml, this.entityXSL, 'entities'); + + // inject an entrance rule + //var xsl_tmp = injectTemplate(this.entityXSL, '<xsl:template match="/"><xsl:apply-templates select="File" mode="top-level-data"/></xsl:template>'); + + + //var params = { + // entitypath: "/entitypath/", + // filesystempath: "/filesystempath/" + //}; + //var html = xslt(xml, xsl, params); + assert.ok(html, "html is ok."); + + var link_e = html.firstElementChild.getElementsByTagName("IMG")[0] + assert.ok(link_e, "<img> tag is there."); + assert.equal(link_e.getAttribute("src"), "/filesystempath/UPPERCASE.JPG", "src location is UPPERCASE.JPG"); +}); + +QUnit.test("back references", function(assert) { + // inject an entrance rule + var xsl = injectTemplate(this.entityXSL, '<xsl:template match="/"><xsl:apply-templates select="*/@id" mode="backreference-link"/></xsl:template>'); + + var xml_str = '<Entity id="265"/>'; + var xml = str2xml(xml_str); + + var link = xslt(xml, xsl).firstChild; + + assert.equal(link.tagName, "A", "is a link"); + assert.equal(link.getAttribute("href"), "/entitypath/?P=0L10&query=FIND+Entity+which+references+265", "query correct."); +}); + +QUnit.test("Entities have a caosdb-annotation-section", function(assert) { + let xml_str = '<Record id="2345"></Record>'; + let xml = str2xml(xml_str) + let html = applyTemplates(xml, this.entityXSL, "entities"); + // inject an entrance rule + //var xsl = injectTemplate(this.entityXSL, '<xsl:template match="/"><xsl:apply-templates select="Record" mode="top-level-data"/></xsl:template>'); + + let secs = html.firstChild.getElementsByClassName("caosdb-annotation-section"); + assert.equal(secs.length, 1, "found one."); + assert.equal(secs[0].tagName, "UL", "section element is DIV"); + + assert.equal(secs[0].getAttribute("data-entity-id"), "2345", "data-entity-id is correct."); +}); + +QUnit.test("LIST Property", function(assert) { + var done = assert.async(); + var entityXSL = this.entityXSL; + assert.expect(2); + $.ajax({ + cache: true, + dataType: 'xml', + url: "xml/test_case_list_of_myrecordtype.xml", + }).done(function(data, textStatus, jdXHR) { + var xsl = injectTemplate(entityXSL, '<xsl:template match="/"><xsl:apply-templates select="Property" mode="property-value"/></xsl:template>'); + var params = { + entitypath: "/entitypath/" + }; + var ret = xslt(data, xsl, params); + assert.ok(ret); + assert.equal(ret.firstChild.className, "caosdb-value-list", "property value contains a list.") + }).always(function() { + done(); + }); +}); + +QUnit.test("single-value template with reference property.", function(assert) { + assert.equal(xml2str(callTemplate(this.entityXSL, 'single-value', { + 'value': '', + 'reference': 'true', + 'boolean': 'false' + })), "", "empty value produces nothing."); + let link = callTemplate(this.entityXSL, 'single-value', { + 'value': '1234', + 'reference': 'true', + 'boolean': 'false' + }).firstElementChild; + assert.equal(link.tagName, 'A', "is link"); + assert.equal(link.getAttribute('href'), "/entitypath/1234", 'link to 1234'); + assert.equal($(link).find('.caosdb-id').length, 1, 'has caosdb-id span'); +}) + +/* MISC FUNCTIONS */ +function applyTemplates(xml, xsl, mode, select = "*") { + let entryRule = '<xsl:template priority="9" match="/"><xsl:apply-templates select="' + select + '" mode="' + mode + '"/></xsl:template>'; + let modXsl = injectTemplate(xsl, entryRule); + return xslt(xml, modXsl); +} + +function callTemplate(xsl, template, params) { + let entryRuleStart = '<xsl:template priority="9" match="/"><xsl:call-template name="' + template + '">'; + let entryRuleEnd = '</xsl:call-template></xsl:template>'; + var entryRule = entryRuleStart; + for (name in params) { + entryRule += '<xsl:with-param name="' + name + '"><xsl:value-of select="\'' + params[name] + '\'"/></xsl:with-param>'; + } + entryRule += entryRuleEnd; + let modXsl = injectTemplate(xsl, entryRule); + return xslt(str2xml('<root/>'), modXsl); +} diff --git a/test/core/js/modules/ext_references.js.js b/test/core/js/modules/ext_references.js.js new file mode 100644 index 00000000..04ea3560 --- /dev/null +++ b/test/core/js/modules/ext_references.js.js @@ -0,0 +1,49 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ +/* testing webcaosdb's javascript sources */ + +/* SETUP ext_references module */ +QUnit.module("ext_references.js", { + before: function(assert) { + } +}); + +QUnit.test("available", function(assert) { + assert.ok(resolve_references); +}); + +QUnit.test("init", function(assert){ + assert.ok(resolve_references.init); +}); + +QUnit.test("get_person_str", function(assert){ + assert.ok(resolve_references.get_person_str); +}); + +QUnit.test("update_single_resolvable_reference", function(assert){ + assert.ok(resolve_references.update_single_resolvable_reference); +}); + +QUnit.test("references", function(assert){ + assert.ok(resolve_references.references); +}); diff --git a/test/core/js/modules/navbar.xsl.js b/test/core/js/modules/navbar.xsl.js new file mode 100644 index 00000000..3649c190 --- /dev/null +++ b/test/core/js/modules/navbar.xsl.js @@ -0,0 +1,81 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ +/* Testing the construction of the query panel via xsl transformation */ + +/* SETUP */ +QUnit.module("navbar.xsl", { + before : function(assert) { + // load query.xsl + var done = assert.async(); + var qunit_obj = this; + $.ajax({ + cache : true, + dataType : 'xml', + url : "xsl/navbar.xsl", + }).done(function(data, textStatus, jdXHR) { + insertParam(data, "entitypath", "/entitypath/"); + insertParam(data, "filesystempath", "/filesystempath/"); + insertParam(data, "basepath", "/basepath/"); + qunit_obj.navbarXsl = injectTemplate(injectTemplate(data, '<xsl:template name="make-filesystem-link"><filesystemlink/></xsl:template>'), '<xsl:template name="caosdb-query-panel"><query/></xsl:template>'); + }).always(function() { + done(); + }); + } +}); + +/* TESTS */ +QUnit.test("availability", function(assert) { + assert.ok(this.navbarXsl); +}); + +QUnit.test("create navbar", function(assert){ + var xml_str = "<Response/>"; + var xml = str2xml(xml_str); + var xsl = injectTemplate(this.navbarXsl, '<xsl:template match="/"><xsl:call-template name="caosdb-top-navbar"/></xsl:template>'); + + var html = xslt(xml, xsl); + assert.ok(html, "html ok"); + assert.equal(html.firstChild.tagName, "NAV", "is nav element"); +}); + +/* MISC FUNCTIONS */ +function getXSLScriptClone(source){ + return str2xml(xml2str(source)) +} + +function injectTemplate(orig_xsl, template){ + var xsl = getXSLScriptClone(orig_xsl); + var entry_t = xsl.createElement("xsl:template"); + xsl.firstElementChild.appendChild(entry_t); + entry_t.outerHTML = template; + return xsl; +} + +function insertParam(xsl, name, value=null){ + var param = xsl.createElement("xsl:param"); + param.setAttribute("name", name); + if (value != null) { + param.setAttribute("select", "'"+value+"'"); + } + xsl.firstElementChild.append(param); +} diff --git a/test/core/js/modules/query.xsl.js b/test/core/js/modules/query.xsl.js new file mode 100644 index 00000000..0ec17bce --- /dev/null +++ b/test/core/js/modules/query.xsl.js @@ -0,0 +1,131 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ +/* Testing the construction of the query panel via xsl transformation */ + +/* SETUP */ +QUnit.module("query.xsl", { + before: function(assert) { + // load query.xsl + var done = assert.async(); + var qunit_obj = this; + $.ajax({ + cache: true, + dataType: 'xml', + url: "xsl/query.xsl", + }).done(function(data, textStatus, jdXHR) { + insertParam(data, "entitypath", "/entitypath/"); + qunit_obj.queryXSL = data; + }).always(function() { + done(); + }); + } +}); + +/* TESTS */ +QUnit.test("availability", function(assert) { + assert.ok(this.queryXSL); +}); + +QUnit.test("basic properties of select-table feature", function(assert) { + assert.equal(this.queryXSL.getElementsByClassName("caosdb-select-table")[0].tagName, "div", "xsl sheet defines a caosdb-select-table div"); + var xsl = injectTemplate(this.queryXSL, '<xsl:template match="/"><xsl:apply-templates select="Selection" mode="select-table"/></xsl:template>'); + var xml = str2xml("<Selection/>"); + var html = xslt(xml, xsl); + assert.equal(html.firstElementChild.tagName, "DIV", "first child is div."); + assert.equal(html.firstElementChild.className, "panel panel-default caosdb-select-table", "first child has class caosdb-select-table."); +}); + +QUnit.test("Query tag is transformed via xslt", function(assert) { + assert.equal(this.queryXSL.getElementsByClassName("caosdb-query-response")[0].tagName, "div", "xsl sheet defines a caosdb-query response div"); + + let html = applyTemplates(str2xml('<Query/>'), this.queryXSL, 'query-results'); + //var html = xslt(xml, xsl); + assert.equal(html.firstElementChild.tagName, "DIV", "first child is div."); + assert.equal(html.firstElementChild.className, "panel panel-default caosdb-query-response", "first child has class caosdb-query-reponse."); +}); + +QUnit.test("xsl defines id 'caosdb-query-form'", function(assert) { + assert.ok(this.queryXSL.getElementById("caosdb-query-form")); +}); + +QUnit.test("xsl defines id 'caosdb-query-form' with tagName=form", function(assert) { + assert.equal(this.queryXSL.getElementById("caosdb-query-form").tagName, "form"); +}); + +QUnit.test("xsl script's 'caosdb-query-form' has a hidden input, with name=P and value=0L10", function(assert) { + var e = this.queryXSL.getElementById("caosdb-query-form").children[1]; + assert.equal(e.tagName, "input", "tagName = input"); + assert.equal(e.getAttribute("name"), "P", "name = P"); + assert.equal(e.getAttribute("value"), "0L10", "value = 0L10"); +}); + + +QUnit.test("Query is available, contained by a div.", function(assert) { + var cont = getQueryFormContainer(this.queryXSL); + assert.equal(cont.tagName, "DIV", "contained by a div"); + assert.equal(cont.className, "container caosdb-query-panel", "container has classname 'container caosdb-query-panel'"); + assert.equal(cont.firstElementChild.tagName, "FORM", "form element is available"); + assert.equal(cont.firstElementChild.className, "panel", "FORM has class 'panel'"); + assert.equal(cont.firstElementChild.id, "caosdb-query-form", "FORM has id 'caosdb-query-form'"); +}); +QUnit.test("Query is send with a paging of 0L10 by default", function(assert) { + var form_e = getQueryForm(this.queryXSL); + + var input_e = form_e.firstElementChild; + assert.equal(input_e.tagName, "INPUT", "input there."); + assert.equal(input_e.getAttribute("type"), "hidden", "input is hidden"); + assert.equal(input_e.getAttribute("name"), "P", "name = P"); + assert.equal(input_e.getAttribute("value"), "0L10", "value = 0L10"); + assert.equal(input_e.id, "caosdb-query-paging-input", "id = caosdb-query-paging-input"); +}); + +QUnit.test("Query form has action attribute", function(assert) { + var form_e = getQueryForm(this.queryXSL); + assert.equal(form_e.getAttribute("action"), "/entitypath/"); +}); + +QUnit.test("template entity-link", function(assert){ + let link = callTemplate(this.queryXSL, "entity-link", {"entity-id": "asdf"}); + assert.equal(link.firstElementChild.tagName, "A", "tagName = A"); + assert.equal(link.firstElementChild.getAttribute("href"), "/entitypath/asdf", "href is /entitypath/asdf"); +}); + +QUnit.test("template select-table-row ", function(assert){ + let row = callTemplate(this.queryXSL, "select-table-row", {"entity-id": "sdfg"}, str2xml('<Response>')); + assert.equal(row.firstElementChild.tagName, "TR", "tagName = TR"); + assert.equal(row.firstElementChild.firstElementChild.tagName, "TD", "tagName = TD"); + assert.equal(row.firstElementChild.firstElementChild.firstElementChild.tagName, "A", "tagName = A"); +}); + +/* MISC FUNCTIONS */ +function getQueryForm(queryXSL) { + var cont = getQueryFormContainer(queryXSL); + return cont.getElementsByTagName("form")[0]; +} + +function getQueryFormContainer(queryXSL) { + var xsl = injectTemplate(queryXSL, '<xsl:template match="/"><xsl:call-template name="caosdb-query-panel"/></xsl:template>"') + var xml = str2xml("<root/>"); + var html = xslt(xml, xsl); + return html.firstElementChild; +} diff --git a/test/core/js/modules/templates_ext.js.js b/test/core/js/modules/templates_ext.js.js new file mode 100644 index 00000000..5f7749e2 --- /dev/null +++ b/test/core/js/modules/templates_ext.js.js @@ -0,0 +1,52 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ + +/* SETUP ext_references module */ +QUnit.module("templates_ext.js", { + before: function(assert) { + } +}); + +QUnit.test("available", function(assert) { + assert.ok(templates_ext); +}); + +QUnit.test("init", function(assert) { + assert.ok(templates_ext.init); +}); + +QUnit.test("generate_template", function(assert) { + assert.ok(templates_ext.generate_template); +}); + +QUnit.test("add_templates", function(assert) { + assert.ok(templates_ext.add_templates); +}); + +QUnit.test("add_user_templates", function(assert) { + assert.ok(templates_ext.add_user_templates); +}); + +QUnit.test("retrieve_templates", function(assert) { + assert.ok(templates_ext.retrieve_templates); +}); diff --git a/test/core/js/modules/webcaosdb.css.js b/test/core/js/modules/webcaosdb.css.js new file mode 100644 index 00000000..45e532da --- /dev/null +++ b/test/core/js/modules/webcaosdb.css.js @@ -0,0 +1,38 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ +/* These unit tests are for the css style sheet 'webcaosdb.css' */ + +/* SETUP */ +QUnit.module("webcaosdb.css", { + before : function(assert) { + this.stylesheet = document.styleSheets[1]; + } +}); + +/* TESTS */ +QUnit.test("availability", function(assert) { + assert.equal(document.styleSheets.length, 2); + var href = this.stylesheet.href; + assert.equal(href.substring(href.length - 14, href.length), + "/webcaosdb.css"); +}); diff --git a/test/core/js/modules/webcaosdb.js.js b/test/core/js/modules/webcaosdb.js.js new file mode 100644 index 00000000..47db4e7d --- /dev/null +++ b/test/core/js/modules/webcaosdb.js.js @@ -0,0 +1,1842 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ +/* testing webcaosdb's javascript sources */ + +QUnit.testStart(function(details) { + connection._init(); + markdown.toHtml = function(textElement){ + return textElement; + }; +}); + +/* SETUP general module */ +QUnit.module("webcaosdb.js", { + before: function(assert) { + } +}); + +/* TESTS */ +QUnit.test("xslt", function(assert) { + let xml_str = '<root/>'; + let xsl_str = '<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"><xsl:output method="html" /><xsl:template match="root"><newroot/></xsl:template></xsl:stylesheet>'; + xml = str2xml(xml_str); + xsl = str2xml(xsl_str); + broken_xsl = str2xml('<blabla/>'); + + html = xslt(xml, xsl); + assert.ok(html); + assert.equal(html.firstChild.localName, "newroot", "<root/> transformed to <newroot/>"); + + // broken xsl throws exception + assert.throws(() => { + xslt(xml, broken_xsl) + }, "broken xsl throws exc."); + + // string throws exception + assert.throws(() => { + xslt(xml_str, xsl) + }, "xml_str throws exc."); + assert.throws(() => { + xslt(xml, xsl_str) + }, "xsl_str throws exc."); + + assert.throws(() => { + xslt(undefined, xsl) + }, "null xml throws exc."); + assert.throws(() => { + xslt(xml, undefined) + }, "nu ll xsl throws exc."); +}); + +QUnit.test("getEntityId", function(assert) { + assert.ok(getEntityId, "function available"); + let okElem = $('<div><div class="caosdb-id">1234</div></div>')[0]; + let notOkElem = $('<div><div class="caosdb-id">asdf</div></div>')[0]; + let emptyElem = $('<div></div>')[0]; + + assert.throws(() => { + getEntityId(); + }, "no parameter throws"); + assert.throws(() => { + getEntityId(null); + }, "null parameter throws"); + assert.throws(() => { + getEntityId(notOkElem); + }, "on-integer ID throws"); + assert.throws(() => { + getEntityId(empty); + }, "empty elem throws"); + + assert.equal("1234", getEntityId(okElem), "ID found"); +}); + +QUnit.test("asyncXslt", function(assert) { + let xml_str = '<root/>'; + let xsl_str = '<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"><xsl:output method="html" /><xsl:template match="root"><newroot/></xsl:template></xsl:stylesheet>'; + xml = str2xml(xml_str); + xsl = str2xml(xsl_str); + xslProm = new Promise((resolve, reject) => { + setTimeout(resolve, 1000, xsl); + }); + broken_xsl = str2xml('<blabla/>'); + + let done = assert.async(2); + asyncXslt(xml, xslProm).then((html) => { + assert.ok(html); + assert.equal(html.firstChild.localName, "newroot", "<root/> transformed to <newroot/>"); + done(); + }); + + // broken xsl throws exception + asyncXslt(xml, broken_xsl).catch((error) => { + assert.equal(/^\[Exception.*\]$/.test(error.toString()), true, "broken xsl thros exc."); + done(); + }); +}); + +QUnit.test("xml2str", function(assert) { + xml = str2xml('<root/>'); + assert.equal(xml2str(xml), '<root/>'); +}); + +QUnit.test("str2xml", function(assert) { + xml = str2xml('<root/>'); + assert.ok(xml); + + // make sure this is a document: + assert.equal(xml.contentType, "text/xml", "has contentType=text/xml"); + assert.ok(xml.documentElement, "has documentElement"); + assert.equal(xml.documentElement.outerHTML, '<root/>', "has outerHTML"); + + // TODO: there is no mechanism to throw an error when the string is not + // valid. +}); + +QUnit.test("postXml", function(assert) { + assert.ok(postXml, "function exists."); +}); + +QUnit.test("createErrorNotification", function(assert) { + assert.ok(createErrorNotification, "function available"); + let err = createErrorNotification("test"); + assert.ok($(err).hasClass(preview.classNameErrorNotification), "has class caosdb-preview-error-notification"); +}); + +/* MODULE connection */ +QUnit.module("webcaosdb.js - connection", { + before: function(assert) { + window.sessionStorage.caosdbBasePath = "../../"; + assert.ok(connection, "connection module is defined"); + } +}); + +QUnit.test("get", function(assert) { + assert.expect(4); + assert.ok(connection.get, "function available"); + let done = assert.async(2); + connection.get("webinterface/xsl/entity.xsl").then(function(resolve) { + assert.equal(resolve.toString(), "[object XMLDocument]", "entity.xsl returned."); + done(); + }); + connection.get("webinterface/non-existent").then((resolve) => resolve, function(error) { + assert.equal(error.toString().split(" - ",1)[0], "Error: GET webinterface/non-existent returned with HTTP status 404", "404 error thrown"); + done(); + }); +}); + + +/* MODULE transformation */ +QUnit.module("webcaosdb.js - transformation", { + before: function(assert) { + assert.ok(transformation, "transformation module is defined"); + } +}); + +QUnit.test("removePermissions", function(assert) { + assert.ok(transformation.removePermissions, "function available"); +}); + +QUnit.test("retrieveXsltScript", function(assert) { + assert.ok(transformation.retrieveXsltScript, "function available"); + let done = assert.async(2); + transformation.retrieveXsltScript("entity.xsl").then(xsl => { + assert.equal(xsl.toString(), "[object XMLDocument]", "entity.xsl returned"); + done(); + }); + transformation.retrieveXsltScript("asdfasdfasdf.xsl").then(xsl => xsl, err => { + assert.equal(err.toString().split(" - ")[0], "Error: GET webinterface/xsl/asdfasdfasdf.xsl returned with HTTP status 404", "not found."); + done(); + }); +}); + +QUnit.test("retrieveEntityXsl", function(assert) { + assert.ok(transformation.retrieveEntityXsl, "function available"); + let done = assert.async(); + transformation.retrieveEntityXsl().then(xsl => { + let xml = str2xml('<Response/>'); + let html = xslt(xml, xsl); + assert.equal(html.firstElementChild.tagName, "DIV"); + done(); + }); +}); + +QUnit.test("transformEntities", function(assert) { + assert.ok(transformation.transformEntities, "function available"); + let done = assert.async(); + let xml = str2xml('<Response><Record id="142"><Warning description="asdf"/></Record></Response>'); + transformation.transformEntities(xml).then(htmls => { + assert.ok($(htmls[0]).hasClass("caosdb-entity-panel"), "entity has been transformed"); + assert.equal($(htmls[0]).find('.caosdb-messages .alert-warning').length, 1, "entity has warning."); + done(); + }, err => { + globalError(err); + }); +}); + +QUnit.test("mergeXsltScripts", function(assert) { + assert.ok(transformation.mergeXsltScripts, 'function available.'); + let xslMainStr = '<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"/>'; + assert.equal(xml2str(transformation.mergeXsltScripts(str2xml(xslMainStr), [])), xslMainStr, 'no includes returns same as xslMain.'); + let xslIncludeStr = '<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"><xsl:template name="bla"/></xsl:stylesheet>' + let xslInclude = str2xml(xslIncludeStr); + assert.ok($(transformation.mergeXsltScripts(str2xml(xslMainStr), [xslInclude])).find("[name='bla']")[0], 'template bla is there.'); +}); + +/* MODULE transaction */ +QUnit.module("webcaosdb.js - transaction", { + before: function(assert) { + assert.ok(transaction, "transaction module is defined"); + }, +}); + +QUnit.test("generateEntitiesUri", function(assert) { + assert.ok(transaction.generateEntitiesUri, "function available"); + + assert.throws(() => { + transaction.generateEntitiesUri() + }, 'no param throws'); + assert.throws(() => { + transaction.generateEntitiesUri(null) + }, 'null param throws'); + assert.equal(transaction.generateEntitiesUri(["asdf", "qwer"]), "Entity/asdf&qwer", "works"); +}); + +QUnit.test("updateEntitiesXml", function(assert) { + assert.ok(transaction.updateEntitiesXml, "function available"); + var done = assert.async(); + connection.put = function(uri, data) { + assert.equal(uri, 'Entity/', "updateEntitiesXml calls connection.put"); + assert.equal(xml2str(data), '<Update/>'); + done(); + }; + transaction.updateEntitiesXml(str2xml('<Update/>')); +}); + +QUnit.test("retrieveEntitiesById", function(assert) { + assert.ok(transaction.retrieveEntitiesById, "function available"); + var done = assert.async(); + connection.get = function(uri) { + assert.equal(uri, 'Entity/1234&2345', "retrieveEntitiesById calls connection.get"); + done(); + }; + transaction.retrieveEntitiesById(["1234", "2345"]); +}); + +QUnit.test("retrieveEntityById", function(assert) { + assert.ok(transaction.retrieveEntityById, "function available"); + var done = assert.async(); + connection.get = function(uri) { + assert.equal(uri, 'Entity/1234', "retrieveEntityById calls connection.get"); + return new Promise((ok, fail) => { + setTimeout(() => ok(str2xml('<Response><Entity id="1234" name="new"/></Response>')), 200); + }); + }; + transaction.retrieveEntityById("1234").then(ret => { + done(); + }); +}); + +/* MODULE transaction.update */ +QUnit.module("webcaosdb.js - transaction.update", { + before: function(assert) { + assert.ok(transaction.update, "transaction.update module is defined"); + } +}); + +QUnit.test("createWaitRetrieveNotification", function(assert) { + assert.ok(transaction.update.createWaitRetrieveNotification(), 'function available and returns non-null'); +}); + +QUnit.test("createWaitUpdateNotification", function(assert) { + assert.ok(transaction.update.createWaitUpdateNotification(), 'function available and returns non-null'); +}); + +QUnit.test("createUpdateForm", function(assert) { + let done = assert.async(); + let cuf = transaction.update.createUpdateForm; + assert.ok(cuf, "function available"); + assert.throws(() => cuf(null, function(xml) {}), "null entityXmlStr throws"); + assert.throws(() => cuf("", null), "null putCallback throws"); + assert.throws(() => cuf("", ""), "non-function putCallback throws"); + assert.throws(() => cuf("", function() {}), "putCallback function without parameters throws"); + + let form = cuf("<root/>", function(xml) { + assert.equal(xml, '<newroot/>', "modified xml is submitted."); + done(); + }); + assert.equal(form.tagName, "FORM", "returns form"); + assert.ok($(form).hasClass(transaction.classNameUpdateForm), "has correct class"); + assert.equal($(form).find('textarea').length, 1, "has one textarea"); + let textarea = $(form).find('textarea')[0]; + assert.equal(textarea.value, "<root/>", "textarea contains xml"); + assert.equal(textarea.name, "updateXml", "textarea has name updateXml"); + assert.equal($(form).find(':submit').length, 1, "has one submit button"); + assert.equal($(form).find(':reset').length, 1, "has one reset button"); + + textarea.value = "<toberesetroot/>" + $(form).trigger('reset'); + assert.equal(textarea.value, "<root/>", "after reset, old xml is there again"); + textarea.value = "<newroot/>"; + $(form).submit(); + + //$(document.body).append(form); +}); + +QUnit.test("createUpdateEntityHeading", function(assert) { + let cueh = transaction.update.createUpdateEntityHeading; + assert.ok(cueh, "function available"); + let eh = $('<div class="panel-heading"><div class="1strow"/><div class="2ndrow"/></div>')[0]; + assert.equal($(eh).children('.1strow').length, 1, "eh has 1st row"); + assert.equal($(eh).children('.2ndrow').length, 1, "eh has 2nd row"); + let uh = cueh(eh); + assert.equal($(uh).children('.2ndrow').length, 0, "uh has no 2nd row"); + assert.equal($(uh).children('.1strow').length, 1, "uh has 1st row"); +}); + +QUnit.test("createUpdateEntityPanel", function(assert) { + let cued = transaction.update.createUpdateEntityPanel; + assert.ok(cued, "function available"); + let div = $(cued($('<div id="headingid">heading</div>'))); + assert.ok(div.hasClass("panel"), "panel has class panel."); + assert.equal(div.children(":first-child")[0].id, "headingid", "heading is first child element"); +}); + +QUnit.test("updateSingleEntity - success", function(assert) { + let done = assert.async(); + let use = transaction.update.updateSingleEntity; + assert.ok(use, "function available"); + let entityPanel = $('<div class="panel panel-default caosdb-entity-panel"><div class="panel-heading caosdb-entity-panel-heading"><div>heading<div class="caosdb-id">1234</div></div><div>other stuff in the heading</div></div>body</div>')[0]; + connection.get = function(uri) { + assert.equal(uri, 'Entity/1234', 'get was called with correct uri'); + return new Promise((ok, fail) => { + setTimeout(() => { + ok(str2xml('<Response><Entity id="1234" name="old"><Permissions/></Entity></Response>')); + }, 200); + }); + }; + + + $(document.body).append(entityPanel); + let app = use(entityPanel); + + assert.equal(app.state, 'waitRetrieveOld', "in waitRetrieveOld state"); + + setTimeout(() => { + connection._init(); + connection.put = function(uri, xml) { + assert.equal(xml2str(xml), '<Update><Entity id="1234" name="new"/></Update>', "put was called with correct xml"); + return new Promise((ok, fail) => { + setTimeout(() => ok(str2xml('<Response><Record id="1234" name="new"></Record></Response>')), 200); + }); + }; + let form = $(document.body).find('form.' + transaction.classNameUpdateForm); + assert.ok(form[0], "form is there."); + assert.equal(form[0].updateXml.value, '<Entity id="1234" name="old"/>', "Permisions have been removed."); + form[0].updateXml.value = '<Entity id="1234" name="new"/>'; + form.submit(); + assert.equal(app.state, 'waitPutEntity', "in waitPutEntity state"); + assert.equal($(document.body).find('form.' + transaction.classNameUpdateForm).length, 0, "form has been removed."); + + }, 400); + + + + app.onEnterFinal = function(e) { + done(); + $(entityPanel).remove(); + }; + +}); + +QUnit.test("updateSingleEntity - with errors in the server's response", function(assert) { + let done = assert.async(); + let use = transaction.update.updateSingleEntity; + let entityPanel = $('<div class="panel panel-default caosdb-entity-panel"><div class="panel-heading caosdb-entity-panel-heading"><div>heading<div class="caosdb-id">1234</div></div><div>other stuff in the heading</div></div>body</div>')[0]; + connection.get = function(uri) { + return new Promise((ok, fail) => { + setTimeout(() => { + ok(str2xml('<Response><Entity id="1234" name="old"/></Response>')); + }, 200); + }); + }; + + $(document.body).append(entityPanel); + let app = use(entityPanel); + + // submit form -> server response contains error tag. + setTimeout(() => { + connection._init(); + connection.put = function(uri, xml) { + return new Promise((ok, fail) => { + setTimeout(() => ok(str2xml('<Response><Record id="1234" name="new"><Error description="This is an error."/></Record></Response>')), 200); + }); + }; + $(document.body).find('form.' + transaction.classNameUpdateForm).submit(); + }, 400); + + app.onLeaveWaitPutEntity = function(e) { + assert.equal(e.transition, "openForm", "app returns to form again due to errors."); + assert.equal($(app.updatePanel).find('.panel-heading .' + preview.classNameErrorNotification).length, 0, "has no error notification before the response is processed."); + + setTimeout(() => { + assert.equal($(app.updatePanel).find('.panel-heading .' + preview.classNameErrorNotification).length, 1, "has an error notification after the response is processed."); + $(app.updatePanel).remove(); + done(); + }, 200); + }; + +}); + +QUnit.test("createErrorInUpdatedEntityNotification", function(assert) { + assert.ok(transaction.update.createErrorInUpdatedEntityNotification, "function available."); +}); + +QUnit.test("addErrorNotification", function(assert) { + assert.ok(transaction.update.addErrorNotification, "function available"); +}); + +/* MODULE preview */ +QUnit.module("webcaosdb.js - preview", { + before: function(assert) { + // load xmlTestCase + var done = assert.async(2); + var qunit_obj = this; + $.ajax({ + cache: true, + dataType: 'xml', + url: "xml/test_case_preview_entities.xml", + }).done(function(data, textStatus, jdXHR) { + qunit_obj.testXml = data; + }).always(function() { + done(); + }); + // load entity.xsl + preview.getEntityXsl("../").then(function(data) { + insertParam(data, "entitypath", "/entitypath/"); + insertParam(data, "filesystempath", "/filesystempath/"); + qunit_obj.entityXSL = injectTemplate(data, '<xsl:template match="/"><root><xsl:apply-templates select="/Response/*" mode="top-level-data"/></root></xsl:template>'); + done(); + }); + + window.sessionStorage.caosdbBasePath = "../../" + assert.ok(preview, "preview module is defined"); + }, + afterEach: function(assert) { + connection._init(); + } +}); + +QUnit.test("halfArray", function(assert){ + assert.ok(preview.halfArray, "function available"); + assert.throws(() => { + preview.halfArray([1]); + }, "length < 2 throws.") + assert.deepEqual(preview.halfArray([1,2]), [[1],[2]]); + assert.deepEqual(preview.halfArray([1,2,3]), [[1],[2,3]]); + assert.deepEqual(preview.halfArray([1,2,3,4]), [[1,2],[3,4]]); +}); + +QUnit.test("xslt file preview", function(assert) { + let done = assert.async(); + let entityXSL = this.entityXSL; + $.ajax({ + cache: true, + dataType: 'xml', + url: "xml/test_case_file_preview.xml", + }).then((data) => { + let html = xslt(data, entityXSL); + assert.ok(html); + done(); + }).catch((err) => { + console.log(err); + assert.ok(false, "error! see console."); + done(); + }); +}); + +QUnit.test("createShowPreviewButton", function(assert) { + assert.ok(preview.createShowPreviewButton, "function available"); + let showPreviewButton = preview.createShowPreviewButton(); + assert.ok(showPreviewButton, "not null"); + assert.equal(showPreviewButton.tagName, "BUTTON", "is button element"); + assert.ok($(showPreviewButton).hasClass("caosdb-show-preview-button"), "has class caosdb-show-preview-button"); +}); + +QUnit.test("createHidePreviewButton", function(assert) { + assert.ok(preview.createHidePreviewButton, "function available"); + let hidePreviewButton = preview.createHidePreviewButton(); + assert.ok(hidePreviewButton, "not null"); + assert.equal(hidePreviewButton.tagName, "BUTTON", "is button element"); + assert.ok($(hidePreviewButton).hasClass("caosdb-hide-preview-button"), "has class 'caosdb-hide-preview-button'"); +}); + +QUnit.test("addHidePreviewButton", function(assert) { + assert.ok(preview.addHidePreviewButton, "function available"); + let okTestElem = $('<div><div class="caosdb-property-value"></div></div>')[0] + let notOkTestElem = $('<div></div>')[0] + + assert.throws(() => { + preview.addHidePreviewButton(); + }, "no argument throws.") + assert.throws(() => { + preview.addHidePreviewButton(null, null); + }, "null arguments throws."); + assert.throws(() => { + preview.addHidePreviewButton(okTestElem, null); + }, "null button_elem parameter throws."); + assert.throws(() => { + preview.addHidePreviewButton(null, $('<div/>')[0]); + }, "null ref_property_elem parameter throws."); + assert.throws(() => { + preview.addHidePreviewButton(notOkTestElem, $('<div/>')[0]); + }, "ref_property_elem w/o caosdb-value-list throws."); + assert.equal(okTestElem.firstChild.childNodes.length, 0, "before: test div has no children"); + assert.equal(okTestElem, preview.addHidePreviewButton(okTestElem, preview.createHidePreviewButton()), "returns the first parameter"); + assert.equal(okTestElem.firstChild.childNodes.length, 1, "after: test div has new child"); +}); + +QUnit.test("addShowPreviewButton", function(assert) { + assert.ok(preview.addShowPreviewButton, "function available"); + let okTestElem = $('<div><div class="caosdb-property-value"></div></div>')[0] + let notOkTestElem = $('<div></div>')[0] + + assert.throws(() => { + preview.addShowPreviewButton(); + }, "no argument throws.") + assert.throws(() => { + preview.addShowPreviewButton(null, null); + }, "null arguments throws."); + assert.throws(() => { + preview.addShowPreviewButton(okTestElem, null); + }, "null button_elem parameter throws."); + assert.throws(() => { + preview.addShowPreviewButton(null, $('<div/>')[0]); + }, "null ref_property_elem parameter throws."); + assert.throws(() => { + preview.addShowPreviewButton(notOkTestElem, $('<div/>')[0]); + }, "ref_property_elem w/o caosdb-value-list throws."); + assert.equal(okTestElem.firstChild.childNodes.length, 0, "before: test div has no children"); + assert.equal(okTestElem, preview.addShowPreviewButton(okTestElem, preview.createShowPreviewButton()), "returns the first parameter"); + assert.equal(okTestElem.firstChild.childNodes.length, 1, "after: test div has new child"); +}); + +QUnit.test("addWaitingNotification", function(assert) { + assert.ok(preview.addWaitingNotification, "function available"); + let testWaiting = $('<div>Waiting!</div>')[0]; + let okTestElem = $('<div><div class="caosdb-preview-notification-area"></div></div>')[0]; + let notOkTestElem = $('<div></div>')[0]; + + assert.throws(() => { + preview.addWaitingNotification(); + }, "no arguments throws"); + assert.throws(() => { + preview.addWaitingNotification(null, null); + }, "null arguments throws"); + assert.throws(() => { + preview.addWaitingNotification(null, testWaiting); + }, "null first argument throws"); + assert.throws(() => { + preview.addWaitingNotification(okTestElem, null); + }, "null second argument throws"); + assert.throws(() => { + preview.addWaitingNotification(notOkTestElem, testWaiting); + }, "ref_property_elem w/o notification area throws"); + + assert.equal(okTestElem.firstChild.childNodes.length, 0, "before: test div has no children"); + assert.equal(okTestElem, preview.addWaitingNotification(okTestElem, testWaiting), "returns the first parameter"); + assert.equal(okTestElem.firstChild.childNodes.length, 1, "after: test div has new child"); +}); + +QUnit.test("addErrorNotification", function(assert) { + assert.ok(preview.addErrorNotification, "function available"); + let testError = $('<div>Error!</div>')[0]; + let okTestElem = $('<div><div class="caosdb-preview-notification-area"></div></div>')[0]; + let notOkTestElem = $('<div></div>')[0]; + + assert.throws(() => { + preview.addErrorNotification(); + }, "no arguments throws"); + assert.throws(() => { + preview.addErrorNotification(null, null); + }, "null arguments throws"); + assert.throws(() => { + preview.addErrorNotification(null, testError); + }, "null first argument throws"); + assert.throws(() => { + preview.addErrorNotification(okTestElem, null); + }, "null second argument throws"); + assert.throws(() => { + preview.addErrorNotification(notOkTestElem, testError); + }, "ref_property_elem w/o notification area throws"); + + assert.equal(okTestElem.firstChild.childNodes.length, 0, "before: test div has no children"); + assert.equal(okTestElem, preview.addErrorNotification(okTestElem, testError), "returns the first parameter"); + assert.equal(okTestElem.firstChild.childNodes.length, 1, "after: test div has new child"); +}); + +QUnit.test("createWaitingNotification", function(assert) { + assert.ok(preview.createWaitingNotification, "function available"); + let welem = preview.createWaitingNotification(); + assert.ok(welem, "not null"); + assert.ok($(welem).hasClass("caosdb-preview-waiting-notification"), "element has class 'caosdb-preview-waiting-notification'"); + }), + + QUnit.test("createNotificationArea", function(assert) { + assert.ok(preview.createNotificationArea, "function available"); + let narea = preview.createNotificationArea(); + assert.ok(narea, "not null"); + assert.equal(narea.tagName, "DIV", "element is div"); + assert.ok($(narea).hasClass("caosdb-preview-notification-area"), "has class caosdb-preview-notification-area"); + }); + +QUnit.test("getHidePreviewButton", function(assert) { + assert.ok(preview.getHidePreviewButton, "function available"); + let okElem = $('<div><button class="caosdb-hide-preview-button">click</button></div>')[0]; + let notOkElem = $('<div></div>')[0]; + + assert.throws(() => { + preview.getHidePreviewButton(); + }, "no parameter throws."); + assert.throws(() => { + preview.getHidePreviewButton(null); + }, "null parameter throws."); + + assert.notOk(preview.getHidePreviewButton(notOkElem), "parameter w/o button returns null"); + assert.equal(preview.getHidePreviewButton(okElem), okElem.firstChild, "button found"); +}); + +QUnit.test("getRefLinksContainer", function(assert) { + assert.ok(preview.getRefLinksContainer, "function available"); + // TODO: references or lists of references should have a special class, not just + // caosdb-value-list. -> entity.xsl + let okElem = $('<div><div class="caosdb-value-list"><a><span class="caosdb-id">1234</span></a></div></div>')[0]; + let okSingle = $('<div><div class="caosdb-property-value"><a class="btn"><span class="caosdb-id">1234</span></a></div><</div>')[0]; + let notOkElem = $('<div></div>')[0]; + + assert.throws(() => { + preview.getRefLinksContainer(); + }, "no parameter throws"); + assert.throws(() => { + preview.getRefLinksContainer(null); + }, "null parameter throws"); + + assert.notOk(preview.getRefLinksContainer(notOkElem), "parameter w/o elem returns null"); + assert.equal(preview.getRefLinksContainer(okElem), okElem.firstChild, "links found"); + assert.equal(preview.getRefLinksContainer(okSingle), okSingle.firstChild.firstChild, "single link found"); +}); + +QUnit.test("getPreviewCarousel", function(assert) { + assert.ok(preview.getPreviewCarousel, "function available"); + let okElem = $('<div><div class="' + preview.classNamePreview + '"></div></div>')[0]; + let notOkElem = $('<div></div>')[0]; + + assert.throws(() => { + preview.getPreviewCarousel(); + }, "no parameter throws"); + assert.throws(() => { + preview.getPreviewCarousel(null); + }, "null parameter throws"); + + assert.notOk(preview.getPreviewCarousel(notOkElem), "parameter w/o carousel returns null"); + assert.equal(preview.getPreviewCarousel(okElem), okElem.firstChild, "carousel found"); +}); + +QUnit.test("getShowPreviewButton", function(assert) { + assert.ok(preview.getShowPreviewButton, "function available"); + let okElem = $('<div><button class="caosdb-show-preview-button">click</button></div>')[0]; + let notOkElem = $('<div></div>')[0]; + + assert.throws(() => { + preview.getShowPreviewButton(); + }, "no parameter throws."); + assert.throws(() => { + preview.getShowPreviewButton(null); + }, "null parameter throws."); + + assert.notOk(preview.getShowPreviewButton(notOkElem), "parameter w/o button returns null"); + assert.equal(preview.getShowPreviewButton(okElem), okElem.firstChild, "button found"); +}); + +QUnit.test("removeAllErrorNotifications", function(assert) { + assert.ok(preview.removeAllErrorNotifications, "function available"); + let okElem = $('<div><div class="caosdb-preview-error-notification">Error1</div>' + + '<div class="caosdb-preview-error-notification">Error2</div>' + + '<div class="caosdb-preview-waiting-notification">Please wait!</div></div>')[0]; + let emptyElem = $('<div></div>')[0]; + + assert.throws(() => { + preview.removeAllErrorNotifications(); + }, "no parameter throws"); + assert.throws(() => { + preview.removeAllErrorNotifications(null); + }, "null parameter throws"); + + assert.equal(okElem.childNodes.length, 3, "before: three children"); + assert.equal(okElem, preview.removeAllErrorNotifications(okElem), "return first parameter"); + assert.equal(okElem.childNodes.length, 1, "after: one child"); + assert.equal(okElem.firstChild.className, "caosdb-preview-waiting-notification", "waiting notification still there"); + + assert.equal(emptyElem, preview.removeAllErrorNotifications(emptyElem), "empty elem works"); +}); + +QUnit.test("removeAllWaitingNotifications", function(assert) { + assert.ok(removeAllWaitingNotifications, "function available"); + let okElem = $('<div><div class="caosdb-preview-waiting-notification">Waiting1</div>' + + '<div class="caosdb-preview-waiting-notification">Waiting2</div>' + + '<div class="caosdb-preview-error-notification">Error!</div></div>')[0]; + let emptyElem = $('<div></div>')[0]; + + assert.throws(() => { + removeAllWaitingNotifications(); + }, "no parameter throws"); + assert.throws(() => { + removeAllWaitingNotifications(null); + }, "null parameter throws"); + + assert.equal(okElem.childNodes.length, 3, "before: three children"); + assert.equal(okElem, removeAllWaitingNotifications(okElem), "return first parameter"); + assert.equal(okElem.childNodes.length, 1, "after: one child"); + assert.equal(okElem.firstChild.className, "caosdb-preview-error-notification", "error notification still there"); + + assert.equal(emptyElem, removeAllWaitingNotifications(emptyElem), "empty elem works"); +}); + +QUnit.test("createSlideItem", function(assert) { + assert.ok(preview.createSlideItem, "function available"); + let p_elem = $('<div>preview</div>')[0]; + + assert.throws(() => { + preview.createSlideItem(); + }, "no parameter throws"); + assert.throws(() => { + preview.createSlideItem(null); + }, "null parameter throws"); + + let item = preview.createSlideItem(p_elem); + assert.ok($(item).hasClass("item"), "has class item"); + assert.equal(p_elem, item.firstChild, "is wrapped"); +}); + +QUnit.test("getActiveSlideItemIndex", function(assert) { + assert.ok(preview.getActiveSlideItemIndex, "function available"); + let okElem0 = $('<div><div class="carousel-inner">' + + '<div class="item active"></div>' // index 0 + + + '<div class="item"></div>' + + '<div class="item"></div>' + + '</div></div>')[0]; + let okElem1 = $('<div><div class="carousel-inner">' + + '<div class="item"></div>' + + '<div class="item active"></div>' // index 1 + + + '<div class="item"></div>' + + '</div></div>')[0]; + let okElem2 = $('<div><div class="carousel-inner">' + + '<div class="item"></div>' + + '<div class="item"></div>' + + '<div class="item active"></div>' // index 2 + + + '</div></div>')[0]; + let noInner = $('<div></div>')[0]; + let noActive = $('<div><div class="carousel-inner"><div class="item"></div></div></div>')[0]; + + assert.throws(() => { + preview.getActiveSlideItemIndex() + }, "no params throws"); + assert.throws(() => { + preview.getActiveSlideItemIndex(null) + }, "null param throws"); + assert.throws(() => { + preview.getActiveSlideItemIndex(noInner) + }, "param w/o .carousel-inner throws"); + assert.throws(() => { + preview.getActiveSlideItemIndex(noActive) + }, "param w/o .active throws"); + + assert.equal(0, preview.getActiveSlideItemIndex(okElem0)); + assert.equal(1, preview.getActiveSlideItemIndex(okElem1)); + assert.equal(2, preview.getActiveSlideItemIndex(okElem2)); +}); + +QUnit.test("getEntityById", function(assert) { + assert.ok(preview.getEntityById, "function available"); + let e1 = $('<div><div class="caosdb-id">1</div></div>')[0]; + let e2 = $('<div><div class="caosdb-id">2</div></div>')[0]; + let e3 = $('<div><div class="caosdb-id">3</div><div><div class="caosdb-id">1</div></div></div>')[0]; + + let es = [e1, e2, e3]; + + assert.throws(() => { + preview.getEntityById() + }, "no param throws."); + assert.throws(() => { + preview.getEntityById(null, 1) + }, "null first param throws."); + assert.throws(() => { + preview.getEntityById("asdf", 1) + }, "string first param throws."); + assert.throws(() => { + preview.getEntityById(es, null) + }, "null second param throws."); + assert.throws(() => { + preview.getEntityById(es, "asdf") + }, "string second param throws."); + + assert.equal(e1, preview.getEntityById(es, 1), "find 1"); + assert.equal(e2, preview.getEntityById(es, 2), "find 2"); + assert.equal(e3, preview.getEntityById(es, 3), "find 3"); + assert.equal(null, preview.getEntityById(es, 4), "find 4 -> null"); +}); + +QUnit.test("createEmptyInner", function(assert) { + assert.ok(preview.createEmptyInner, "function available"); + + assert.throws(() => { + preview.createEmptyInner(); + }, "no param throws"); + assert.throws(() => { + preview.createEmptyInner(null); + }, "null param throws"); + assert.throws(() => { + preview.createEmptyInner("asdf"); + }, "NaN param throws"); + assert.throws(() => { + preview.createEmptyInner(-5); + }, "smaller 1 param throws"); + + let inner = preview.createEmptyInner(3); + assert.equal(inner.children.length, 3, "three items"); + assert.equal(inner.children[0].className, "item active", "first item is active"); + assert.equal(inner.children[1].className, "item", "second item is not active"); + assert.equal(inner.children[2].className, "item", "third item is not active"); +}); + +QUnit.test("createCarouselNav", function(assert) { + assert.ok(preview.createCarouselNav, "function available"); + let refLinks = $('<div style="display: none;" class="caosdb-value-list"><a><span class="caosdb-id">1234</span></a><a><span class="caosdb-id">2345</span></a><a><span class="caosdb-id">3456</span></a><a><span class="caosdb-id">4567</span></a></div>')[0]; + assert.throws(() => { + preview.createCarouselNav(); + }, "no param throws"); + assert.throws(() => { + preview.createCarouselNav(null, "asdf"); + }, "null 1st param throws"); + assert.throws(() => { + preview.createCarouselNav(refLinks, null); + }, "null 2nd param throws"); + + let nav = preview.createCarouselNav(refLinks, "cid"); + assert.equal(nav.className, "caosdb-preview-carousel-nav", "caosdb-carousel-nav"); + assert.ok($(nav).find('[data-slide="prev"][href="#cid"]')[0], "has prev button"); + assert.ok($(nav).find('[data-slide="next"][href="#cid"]')[0], "has next button"); + let selectors = preview.getRefLinksContainer(nav); + assert.equal(selectors.children.length, 4, '4 selctor buttons'); + $(document.body).append(nav); + assert.equal($(selectors).is(':hidden'), false, "selectors not hidden."); + $(nav).remove(); + $(selectors).find('a').each((index, button) => { + assert.equal(button.getAttribute("data-slide-to"), index, "buttons have correct data-slide-to attribute"); + assert.equal(button.getAttribute("data-target"), "#cid", "buttons have correct data-target attribute"); + assert.notOk(button.getAttribute("href"), "button dont have href"); + }); + assert.equal($(selectors).find('a:first').hasClass('active'), true, "first button is active"); + assert.equal($(selectors).find('.active').length, 1, "no other bu tton is active"); +}); + +{ + let refLinks = $('<div class="caosdb-value-list"><a><span class="caosdb-id">1234</span></a><a><span class="caosdb-id">2345</span></a><a><span class="caosdb-id">3456</span></a><a><span class="caosdb-id">4567</span></a></div>')[0]; + let e1 = $('<div><div class="caosdb-id">1234</div></div>')[0]; + let e2 = $('<div><div class="caosdb-id">2345</div></div>')[0]; + let e3 = $('<div><div class="caosdb-id">3456</div><div><div class="caosdb-id">1234</div></div></div>')[0]; + let e4 = $('<div><div class="caosdb-id">4567</div></div>')[0]; + let entities = [e1, e3, e4, e2]; + let carousel = preview.createPreviewCarousel(entities, refLinks); + let correct_order_id = ["1234", "2345", "3456", "4567"]; + let preview3Links = preview.createPreview(entities, refLinks); + let preview1Link = preview.createPreview([e1], refLinks.children[0]); + + QUnit.test("createPreviewCarousel", function(assert) { + assert.ok(preview.createPreviewCarousel, "function available"); + + + assert.throws(() => { + preview.createPreviewCarousel() + }, 'no param throws'); + assert.throws(() => { + preview.createPreviewCarousel(null, refLinks) + }, 'null 1st param throws'); + assert.throws(() => { + preview.createPreviewCarousel(entities, null) + }, 'null 2nd param throws'); + assert.throws(() => { + preview.createPreviewCarousel([], null) + }, 'missing entities in 1st param throws'); + + assert.equal($(carousel).find("." + preview.classNamePreviewCarouselNav).length, 1, "carousel has nav"); + assert.equal($(carousel).find(".carousel-inner").length, 1, "carousel has inner"); + for (let i = 0; i < correct_order_id.length; i++) { + assert.equal(getEntityId($(carousel).find('.item')[i]), correct_order_id[i], "entities ids are in order") + } + + assert.ok(carousel.id, "has id"); + assert.equal($(carousel).attr("data-interval"), "false", "no auto-sliding"); + }); + + QUnit.test("getSelectorButtons", function(assert) { + assert.ok(preview.getSelectorButtons, "function available"); + assert.equal(preview.getSelectorButtons($(carousel).find('.' + preview.classNamePreviewCarouselNav)[0])[0].getAttribute('data-slide-to'), "0", "found selector button"); + }); + + QUnit.test("setActiveSlideItemSelector", function(assert) { + assert.ok(preview.setActiveSlideItemSelector, "function available"); + + assert.throws(() => { + preview.setActiveSlideItemSelector() + }, "no param throws"); + assert.throws(() => { + preview.setActiveSlideItemSelector(null, 1) + }, "null 1st param throws"); + assert.throws(() => { + preview.setActiveSlideItemSelector(carousel, null) + }, "null 2nd param throws"); + assert.throws(() => { + preview.setActiveSlideItemSelector(carousel, 15) + }, "too big 2nd param throws"); + assert.throws(() => { + preview.setActiveSlideItemSelector(carousel, "asdf") + }, "NaN 2nd param throws"); + assert.throws(() => { + preview.setActiveSlideItemSelector(carousel, -5) + }, "negative 2nd param throws"); + + assert.equal(preview.setActiveSlideItemSelector(carousel, 1), carousel, "returns carousel"); + for (let i = 0; i < correct_order_id.length; i++) { + preview.setActiveSlideItemSelector(carousel, i); + assert.equal($($(carousel).find('[data-slide-to]')[i]).hasClass("active"), true, "button " + i + " is active"); + assert.equal($(carousel).find('.item.active').length, 1, "and none else"); + } + }); + + QUnit.test("triggerUpdateActiveSlideItemSelector", function(assert) { + assert.ok(preview.triggerUpdateActiveSlideItemSelector, "function available"); + + preview.setActiveSlideItemSelector(carousel, 1); + assert.equal(preview.getActiveSlideItemIndex(carousel), 0, "before: active item is 0"); + assert.equal($(carousel).find('.' + preview.classNamePreviewCarouselNav).find('.active')[0].getAttribute('data-slide-to'), 1, 'before: active selector is 1.'); + $(carousel).on('slid.bs.carousel', preview.triggerUpdateActiveSlideItemSelector); + $(carousel).trigger('slid.bs.carousel'); + assert.equal(preview.getActiveSlideItemIndex(carousel), 0, "after: active item is 0"); + assert.equal($(carousel).find('.' + preview.classNamePreviewCarouselNav).find('.active')[0].getAttribute('data-slide-to'), 0, 'after: active selector is 0.'); + }); + + QUnit.test("createPreview", function(assert) { + assert.ok($(preview3Links).hasClass(preview.classNamePreview), "3 links class name."); + assert.ok($(preview1Link).hasClass(preview.classNamePreview), "1 links class name."); + assert.ok($(preview1Link).hasClass(preview.classNamePreview), "1 links returns entity element"); + }); + + let xmlResponse = str2xml('<Response><Record id="1234"/><Record id="2345"/><Record id="3456"/><Record id="4567"/></Response>'); + let ref_property_elem = $('<div><div class="caosdb-property-value"></div></div>'); + let original_get = connection.get; + ref_property_elem.find('div').append(refLinks); + QUnit.test("initProperty", function(assert) { + //$(document.body).append(ref_property_elem); + let done = assert.async(); + assert.ok(preview.initProperty, "function available"); + let app = preview.initProperty(ref_property_elem[0]); + assert.equal(app.state, 'showLinks', 'app returned'); + + let showPreviewButton = $(ref_property_elem).find('.' + preview.classNameShowPreviewButton); + assert.equal(showPreviewButton.length, 1, 'one show preview button.'); + + connection.get = function(uri) { + assert.equal(uri, "Entity/1234&2345&3456&4567", "get called with correct uri"); + return new Promise(function(ok, err) { + setTimeout(() => { + err('test error'); + }, 200); + }); + }; + + showPreviewButton.click(); + + setTimeout(() => { + assert.equal(app.state, 'showLinks', 'after reset in showLinks state'); + connection.get = function(uri) { + if (uri === "webinterface/xsl/entity.xsl" || uri === "webinterface/xsl/messages.xsl") { + console.log("returning"); + return original_get(uri); + } + return new Promise(function(ok, err) { + setTimeout(() => { + ok(xmlResponse); + }, 200); + }); + }; + showPreviewButton.click(); + assert.equal(app.state, 'waiting', 'app is now in waiting state'); + + setTimeout(() => { + assert.equal(app.state, 'showPreview', 'app is now in showPreview state'); + + let hidePreviewButton = $(ref_property_elem).find('.' + preview.classNameHidePreviewButton); + assert.equal(hidePreviewButton.length, 1, 'one hidePreviewButton'); + hidePreviewButton.click(); + assert.equal(app.state, 'showLinks', 'again in showLinks state.'); + + connection.get = function(uri) { + assert.ok(null, 'get was called: ' + uri); + } + showPreviewButton.click(); + assert.equal(app.state, 'showPreview', 'again in showPreview state but without calling connection.get again.'); + app.resetApp(); + assert.equal($(ref_property_elem).find('.' + preview.classNamePreview).length, 0, 'no carousel after reset'); + done(); + + }, 600); + + }, 400); + + + }); + + +}; + +QUnit.test("preparePreviewEntity", function(assert){ + assert.ok(preview.preparePreviewEntity, "function available"); + let e = $('<div><div class="label caosdb-id">1234</div></div>')[0]; + let prepared = preview.preparePreviewEntity(e); + assert.equal($(prepared).find('a.caosdb-id')[0].href, connection.getBasePath() + "Entity/1234", "link is correct."); +}); + +QUnit.test("getEntitiyIds", function(assert) { + assert.ok(preview.getEntityIds, 'function available'); +}); + +QUnit.test("retrievePreviewEntities", function(assert) { + let done = assert.async(3); + connection.get = function(url){ + if(url.length>15) { + assert.equal(url, "Entity/1&2&3&4&5", "All five entities are to be retrieved."); + done(); + throw new Error("UriTooLongException") + } else { + assert.equal(url, "Entity/1&2", "Only the first two entities are to be retrieved."); + done(); + throw new Error("Terminate this test!"); + } + } + assert.ok(preview.retrievePreviewEntities, "function available"); + preview.retrievePreviewEntities([1,2,3,4,5]).catch(err=>{assert.equal(err.message, "Terminate this test!", "The url had been split up.");done();}); +}); + +QUnit.test("transformXmlToPreviews", function(assert) { + assert.ok(preview.transformXmlToPreviews, "function available"); + assert.ok(this.entityXSL, "xsl there"); + assert.ok(this.testXml, "xml there"); + + let done = assert.async(); + let asyncTestCase = function(resolve) { + done(); + }; + let asyncErr = function(error) { + console.log(error); + done(); + done(); + } + preview.transformXmlToPreviews(this.testXml, this.entityXSL).then(asyncTestCase).catch(asyncErr); +}); + +QUnit.test("init", function(assert) { + assert.ok(preview.init, "function available"); +}); + +QUnit.test("initEntity", function(assert) { + assert.ok(preview.initEntity, "function available"); +}); + +/* MODULE queryForm */ +QUnit.module("webcaosdb.js - queryForm", { + before: function(assert) { + assert.ok(queryForm, "queryForm is defined"); + } +}); + +QUnit.test("removePagingField", function(assert) { + assert.ok(queryForm.removePagingField, "function available."); + assert.throws(() => queryForm.removePagingField(), "null param throws."); + let form = $('<form><input name="P"></form>')[0]; + assert.ok(form.P, "before: paging available."); + queryForm.removePagingField(form); + assert.notOk(form.P, "after: paging removed."); + +}); + +QUnit.test("isSelectQuery", function(assert) { + assert.ok(queryForm.isSelectQuery, "function available."); + assert.throws(() => queryForm.isSelectQuery(), "null param throws."); + assert.equal(queryForm.isSelectQuery("SELECT asdf"), true); + assert.equal(queryForm.isSelectQuery("select asdf"), true); + assert.equal(queryForm.isSelectQuery("SEleCt"), true); + assert.equal(queryForm.isSelectQuery("FIND"), false); + assert.equal(queryForm.isSelectQuery("asd"), false); + assert.equal(queryForm.isSelectQuery("SEL ECT"), false); +}); + +QUnit.test("init", function(assert) { + assert.ok(queryForm.init, "init available"); +}); + +QUnit.test("restoreLastQuery", function(assert) { + assert.ok(queryForm.restoreLastQuery, "available"); + + let form = document.createElement("form"); + form.innerHTML = '<textarea name="query"></textarea><div class="caosdb-search-btn"></div>'; + + assert.throws(() => queryForm.restoreLastQuery(form, null), "null getter throws exc."); + assert.throws(() => queryForm.restoreLastQuery(null, () => "test"), "null form throws exc."); + assert.throws(() => queryForm.restoreLastQuery(null, () => undefined), "null form throws exc."); + + assert.equal(form.query.value, "", "before1: field is empty"); + queryForm.restoreLastQuery(form, () => undefined); + assert.equal(form.query.value, "", "after1: field is still empty"); + + assert.equal(form.query.value, "", "before2: field is empty"); + queryForm.restoreLastQuery(form, () => "this is the old query"); + assert.equal(form.query.value, "this is the old query", "after2: field is not empty"); +}); + +QUnit.test("bindOnClick", function(assert) { + assert.ok(queryForm.bindOnClick, "available"); + + let form = document.createElement("form"); + form.innerHTML = '<textarea name="query"></textarea><div class="caosdb-search-btn"></div>'; + + assert.throws(() => queryForm.bindOnClick(form, null), "null setter throws exc."); + assert.throws(() => queryForm.bindOnClick(form, "asdf"), "non-function setter throws exc."); + assert.throws(() => queryForm.bindOnClick(form, () => undefined), "setter with zero params throws exc."); + assert.throws(() => queryForm.bindOnClick(null, (set) => undefined), "null form throws exc."); + assert.throws(() => queryForm.bindOnClick("asdf", (set) => undefined), "string form throws exc."); + + let storage = function() { + let x = undefined; + return function(set) { + if (set) { + x = set; + } + return x; + } + }(); + queryForm.bindOnClick(form, storage); + assert.equal(storage(), undefined, "before1: storage empty."); + form.getElementsByClassName("caosdb-search-btn")[0].onclick(); + assert.equal(storage(), undefined, "after1: storage still empty."); + + + form.query.value = "free text"; + assert.equal(storage(), undefined, "before2: storage empty."); + form.getElementsByClassName("caosdb-search-btn")[0].onclick(); + assert.equal(storage(), "FIND ENTITY WHICH HAS A PROPERTY LIKE '*free text*'", "after2: storage not empty."); +}) + +/* MODULE paging */ +QUnit.module("webcaosdb.js - paging", { + before: function(assert) {} +}); + +QUnit.test("initPaging", function(assert) { + let initPaging = paging.initPaging; + let getPageHref = paging.getPageHref; + assert.ok(initPaging, "function exists."); + assert.equal(initPaging(), false, "no parameter returs false."); + assert.equal(initPaging(null), false, "null parameter returns false."); + assert.equal(initPaging(null, null), false, "null,null parameter returns false."); + assert.throws(() => { + initPaging(getPageHref(window.location.href, "0L10"), "asdf") + }, "string parameter throws exc."); + assert.equal(initPaging(window.location.href, 1234), true, "no paging."); + assert.equal(initPaging(getPageHref(window.location.href, "0L10"), 1234), true, "1234 returns true."); + assert.equal(initPaging(getPageHref(window.location.href, "0L10"), '1234'), true, "'1234' returns true."); + + // test effectiveness + let $pagingPanel = $('<div>', { + "class": "caosdb-paging-panel" + }); + let $prevButton = $('<a>', { + "class": "caosdb-prev-button" + }); + let $nextButton = $('<a>', { + "class": "caosdb-next-button" + }); + + $pagingPanel.append($prevButton).append($nextButton); + $(document.body).append($pagingPanel); + + $prevButton.hide(); + $nextButton.hide(); + $pagingPanel.hide(); + + + // no paging at all: + let hidden_prev = $('.caosdb-prev-button').css("display") == "none"; + let hidden_next = $('.caosdb-next-button').css("display") == "none"; + let hidden_panel = $('.caosdb-paging-panel').css("display") == "none"; + + initPaging(window.location.href); + hidden_prev = hidden_prev && $('.caosdb-prev-button').css("display") == "none"; + hidden_next = hidden_next && $('.caosdb-next-button').css("display") == "none"; + hidden_panel = hidden_panel && $('.caosdb-paging-panel').css("display") == "none"; + + initPaging(window.location.href, null); + hidden_prev = hidden_prev && $('.caosdb-prev-button').css("display") == "none"; + hidden_next = hidden_next && $('.caosdb-next-button').css("display") == "none"; + hidden_panel = hidden_panel && $('.caosdb-paging-panel').css("display") == "none"; + + initPaging(null, null); + hidden_prev = hidden_prev && $('.caosdb-prev-button').css("display") == "none"; + hidden_next = hidden_next && $('.caosdb-next-button').css("display") == "none"; + hidden_panel = hidden_panel && $('.caosdb-paging-panel').css("display") == "none"; + + initPaging(window.location.href, "100"); + hidden_prev = hidden_prev && $('.caosdb-prev-button').css("display") == "none"; + hidden_next = hidden_next && $('.caosdb-next-button').css("display") == "none"; + hidden_panel = hidden_panel && $('.caosdb-paging-panel').css("display") == "none"; + + initPaging(window.location.href, 100); + hidden_prev = hidden_prev && $('.caosdb-prev-button').css("display") == "none"; + hidden_next = hidden_next && $('.caosdb-next-button').css("display") == "none"; + hidden_panel = hidden_panel && $('.caosdb-paging-panel').css("display") == "none"; + + initPaging(getPageHref(window.location.href, "0L100"), "100"); + hidden_prev = hidden_prev && $('.caosdb-prev-button').css("display") == "none"; + hidden_next = hidden_next && $('.caosdb-next-button').css("display") == "none"; + hidden_panel = hidden_panel && $('.caosdb-paging-panel').css("display") == "none"; + + assert.equal(hidden_prev, true, "prev button has display=none"); + assert.equal(hidden_next, true, "next button has display=none"); + assert.equal(hidden_panel, true, "paging panel has display=none"); + + // show next button + initPaging(getPageHref(window.location.href, "0L10"), 100); + hidden_prev = $('.caosdb-prev-button').css("display") == "none"; + hidden_panel = $('.caosdb-paging-panel').css("display") != "block"; + hidden_next = $('.caosdb-next-button').css("display") != "inline"; + let nextHrefOk = $('.caosdb-next-button').attr('href') == getPageHref(window.location.href, "10L10"); + + initPaging(getPageHref(window.location.href, "0L10"), "100"); + hidden_prev = hidden_prev && $('.caosdb-prev-button').css("display") == "none"; + hidden_panel = hidden_panel || $('.caosdb-paging-panel').css("display") != "block"; + hidden_next = hidden_next || $('.caosdb-next-button').css("display") != "inline"; + nextHrefOk = nextHrefOk && $('.caosdb-next-button').attr('href') == getPageHref(window.location.href, "10L10"); + + initPaging(getPageHref(window.location.href, "0L99"), "100"); + hidden_prev = hidden_prev && $('.caosdb-prev-button').css("display") == "none"; + hidden_panel = hidden_panel || $('.caosdb-paging-panel').css("display") != "block"; + hidden_next = hidden_next || $('.caosdb-next-button').css("display") != "inline"; + nextHrefOk = nextHrefOk && $('.caosdb-next-button').attr('href') == getPageHref(window.location.href, "99L99"); + + assert.equal(hidden_prev, true, "prev button has display=none"); + assert.equal(hidden_next, false, "next button has display=inline"); + assert.equal(hidden_panel, false, "paging panel has display=block"); + assert.equal(nextHrefOk, true, "next buttons href is ok"); + + // show prev button + initPaging(getPageHref(window.location.href, "10L100"), 100); + hidden_prev = $('.caosdb-prev-button').css("display") != "inline"; + hidden_panel = $('.caosdb-paging-panel').css("display") != "block"; + hidden_next = $('.caosdb-next-button').css("display") == "none"; + let prevHrefOk = $('.caosdb-prev-button').attr('href') == getPageHref(window.location.href, "0L100"); + + initPaging(getPageHref(window.location.href, "1L100"), 100); + hidden_prev = hidden_prev || $('.caosdb-prev-button').css("display") != "inline"; + hidden_panel = hidden_panel || $('.caosdb-paging-panel').css("display") != "block"; + hidden_next = hidden_next && $('.caosdb-next-button').css("display") == "none"; + prevHrefOk = prevHrefOk && $('.caosdb-prev-button').attr('href') == getPageHref(window.location.href, "0L100"); + + initPaging(getPageHref(window.location.href, "20L10"), 100); + hidden_prev = hidden_prev || $('.caosdb-prev-button').css("display") != "inline"; + hidden_panel = hidden_panel || $('.caosdb-paging-panel').css("display") != "block"; + prevHrefOk = prevHrefOk && $('.caosdb-prev-button').attr('href') == getPageHref(window.location.href, "10L10"); + + assert.equal(hidden_prev, false, "prev button has display=inline"); + assert.equal(hidden_next, true, "next button has display=none"); + assert.equal(hidden_panel, false, "paging panel has display=block"); + assert.equal(prevHrefOk, true, "prev buttons href is ok"); + + // show both + initPaging(getPageHref(window.location.href, "10L10"), 100); + hidden_prev = $('.caosdb-prev-button').css("display") != "inline"; + hidden_panel = $('.caosdb-paging-panel').css("display") != "block"; + hidden_next = $('.caosdb-next-button').css("display") != "inline"; + nextHrefOk = nextHrefOk && $('.caosdb-next-button').attr('href') == getPageHref(window.location.href, "20L10"); + prevHrefOk = prevHrefOk && $('.caosdb-prev-button').attr('href') == getPageHref(window.location.href, "0L10"); + + initPaging(getPageHref(window.location.href, "1L100"), 200); + hidden_prev = hidden_prev || $('.caosdb-prev-button').css("display") != "inline"; + hidden_panel = hidden_panel || $('.caosdb-paging-panel').css("display") != "block"; + hidden_next = hidden_next || $('.caosdb-next-button').css("display") != "inline"; + nextHrefOk = nextHrefOk && $('.caosdb-next-button').attr('href') == getPageHref(window.location.href, "101L100"); + prevHrefOk = prevHrefOk && $('.caosdb-prev-button').attr('href') == getPageHref(window.location.href, "0L100"); + + assert.equal(hidden_prev, false, "prev button has display=inline"); + assert.equal(hidden_next, false, "next button has display=inline"); + assert.equal(hidden_panel, false, "paging panel has display=block"); + assert.equal(prevHrefOk, true, "prev buttons href is ok"); + assert.equal(nextHrefOk, true, "next buttons href is ok"); + + document.body.removeChild($pagingPanel[0]); +}); + + +QUnit.test("getNextPage", function(assert) { + let getNextPage = paging.getNextPage; + assert.ok(getNextPage, "function exists."); + assert.throws(() => { + getNextPage(null, 100) + }, "null P throws exc."); + assert.throws(() => { + getNextPage("0L10", null) + }, "null n throws exc."); + assert.throws(() => { + getNextPage("asdf", 100) + }, "P with wrong format 1."); + assert.throws(() => { + getNextPage("1234l2345", 100) + }, "P with wrong format 2."); + assert.throws(() => { + getNextPage("1234Lasdf", 100) + }, "P with wrong format 3."); + assert.throws(() => { + getNextPage("0L10", -100) + }, "n is negative."); + assert.throws(() => { + getNextPage("0L10", "asdf") + }, "string n throws exc."); + assert.equal(getNextPage("23L11", 30), null, "n is smaller than index+length -> no next page."); + assert.equal(getNextPage("0L10", 10), null, "n equals index+length -> no next page."); + assert.equal(getNextPage("0L22", 30), "22L22", "0L22 to 22L22.") + assert.equal(getNextPage("5L22", 30), "27L22", "5L22 to 27L22.") + assert.equal(getNextPage("5L10", 30), "15L10", "5L10 to 15L10.") + +}); + +QUnit.test("getPrevPage", function(assert) { + let getPrevPage = paging.getPrevPage; + assert.ok(getPrevPage, "function exists."); + assert.throws(() => { + getPrevPage(null) + }, "null P throws exc."); + assert.throws(() => { + getPrevPage("asdf") + }, "P with wrong format 1."); + assert.throws(() => { + getPrevPage("1234l2345") + }, "P with wrong format 2."); + assert.throws(() => { + getPrevPage("1234Lasdf") + }, "P with wrong format 3."); + assert.equal(getPrevPage("0L10"), null, "Begins with 0 -> No previous page."); + assert.equal(getPrevPage("10L10"), "0L10", "Index 10 to index 0."); + assert.equal(getPrevPage("5L10"), "0L10", "Index 5 to index 0."); + assert.equal(getPrevPage("23L11"), "12L11", "Index 5 to index 0."); +}); + +QUnit.test("getPSegmentFromUri", function(assert) { + let getPSegmentFromUri = paging.getPSegmentFromUri; + assert.ok(getPSegmentFromUri, "function exists."); + assert.throws(() => { + getPSegmentFromUri(null) + }, "null uri throws exc."); + assert.equal(getPSegmentFromUri("https://example:1234/blabla"), null, "asdf has no P segment"); + assert.equal(getPSegmentFromUri("https://example:1234/blabla?asdf"), null, "asdf?asdf has no P segment"); + assert.equal(getPSegmentFromUri("https://example:1234/blabla?P"), null, "asdf?P has no P segment"); + assert.equal(getPSegmentFromUri("https://example:1234/blablaP=0L10"), null, "asdfP=0L10 has no P segment"); + assert.equal(getPSegmentFromUri("https://example:1234/blabla?P=0L10"), "0L10", "asdf?P=0L10 -> P=0L10"); + assert.equal(getPSegmentFromUri("https://example:1234/blabla?asdfP=0L10"), null, "asdf?asdfP=0L10 has no P segment."); + assert.equal(getPSegmentFromUri("https://example:1234/blabla?asdf&P=0L10"), "0L10", "asdf?asdf&P=0L10 -> P=0L10"); + assert.equal(getPSegmentFromUri("https://example:1234/blabla?&P=0L10"), "0L10", "asdf?&P=0L10 -> P=0L10"); + assert.equal(getPSegmentFromUri("https://example:1234/blabla?asdf&P=0L10&"), "0L10", "asdf?asdf&P=0L10& -> P=0L10"); + assert.equal(getPSegmentFromUri("https://example:1234/blabla?asdf&P=0L10&asdfasdf"), "0L10", "asdf?asdf&P=0L10&asdfasdf -> P=0L10"); +}); + +QUnit.test("getPageHref", function(assert) { + let getPageHref = paging.getPageHref; + assert.ok(getPageHref, "function exists."); + assert.throws(() => { + getPageHref(null, "asdf") + }, "null uri_old throws exc."); + assert.equal(getPageHref("1234?P=1234", null), null, "null page returns null."); + assert.equal(getPageHref("https://example:1234/blabla?P=page1&", "page2"), "https://example:1234/blabla?P=page2&", "replace page1 with page2"); + assert.equal(getPageHref("https://example:1234/blabla?P=page1", "page2"), "https://example:1234/blabla?P=page2", "replace page1 with page2"); + assert.equal(getPageHref("https://example:1234/blabla?asdf&P=page1", "page2"), "https://example:1234/blabla?asdf&P=page2", "replace page1 with page2"); + assert.equal(getPageHref("https://example:1234/blabla?asdfP=page1", "page2"), "https://example:1234/blabla?asdfP=page1&P=page2", "append page2"); + assert.equal(getPageHref("https://example:1234/blabla", "page2"), "https://example:1234/blabla?P=page2", "append page2"); +}); + +/* MODULE annotation */ +QUnit.module("webcaosdb.js - annotation", { + before: function(assert) { + // overwrite (we don't actually want to send any post requests) + annotation.postCommentXml = function(xml) { + return new Promise(resolve => setTimeout(resolve, 1000, str2xml("<Response/>"))); + } + window.sessionStorage.caosdbBasePath = "../../"; + } +}); + +QUnit.test("loadAnnotationXsl", function(assert) { + assert.ok(annotation.loadAnnotationXsl, "function exists"); +}); + +QUnit.test("getAnnotationsForEntity", function(assert) { + let xsl_str = '<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"><xsl:output method="html" /><xsl:template match="annotation"><div><xsl:value-of select="@id"/></div></xsl:template><xsl:template match="Response"><root><xsl:apply-templates select="annotation"/></root></xsl:template></xsl:stylesheet>'; + let xslPromise = str2xml(xsl_str); + let xml_str = '<Response><annotation id="1"/><annotation id="2"/><annotation id="3"/></Response>'; + let response = str2xml(xml_str); + let databaseRequest = (id) => { + return response; + }; + + let done = assert.async(); + let asyncTestCase = function(result) { + assert.equal(result.length, 3, "3 divs"); + assert.equal(result[0].tagName, "DIV", "is DIV"); + assert.equal(result[0].childNodes[0].nodeValue, "1", "test is '1'"); + assert.equal(result[1].tagName, "DIV", "is DIV"); + assert.equal(result[1].childNodes[0].nodeValue, "2", "test is '2'"); + assert.equal(result[2].tagName, "DIV", "is DIV"); + assert.equal(result[2].childNodes[0].nodeValue, "3", "test is '3'"); + done(); + } + annotation.getAnnotationsForEntity(1337, databaseRequest, xslPromise).then(asyncTestCase).catch((error) => { + console.log(error); + assert.ok(false, "failure!"); + done(); + }); +}); + +QUnit.test("async/await behavior", (assert) => { + let af = async function() { + return await "returnval"; + }; + + let done = assert.async(3); + af().then((result) => { + assert.equal(result, "returnval", "af() called once."); + done(); + }); + af().then((result) => { + assert.equal(result, "returnval", "af() called twice."); + done(); + }); + + let er = async function() { + throw "asyncerror"; + }; + + er().catch((error) => { + assert.equal(error, "asyncerror", "er() called."); + done(); + }); +}); + +QUnit.test("convertNewCommentForm", function(assert) { + assert.ok(annotation.convertNewCommentForm, "function exists."); + assert.equal(xml2str(annotation.convertNewCommentForm(annotation.createNewCommentForm(2345))), "<Insert><Record><Parent name=\"CommentAnnotation\"/><Property name=\"comment\"/><Property name=\"annotationOf\">2345</Property></Record></Insert>", "conversion ok."); +}); + + +QUnit.test("convertNewCommentResponse", function(assert) { + let convertNewAnnotationResponse = annotation.convertNewCommentResponse; + assert.ok(convertNewAnnotationResponse, "function exists."); + let done = assert.async(); + let testResponse = '<Response><Record><Property name="annotationOf"/><History transaction="INSERT" datetime="2015-12-24T20:15:00" username="someuser"/><Property name="comment">This is a comment</Property></Record></Response>'; + let expectedResult = "<li xmlns=\"http://www.w3.org/1999/xhtml\" class=\"list-group-item\"><div class=\"media\"><div class=\"media-left\"><h3>»</h3></div><div class=\"media-body\"><h4 class=\"media-heading\">someuser<small><i> posted on 2015-12-24T20:15:00</i></small></h4><p class=\"caosdb-comment-annotation-text\">This is a comment</p></div></div></li>"; + convertNewAnnotationResponse(str2xml(testResponse), annotation.loadAnnotationXsl("../../")).then(function(result) { + assert.equal(result.length, 1, "one element returned."); + assert.equal(xml2str(result[0]), expectedResult, "result converted correctly"); + done(); + }, function(error) { + console.log(error); + assert.ok(false, "see console.log"); + done(); + }); +}); + +QUnit.test("getEntityId", function(assert) { + let annotationSection = $('<div data-entity-id="dfgh"/>')[0]; + assert.ok(annotation.getEntityId, "function exists."); + assert.equal(annotation.getEntityId($('<div/>')[0]), null, "no data-entity-id attribute returns null"); + assert.equal(annotation.getEntityId(annotationSection), "dfgh", "returns correct entityId."); + assert.equal(annotation.getEntityId(), null, "no param returns null."); +}); + +QUnit.test("createNewCommentForm", function(assert) { + let createNewCommentForm = annotation.createNewCommentForm; + assert.ok(createNewCommentForm, "function exists."); + assert.equal(createNewCommentForm(1234).tagName, "FORM", "returns form"); + assert.equal(createNewCommentForm(1234).elements["annotationOf"].value, "1234", "annotationOf there"); + assert.equal($(createNewCommentForm(1234)).find("button[type='submit']")[0].name, "submit", "has submit button"); + assert.equal($(createNewCommentForm(1234)).find("button[type='submit']")[0].innerHTML, "Submit", "is labled 'Submit'"); + assert.equal($(createNewCommentForm(1234)).find("button[type='reset']")[0].name, "cancel", "has cancel button"); + assert.equal($(createNewCommentForm(1234)).find("button[type='reset']")[0].innerHTML, "Cancel", "is labled 'Cancel'"); + assert.equal($(createNewCommentForm(1234)).find("button[type='asdf']")[0], null, "no asdf button"); +}); + +QUnit.test("getNewCommentButton", function(assert) { + assert.ok(annotation.getNewCommentButton, "function exists."); + assert.equal(annotation.getNewCommentButton($('<div/>')[0]), null, "not present"); + assert.equal(annotation.getNewCommentButton($('<div><button class="otherclass"/></div>')[0]), null, "not present"); + assert.equal(annotation.getNewCommentButton($('<div><button id="asdf" class="caosdb-new-comment-button"/></div>')[0]).id, "asdf", "button found."); + assert.equal(annotation.getNewCommentButton(), null, "no parameter"); + assert.equal(annotation.getNewCommentButton(null), null, "null parameter"); +}); + +QUnit.test("createPleaseWaitNotification", function(assert) { + assert.ok(annotation.createPleaseWaitNotification, "function exists."); + assert.ok($(annotation.createPleaseWaitNotification()).hasClass("caosdb-please-wait-notification"), "has class caosdb-please-wait-notification"); +}); + +QUnit.test("getNewCommentForm", function(assert) { + let annotationSection = $('<div><form id="sdfg" class="caosdb-new-comment-form"></form></div>')[0]; + assert.ok(annotation.getNewCommentForm, "function exists"); + assert.equal(annotation.getNewCommentForm(annotationSection).id, "sdfg", "NewCommentForm found."); + assert.equal(annotation.getNewCommentForm(), null, "no param returns null"); +}); + +QUnit.test("validateNewCommentForm", function(assert) { + assert.ok(annotation.validateNewCommentForm, "function exists."); + let entityId = "asdf"; + let form = annotation.createNewCommentForm(entityId); + + assert.throws(annotation.validateNewCommentForm, "no param throws exc."); + assert.equal(annotation.validateNewCommentForm(form), false, "empty returns false"); + form.newComment.value = "this is a new comment"; + assert.equal(annotation.validateNewCommentForm(form, 50), false, "to short returns false"); + assert.equal(annotation.validateNewCommentForm(form), true, "long enough returns true"); +}); + +QUnit.test("getPleaseWaitNotification", function(assert) { + assert.ok(annotation.getPleaseWaitNotification, "function exists"); + assert.equal(annotation.getPleaseWaitNotification(), null, "no param returns null"); + assert.equal(annotation.getPleaseWaitNotification($('<div><div class="blablabla" id="asdf"></div></div>')[0]), null, "does not exist"); + assert.equal(annotation.getPleaseWaitNotification($('<div><div class="caosdb-please-wait-notification" id="asdf"></div></div>')[0]).id, "asdf", "found."); +}); + +QUnit.test("NewCommentApp exception", function(assert) { + try { + var original = annotation.createNewCommentForm; + annotation.createNewCommentForm = function() { + throw new TypeError("This is really bad!"); + } + + let annotationSection = $('<div data-entity-id="tzui"><button class="caosdb-new-comment-button"></button></div>')[0]; + let app = annotation.initNewCommentApp(annotationSection); + app.openForm(); + + let done = assert.async(); + setTimeout(() => { + assert.equal(app.state, "read"); + done(); + }, 2000); + } finally { + // clean up + annotation.createNewCommentForm = original + } +}); + +QUnit.test("convertNewCommentResponse error", function(assert) { + let errorStr = '<Response username="tf" realm="PAM" srid="dc1df091045eca7bd6940b88aa6db5b6" timestamp="1499814014684" baseuri="https://baal:8444/mpidsserver" count="1">\ + <Error code="12" description="One or more entities are not qualified. None of them have been inserted/updated/deleted." />\ + <Record>\ + <Error code="114" description="Entity has unqualified properties." />\ + <Warning code="0" description="Entity has no name." />\ + <Parent name="CommentAnnotation">\ + <Error code="101" description="Entity does not exist." />\ + </Parent>\ + <Property name="comment" importance="FIX">\ + sdfasdfasdf\ + <Error code="101" description="Entity does not exist." />\ + <Error code="110" description="Property has no datatype." />\ + </Property>\ + <Property name="annotationOf" importance="FIX">\ + 20\ + <Error code="101" description="Entity does not exist." />\ + <Error code="110" description="Property has no datatype." />\ + </Property>\ + </Record>\ + </Response>'; + + let done = assert.async(); + let expectedResult = "<divxmlns=\"http://www.w3.org/1999/xhtml\"class=\"alertalert-dangercaosdb-new-comment-erroralert-dismissable\"><buttonclass=\"close\"data-dismiss=\"alert\"aria-label=\"close\">×</button><strong>Error!</strong>Thiscommenthasnotbeeninserted.<pclass=\"small\"><pre><code><record><errorcode=\"114\"description=\"Entityhasunqualifiedproperties.\"></error><warningcode=\"0\"description=\"Entityhasnoname.\"></warning><parentname=\"CommentAnnotation\"><errorcode=\"101\"description=\"Entitydoesnotexist.\"></error></parent><propertyname=\"comment\"importance=\"FIX\">sdfasdfasdf<errorcode=\"101\"description=\"Entitydoesnotexist.\"></error><errorcode=\"110\"description=\"Propertyhasnodatatype.\"></error></property><propertyname=\"annotationOf\"importance=\"FIX\">20<errorcode=\"101\"description=\"Entitydoesnotexist.\"></error><errorcode=\"110\"description=\"Propertyhasnodatatype.\"></error></property></record></code></pre></p></div>"; + annotation.convertNewCommentResponse(str2xml(errorStr), annotation.loadAnnotationXsl("../../")).then(function(result) { + assert.equal(xml2str(result[0]).replace(/[\t\n\ ]/g, ""), expectedResult.replace(/[\t\n\ ]/g, ""), "transformed into an error div."); + done(); + }, function(error) { + console.log(error); + assert.ok(false, "see console.log"); + done(); + }); +}) + +QUnit.test("NewCommentApp convertNewCommentResponse", function(assert) { + var done = assert.async(2); + var original = annotation.convertNewCommentResponse; + annotation.convertNewCommentResponse = function(xmlPromise, xslPromise) { + done(1); // was called; + return original(xmlPromise, xslPromise); + } + let originalPost = annotation.postCommentXml; + annotation.postCommentXml = function(xml) { + let testResponse = '<Response><Record><Property name="annotationOf"/><History transaction="INSERT" datetime="2015-12-24T20:15:00" username="someuser"/><Property name="comment">This is a comment</Property></Record></Response>'; + return new Promise(resolve => setTimeout(resolve, 1000, str2xml(testResponse))); + } + + // prepare app + var annotationSection = $('<div data-entity-id="tzui"><button class="caosdb-new-comment-button"></button></div>')[0]; + var app = annotation.initNewCommentApp(annotationSection); + // prepare form + app.openForm(); + var form = annotation.getNewCommentForm(annotationSection); + form.newComment.value = "This is a new comment qwerasdf."; + + app.observe("onEnterRead", () => { + assert.equal(app.state, "read", "finally in read state"); + assert.equal($(annotationSection).find(".caosdb-comment-annotation-text")[0].innerHTML, "This is a comment", "new comment appended."); + + let button = annotation.getNewCommentButton(annotationSection); + assert.ok(button.onclick, "button click is enabled."); + assert.notOk($(button).hasClass('disabled'), "button is not visually disabled."); + assert.equal(annotation.getNewCommentForm(annotationSection), null, "form is not there"); + assert.equal(annotation.getPleaseWaitNotification(annotationSection), null, "please wait is not there"); + + done(); + }); + app.submitForm(form); + + // clean up + annotation.convertNewCommentResponse = original; + annotation.postCommentXml = originalPost; +}); + +QUnit.test("NewCommentApp convertNewCommentForm/postCommentXml", function(assert) { + let done = assert.async(2); + + // do not actually post the comment, just wait 1 second and return + // something. + let originalPost = annotation.postCommentXml; + annotation.postCommentXml = function(xml) { + assert.equal(xml2str(xml), "<Insert><Record><Parent name=\"CommentAnnotation\"/><Property name=\"comment\">This is a new comment qwerasdf.</Property><Property name=\"annotationOf\">tzui</Property></Record></Insert>", "the conversion was sucessful"); + done(2); // postCommentXml was called + return new Promise(resolve => setTimeout(resolve, 1000, str2xml("<Response/>"))); + } + + + // prepare app + let annotationSection = $('<div data-entity-id="tzui"><button class="caosdb-new-comment-button"></button></div>')[0]; + let app = annotation.initNewCommentApp(annotationSection); + app.onBeforeReceiveResponse = null; // because the response is empty. + // prepare form + app.openForm(); + let form = annotation.getNewCommentForm(annotationSection); + form.newComment.value = "This is a new comment qwerasdf."; + + let originalConvert = annotation.convertNewCommentForm; + annotation.convertNewCommentForm = function(sendform) { + assert.ok(sendform == form, "form is still the same"); + done(1); // convertNewCommentForm was called + return originalConvert(sendform); + } + + app.submitForm(form); + assert.equal(app.state, "send", "in send state"); + + + // clean up + annotation.convertNewCommentForm = originalConvert; + annotation.postCommentXml = originalPost; +}); + +QUnit.test("NewCommentApp waitingNotification", function(assert) { + // prepare app + let annotationSection = $('<div><button class="caosdb-new-comment-button"></button></div>')[0]; + let app = annotation.initNewCommentApp(annotationSection); + app.onBeforeReceiveResponse = null; // because the response is empty + // prepare form + app.openForm(); + let form = annotation.getNewCommentForm(annotationSection); + form.newComment.value = "This is a new comment"; + + + $(form).submit(); + assert.equal(app.state, "send", "in send state"); + assert.equal(annotation.getPleaseWaitNotification(annotationSection).className, "caosdb-please-wait-notification", "please wait is there"); +}); + + +QUnit.test("NewCommentApp form.onsubmit", function(assert) { + let done = assert.async(2); + + let annotationSection = $('<div><button class="caosdb-new-comment-button"></button></div>')[0]; + // add to body, otherwise click event would not be working + $(document.body).append(annotationSection); + let app = annotation.initNewCommentApp(annotationSection); + app.onEnterSend = null; // remove form submission + + app.openForm(); + + let form = annotation.getNewCommentForm(annotationSection); + let submitButton = annotation.getSubmitNewCommentButton(annotationSection); + + // test with empty form -> rejected + app.observe("onBeforeTransition", function(e) { + done("1&2"); + }); + + assert.equal(app.state, "write", "before in write state"); + submitButton.click(); + assert.equal(app.state, "write", "after emtpy submit still in write state"); + + form.newComment.value = "This is a new comment"; + submitButton.click(); + assert.equal(app.state, "send", "after non-empty submit in send state"); + + + // clean up + $(annotationSection).remove(); +}); + +QUnit.test("NewCommentApp form.onreset", function(assert) { + let annotationSection = $('<div><button class="caosdb-new-comment-button"></button></div>')[0]; + let app = annotation.initNewCommentApp(annotationSection); + app.openForm(); + + let done = assert.async(); + assert.equal(annotation.getNewCommentForm(annotationSection).className, "caosdb-new-comment-form", "form is there"); + app.observe("onBeforeCancelForm", function(e) { + assert.equal(e.from, "write", "leaving write state"); + done(); + }); + + // add to body since the click event isn't working otherwise (damn!) + $(document.body).append(annotationSection); + + annotation.getCancelNewCommentButton(annotationSection).click(); + assert.equal(annotation.getNewCommentForm(annotationSection), null, "form is not there"); + + app.openForm(); + assert.equal($(annotationSection).find('form').length, 1, "form is not duplicated") + + // clean up + $(annotationSection).remove(); +}); + +QUnit.test("getCancelNewCommentButton", function(assert) { + let annotationSection = $('<div><form class="caosdb-new-comment-form"><button id="fghj" type="reset"/></form></div>')[0]; + assert.ok(annotation.getCancelNewCommentButton, "function exists."); + assert.equal(annotation.getCancelNewCommentButton(), null, "no param returns null"); + assert.equal(annotation.getCancelNewCommentButton(null), null, "null param returns null"); + assert.equal(annotation.getCancelNewCommentButton(annotationSection).id, "fghj", "returns correctly"); + assert.equal(annotation.getCancelNewCommentButton($('<div><form class="caosdb-new-comment-form"><button type="submit"/></form></div>')[0]), null, "button does not exist"); +}); + +QUnit.test("getSubmitNewCommentButton", function(assert) { + let annotationSection = $('<div><form class="caosdb-new-comment-form"><button id="fghj" type="submit"/></form></div>')[0]; + assert.ok(annotation.getSubmitNewCommentButton, "function exists."); + assert.equal(annotation.getSubmitNewCommentButton(), null, "no param returns null"); + assert.equal(annotation.getSubmitNewCommentButton(null), null, "null param returns null"); + assert.equal(annotation.getSubmitNewCommentButton(annotationSection).id, "fghj", "returns correctly"); + assert.equal(annotation.getSubmitNewCommentButton($('<div><form class="caosdb-new-comment-form"><button type="reset"/></form></div>')[0]), null, "button does not exist"); +}); + +QUnit.test("NewCommentApp newCommentButton.onclick", function(assert) { + let annotationSection = $('<div><button class="caosdb-new-comment-button"></button></div>')[0]; + let app = annotation.initNewCommentApp(annotationSection); + let button = annotation.getNewCommentButton(annotationSection); + + assert.equal(app.state, "read", "in read state"); + assert.ok(button.onclick, "button click is enabled."); + assert.notOk($(button).hasClass('disabled'), "button is not visually disabled."); + assert.equal(annotation.getNewCommentForm(annotationSection), null, "form is not there"); + + $(button).click(); + assert.equal(app.state, "write", "after click in write state"); + assert.equal(button.onclick, null, "button click is disabled"); + assert.ok($(button).hasClass("disabled"), "button is visually disabled.") + assert.ok(annotation.getNewCommentForm(annotationSection), "form is there"); + assert.equal(annotation.getNewCommentForm(annotationSection).parentNode.className, "list-group-item", "form is wrapped into list-group-item"); +}); + +QUnit.test("NewCommentApp transitions", function(assert) { + assert.throws(annotation.initNewCommentApp, "null parameter throws exc."); + + let annotationSection = $('<div><button class="caosdb-new-comment-button"></button></div>')[0]; + let app = annotation.initNewCommentApp(annotationSection); + app.onBeforeSubmitForm = null; // remove validation + app.onEnterSend = null; // remove form submission + app.onLeaveWrite = null; // remove form deletion + app.onBeforeReceiveResponse = null; // remove appending the result + + assert.ok(app, "app ok"); + assert.equal(app.state, "read", "initial state is read"); + app.openForm(); + assert.equal(app.state, "write", "open form -> state write"); + app.cancelForm(); + assert.equal(app.state, "read", "cancel -> state read"); + app.openForm(); + assert.equal(app.state, "write", "open form -> state write"); + app.submitForm(); + assert.equal(app.state, "send", "submit -> state send"); + app.receiveResponse(); + assert.equal(app.state, "read", "receiveRequest -> state read"); + app.resetApp("no error"); + assert.equal(app.state, "read", "reset -> state read"); +}); + +QUnit.test("annotation module", function(assert) { + assert.ok(annotation, "module exists."); + assert.ok(annotation.createNewCommentForm, "createNewCommentForm exists."); + assert.ok(annotation.initNewCommentApp, "initNewCommentApp exists."); +}); + +/* MISC FUNCTIONS */ diff --git a/test/core/js/modules/welcome.xsl.js b/test/core/js/modules/welcome.xsl.js new file mode 100644 index 00000000..778ecc71 --- /dev/null +++ b/test/core/js/modules/welcome.xsl.js @@ -0,0 +1,76 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ +/* Testing the construction of the welcome screen */ + +/* SETUP */ +QUnit.module("welcome.xsl", { + beforeEach : function(assert) { + // load welcome.xsl + var done = assert.async(); + var qunit_obj = this; + $.ajax({ + cache : true, + dataType : 'xml', + url : "xsl/welcome.xsl", + }).done(function(data, textStatus, jdXHR) { + qunit_obj.welcomeXSL = data; + }).always(function() { + done(); + }); + } +}); + +/* TESTS */ +QUnit.test("availability", function(assert) { + assert.ok(this.welcomeXSL); +}) + +QUnit.test("squarefield template available, produces div.caosdb-square which has actually width=height.",function(assert){ + // inject an entrance rule + var entry_t = this.welcomeXSL.createElement("xsl:template"); + this.welcomeXSL.firstElementChild.appendChild(entry_t); + entry_t.outerHTML = '<xsl:template match="/"><div style="width: 145px;"><xsl:call-template name="squarefield"><xsl:with-param name="content">content</xsl:with-param></xsl:call-template></div></xsl:template>'; + + // check template works at all + var xsl = this.welcomeXSL; + var xml = str2xml("<root/>"); + var html = xslt(xml,xsl); + assert.ok(html,"document fragment exists"); + + // check output + var match = xml2str(html).match(/^<div.*><div class="caosdb-square"><div class="caosdb-square-content">content<\/div><\/div><\/div>$/); + assert.ok(match,"div created"); + + // check actual size + document.body.appendChild(html); + + var square_e = $(".caosdb-square")[0]; + assert.equal(square_e.offsetWidth,"145","width is 145px"); + assert.equal(square_e.offsetHeight,"145","height is 145px"); + + var elem =document.getElementsByClassName("caosdb-square")[0].parentNode; + elem.parentNode.removeChild(elem); +}); + + +/* MISC FUNCTIONS */ diff --git a/test/core/js/setup.js b/test/core/js/setup.js new file mode 100644 index 00000000..c6431e13 --- /dev/null +++ b/test/core/js/setup.js @@ -0,0 +1,62 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ +"use strict"; + +function getQueryValue(key) { + var match = RegExp('[?&]' + key + '=([^&]*)').exec(window.location.search); + return match && decodeURIComponent(match[1].replace(/\+/g, ' ')); +} + +{ + let loggerPort = getQueryValue("loggerPort"); + console.log("found loggerPort: " + loggerPort); + if (loggerPort) { + console.log("logging QUnit results to http://127.0.0.1:" + loggerPort); + QUnit.testDone( function( details ) { + var result = { + "Module name": details.module, + "Test name": details.name, + "Assertions": { + "Total": details.total, + "Passed": details.passed, + "Failed": details.failed + }, + "Skipped": details.skipped, + "Todo": details.todo, + "Runtime": details.runtime + }; + + $.post("http://127.0.0.1:" + loggerPort + "/log", JSON.stringify( result, null, 2 )); + + } ); + + QUnit.done(function( details ) { + console.log("done"); + let report = (details.failed === 0 ? "SUCCESS\n" : "") + "Total: " + details.total + "\nFailed: " + details.failed + "\nPassed: " + details.passed + "\nRuntime: " + details.runtime + "\n"; + console.log(report); + $.post("http://127.0.0.1:" + loggerPort + "/done", report); + }); + + + } +} diff --git a/test/core/xml/test_case_file_preview.xml b/test/core/xml/test_case_file_preview.xml new file mode 100644 index 00000000..ae76ecf2 --- /dev/null +++ b/test/core/xml/test_case_file_preview.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header +--> +<?xml-stylesheet type="text/xsl" href="https://dumiatis01:8833/playground/webinterface/webcaosdb.xsl" ?> + <Response username="user" realm="PAM" srid="19b9c846d7c4519429be6a9df551b1df" timestamp="1503319134742" baseuri="https://host:port/root" count="1"> + <File id="77176" name="test.txt" checksum="CF83E1357EEFB8BDF1542850D66D8007D620E4050B5715DC83F4A921D36CE9CE47D0D13C5D85F2B0FF8318D2877EEC2F63B931BD47417A81A538327AF927DA3E" path="/path/to/test.txt" size="0"> + <Permissions> + <Permission name="USE:AS_REFERENCE" /> + <Permission name="USE:AS_DATA_TYPE" /> + <Permission name="RETRIEVE:ENTITY" /> + <Permission name="UPDATE:DESCRIPTION" /> + <Permission name="UPDATE:FILE:ADD" /> + <Permission name="UPDATE:FILE:MOVE" /> + <Permission name="RETRIEVE:ACL" /> + <Permission name="DELETE" /> + <Permission name="RETRIEVE:OWNER" /> + <Permission name="UPDATE:PROPERTY:ADD" /> + <Permission name="UPDATE:ROLE" /> + <Permission name="UPDATE:PARENT:ADD" /> + <Permission name="RETRIEVE:FILE" /> + <Permission name="USE:AS_PROPERTY" /> + <Permission name="UPDATE:PARENT:REMOVE" /> + <Permission name="USE:AS_PARENT" /> + <Permission name="UPDATE:PROPERTY:REMOVE" /> + <Permission name="UPDATE:NAME" /> + <Permission name="UPDATE:DATA_TYPE" /> + <Permission name="UPDATE:VALUE" /> + <Permission name="RETRIEVE:HISTORY" /> + <Permission name="EDIT:ACL" /> + <Permission name="UPDATE:FILE:REMOVE" /> + <Permission name="UPDATE:QUERY_TEMPLATE_DEFINITION" /> + </Permissions> + <Parent id="149346" name="A Test" description="Blablabla" /> + </File> + </Response> diff --git a/test/core/xml/test_case_list_of_myrecordtype.xml b/test/core/xml/test_case_list_of_myrecordtype.xml new file mode 100644 index 00000000..b67c857e --- /dev/null +++ b/test/core/xml/test_case_list_of_myrecordtype.xml @@ -0,0 +1,54 @@ +<!-- + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header +--> + +<Property id="149315" name="MyRecordType" datatype="LIST<MyRecordType>" + importance="FIX"> + <Value>167510</Value> + <Value>167546</Value> + <Value>167574</Value> + <Value>167625</Value> + <Value>167515</Value> + <Value>167441</Value> + <Value>167596</Value> + <Value>167249</Value> + <Value>167632</Value> + <Value>167593</Value> + <Value>167321</Value> + <Value>167536</Value> + <Value>167389</Value> + <Value>167612</Value> + <Value>167585</Value> + <Value>167228</Value> + <Value>167211</Value> + <Value>167414</Value> + <Value>167282</Value> + <Value>167409</Value> + <Value>167637</Value> + <Value>167487</Value> + <Value>167328</Value> + <Value>167572</Value> + <Value>167245</Value> + <Value>167615</Value> + <Value>167301</Value> + <Value>167466</Value> +</Property> diff --git a/test/core/xml/test_case_preview_entities.xml b/test/core/xml/test_case_preview_entities.xml new file mode 100644 index 00000000..2a24f51e --- /dev/null +++ b/test/core/xml/test_case_preview_entities.xml @@ -0,0 +1,113 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header +--> +<?xml-stylesheet type="text/xsl" href="https://smaxx15:8433/mpidsserver/webinterface/webcaosdb.xsl" ?> +<Response username="user" realm="PAM" srid="f04608453407431fa208390742db285b" timestamp="1503058894805" baseuri="https://host:port/root" count="3"> +<Record id="142"> +<Permissions> +<Permission name="USE:AS_REFERENCE"/> +<Permission name="USE:AS_DATA_TYPE"/> +<Permission name="RETRIEVE:ENTITY"/> +<Permission name="UPDATE:DESCRIPTION"/> +<Permission name="UPDATE:FILE:ADD"/> +<Permission name="UPDATE:FILE:MOVE"/> +<Permission name="RETRIEVE:ACL"/> +<Permission name="DELETE"/> +<Permission name="RETRIEVE:OWNER"/> +<Permission name="UPDATE:PROPERTY:ADD"/> +<Permission name="UPDATE:ROLE"/> +<Permission name="UPDATE:PARENT:ADD"/> +<Permission name="RETRIEVE:FILE"/> +<Permission name="USE:AS_PROPERTY"/> +<Permission name="UPDATE:PARENT:REMOVE"/> +<Permission name="USE:AS_PARENT"/> +<Permission name="UPDATE:PROPERTY:REMOVE"/> +<Permission name="UPDATE:NAME"/> +<Permission name="UPDATE:DATA_TYPE"/> +<Permission name="UPDATE:VALUE"/> +<Permission name="RETRIEVE:HISTORY"/> +<Permission name="EDIT:ACL"/> +<Permission name="UPDATE:FILE:REMOVE"/> +<Permission name="UPDATE:QUERY_TEMPLATE_DEFINITION"/> +</Permissions> +<Parent id="133" name="RT" description="Blablabla"/> +</Record> +<Record id="143"> +<Permissions> +<Permission name="USE:AS_REFERENCE"/> +<Permission name="USE:AS_DATA_TYPE"/> +<Permission name="RETRIEVE:ENTITY"/> +<Permission name="UPDATE:DESCRIPTION"/> +<Permission name="UPDATE:FILE:ADD"/> +<Permission name="UPDATE:FILE:MOVE"/> +<Permission name="RETRIEVE:ACL"/> +<Permission name="DELETE"/> +<Permission name="RETRIEVE:OWNER"/> +<Permission name="UPDATE:PROPERTY:ADD"/> +<Permission name="UPDATE:ROLE"/> +<Permission name="UPDATE:PARENT:ADD"/> +<Permission name="RETRIEVE:FILE"/> +<Permission name="USE:AS_PROPERTY"/> +<Permission name="UPDATE:PARENT:REMOVE"/> +<Permission name="USE:AS_PARENT"/> +<Permission name="UPDATE:PROPERTY:REMOVE"/> +<Permission name="UPDATE:NAME"/> +<Permission name="UPDATE:DATA_TYPE"/> +<Permission name="UPDATE:VALUE"/> +<Permission name="RETRIEVE:HISTORY"/> +<Permission name="EDIT:ACL"/> +<Permission name="UPDATE:FILE:REMOVE"/> +<Permission name="UPDATE:QUERY_TEMPLATE_DEFINITION"/> +</Permissions> +<Parent id="133" name="RT" description="Blablabla"/> +</Record> +<Record id="144"> +<Permissions> +<Permission name="USE:AS_REFERENCE"/> +<Permission name="USE:AS_DATA_TYPE"/> +<Permission name="RETRIEVE:ENTITY"/> +<Permission name="UPDATE:DESCRIPTION"/> +<Permission name="UPDATE:FILE:ADD"/> +<Permission name="UPDATE:FILE:MOVE"/> +<Permission name="RETRIEVE:ACL"/> +<Permission name="DELETE"/> +<Permission name="RETRIEVE:OWNER"/> +<Permission name="UPDATE:PROPERTY:ADD"/> +<Permission name="UPDATE:ROLE"/> +<Permission name="UPDATE:PARENT:ADD"/> +<Permission name="RETRIEVE:FILE"/> +<Permission name="USE:AS_PROPERTY"/> +<Permission name="UPDATE:PARENT:REMOVE"/> +<Permission name="USE:AS_PARENT"/> +<Permission name="UPDATE:PROPERTY:REMOVE"/> +<Permission name="UPDATE:NAME"/> +<Permission name="UPDATE:DATA_TYPE"/> +<Permission name="UPDATE:VALUE"/> +<Permission name="RETRIEVE:HISTORY"/> +<Permission name="EDIT:ACL"/> +<Permission name="UPDATE:FILE:REMOVE"/> +<Permission name="UPDATE:QUERY_TEMPLATE_DEFINITION"/> +</Permissions> +<Parent id="133" name="RT" description="Blablabla"/> +</Record> +</Response> diff --git a/test/ext/README b/test/ext/README new file mode 100644 index 00000000..9f476656 --- /dev/null +++ b/test/ext/README @@ -0,0 +1 @@ +This directory should not contain any files. It is a placeholder for the extensions which are copied to this directory from other repositories. -- GitLab