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">&times;</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&amp;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&lt;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('&lt;',@datatype),'&lt;LIST&lt;')">
+        <!-- list -->
+        <xsl:choose>
+          <xsl:when test="translate(normalize-space(text()),'0123456789','')='' and not(contains('+LIST&lt;INTEGER>+LIST&lt;DOUBLE>+LIST&lt;TEXT>+LIST&lt;BOOLEAN>+LIST&lt;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('&lt;',@datatype),'&lt;LIST&lt;')">
+              <!-- 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('&amp;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&#60;INTEGER&#62;"><Value>245</Value></Property>
+    <Property name="A" datatype="LIST&#60;INTEGER&#62;"></Property>
+
+    <Property name="A" datatype="LIST&#60;INTEGER&#62;">
+      <Value>245</Value>
+      <Value>245</Value>
+    </Property>
+
+    <Property name="A" datatype="LIST&#60;Uboot&#62;">
+      <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>&lt;record&gt;&lt;errorcode=\"114\"description=\"Entityhasunqualifiedproperties.\"&gt;&lt;/error&gt;&lt;warningcode=\"0\"description=\"Entityhasnoname.\"&gt;&lt;/warning&gt;&lt;parentname=\"CommentAnnotation\"&gt;&lt;errorcode=\"101\"description=\"Entitydoesnotexist.\"&gt;&lt;/error&gt;&lt;/parent&gt;&lt;propertyname=\"comment\"importance=\"FIX\"&gt;sdfasdfasdf&lt;errorcode=\"101\"description=\"Entitydoesnotexist.\"&gt;&lt;/error&gt;&lt;errorcode=\"110\"description=\"Propertyhasnodatatype.\"&gt;&lt;/error&gt;&lt;/property&gt;&lt;propertyname=\"annotationOf\"importance=\"FIX\"&gt;20&lt;errorcode=\"101\"description=\"Entitydoesnotexist.\"&gt;&lt;/error&gt;&lt;errorcode=\"110\"description=\"Propertyhasnodatatype.\"&gt;&lt;/error&gt;&lt;/property&gt;&lt;/record&gt;</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&lt;MyRecordType&gt;"
+    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.