diff --git a/conf/ext/README b/conf/ext/README new file mode 100644 index 0000000000000000000000000000000000000000..9f476656218d5af82a6da4311fde7457556cf138 --- /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 029826b8d3249506318158fef93d08842940366b..0000000000000000000000000000000000000000 --- 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 513c7bce925bf48de149c35dd207c4938b18055b..3deb13e6b4f0cadcbf293c979fff58b9c54f3366 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 0000000000000000000000000000000000000000..2ada51a551f3919058694b83bffc2bbd2f4345a4 --- /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 0000000000000000000000000000000000000000..5ecb7fdf28a4cf3e5f93c78863faaef40e34e385 --- /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 0000000000000000000000000000000000000000..2f91a292471039a836c3dedecfcc90ca50de7060 --- /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 0000000000000000000000000000000000000000..fce3ed776304937d61e4d2930042cce60e07dd21 --- /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 0000000000000000000000000000000000000000..dde29b179985b69f05ace261b75da959a9512b81 --- /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 0000000000000000000000000000000000000000..d15e5cf8a30d285c7acb4bfddbffcf05fd9cf1b1 --- /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 0000000000000000000000000000000000000000..fe1819ed1be69fa390fbf87ae34362efacf24c83 --- /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 0000000000000000000000000000000000000000..0fa49dbb3ed230ceb8970dfe38e02315fa65753a --- /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 0000000000000000000000000000000000000000..30b9c95bc62e2d0486a922a1e6073809481738b9 --- /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 0000000000000000000000000000000000000000..9966782204279a99220ab376c7119b046fdc829e --- /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 0000000000000000000000000000000000000000..e9a293f68e058b6c146239318bde78c05a2dac69 --- /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 0000000000000000000000000000000000000000..1a6126b92c1b23ce9f2db29da7451f86ebba90a1 --- /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 0000000000000000000000000000000000000000..2eb2136ba8604de21eef98cd02cb20e2fbe5343b --- /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 0000000000000000000000000000000000000000..bca557c06d909de6f70febcd90d2e03e77112efc --- /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 0000000000000000000000000000000000000000..25668e5df6795cf9ea057ffffe3ab79e458d6472 --- /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 Binary files /dev/null and b/src/core/pics/caosdb_logo_42.png differ 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 Binary files /dev/null and b/src/core/pics/caosdb_logo_medium.png differ 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 Binary files /dev/null and b/src/core/pics/caosdb_logo_small.png differ 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 Binary files /dev/null and b/src/core/pics/caosdb_no_undertitle.png differ 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 Binary files /dev/null and b/src/core/pics/caosdb_no_undertitle_622.png differ diff --git a/src/core/webcaosdb.xsl b/src/core/webcaosdb.xsl new file mode 100644 index 0000000000000000000000000000000000000000..60b4463e9d7838ccf3959b1d1fbd33bb1ec79c85 --- /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 0000000000000000000000000000000000000000..f41f6cbb47680bc9825300de645ae39c67c809cb --- /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 0000000000000000000000000000000000000000..e166cf9123a30e2806ce9ff8edca86091303a99e --- /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 0000000000000000000000000000000000000000..32f563333081cdc5976587cdc28bc2896a5d0478 --- /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 0000000000000000000000000000000000000000..bbe3a7dc7e5c2dff2cd0442a0e5702434a6d6e83 --- /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 0000000000000000000000000000000000000000..35ce1d7140db69fbba3fc5dc3d84a6f547b497a1 --- /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 0000000000000000000000000000000000000000..90467f1852a20001e6b5b390ee2df54a864e85c5 --- /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 0000000000000000000000000000000000000000..eab7d2ce421a9b191d9a026183bf2246c47a36c9 --- /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 0000000000000000000000000000000000000000..0619beccd65bbbb76e826e6539b7c0f4decf8c9f --- /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 0000000000000000000000000000000000000000..12345d584248333a373f7150a19b44a8b542be29 --- /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 0000000000000000000000000000000000000000..d3524fbdf74ef17f8a2c69554371876dab79eae2 --- /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 0000000000000000000000000000000000000000..9375354ac2cf38f9b987e5793d47147022ff1a01 --- /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 0000000000000000000000000000000000000000..9369dfccc28e7dde7b328d563da25bf841afbaae --- /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 0000000000000000000000000000000000000000..9f476656218d5af82a6da4311fde7457556cf138 --- /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 0000000000000000000000000000000000000000..e9833d5682b50c35b827f58e3556f1a305e9d409 --- /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 0000000000000000000000000000000000000000..20815816d5d1de75c4efeb11117f839bacd4ca1d --- /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 0000000000000000000000000000000000000000..9de2e659bba52dd3d1264e1c18c2c23f87cbcc6a --- /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 0000000000000000000000000000000000000000..d042665d3f34a401d8ab4f720891f79526895225 --- /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 0000000000000000000000000000000000000000..04ea35609b9c53064cb92b5b76de07c10b5836dd --- /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 0000000000000000000000000000000000000000..3649c1908939400d0b7a77b4de90cf29dfde638b --- /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 0000000000000000000000000000000000000000..0ec17bce63ab188e6489c9941639437a79b9ed6d --- /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 0000000000000000000000000000000000000000..5f7749e2b54f38037697c216dc631b60280a63cc --- /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 0000000000000000000000000000000000000000..45e532daba0afb6255d6cbbeb96457d43542d4ae --- /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 0000000000000000000000000000000000000000..47db4e7def63b6a379d1615018e341e435bd52a6 --- /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 0000000000000000000000000000000000000000..778ecc711b98a17d21ae5b97153268542c1406be --- /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 0000000000000000000000000000000000000000..c6431e130e98c7a1eeedc09d57373dee86058323 --- /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 0000000000000000000000000000000000000000..ae76ecf2e66025774bcc68ba42164831f42127c0 --- /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 0000000000000000000000000000000000000000..b67c857e22a983774b0ae1ba7e88d9ccd2515de8 --- /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 0000000000000000000000000000000000000000..2a24f51e1e9d665c36d03393653f87c2c495b2d8 --- /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 0000000000000000000000000000000000000000..9f476656218d5af82a6da4311fde7457556cf138 --- /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.