diff --git a/.gitignore b/.gitignore
index ddfb9ac071b823c0b1f9b2495c1e44c49290ec1b..f69db87ad5a5226535559b6965e771d975ded103 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
+# -*- mode:conf; -*-
+
 # dot files
 .*
 !/.git*
@@ -9,13 +11,18 @@
 # the build dir
 /public
 /sss_bin
+/node_modules/
+/build
+__pycache__
+
+# auto-generated sources
+/src/doc/api
 
 # screen logs
 screenlog.*
 xerr.log
 
 # extensions
-
 conf/ext
 test/ext
 src/ext
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 2a014f451fb7b6de3dd5a8715b3aeae701d806e5..bcbcfcda9df7fa823ecd1454990bb8d6ff28ff79 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -6,6 +6,7 @@
 # Copyright (C) 2019 Henrik tom Wörden
 # Copyright (C) 2020 Timm Fitschen (t.fitschen@indiscale.com)
 # Copyright (C) 2020 IndiScale GmbH (info@indiscale.com)
+# Copyright (C) 2020 Daniel Hornung <d.hornung@indiscale.com>
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU Affero General Public License as
@@ -86,13 +87,33 @@ build-testenv:
   tags: [ cached-dind ]
   image: docker:19.03
   stage: setup
+  timeout: 3 h
   script: 
     - cd test/docker
     - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
       # use here general latest or specific branch latest...
-    - docker pull $CI_REGISTRY_IMAGE:latest || true
     - docker build 
       --pull
-      --cache-from $CI_REGISTRY_IMAGE:latest
       -t $CI_REGISTRY_IMAGE:latest .
     - docker push $CI_REGISTRY_IMAGE:latest
+
+# Build the sphinx documentation and make it ready for deployment by Gitlab Pages
+# documentation:
+#   stage: deploy
+
+# Special job for serving a static website. See https://docs.gitlab.com/ee/ci/yaml/README.html#pages
+pages:
+  tags: [ docker ]
+  stage: deploy
+  only:
+    - dev
+  script:
+      # TODO is this a good location here?
+    - npm install jsdoc
+    - npm install jsdoc-sphinx
+    - echo "Deploying"
+    - make doc
+    - rm -r public || true ; cp -r build/doc/html public
+  artifacts:
+    paths:
+      - public
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 67446d22180ea9ef38664aee86e288ab13becdc8..985d1541eaeb597156b4ddb3de377b55d71e021c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ## [Unreleased]
 
 ### Added (for new features, dependecies etc.)
+
+### Changed (for changes in existing functionality)
+
+### Deprecated (for soon-to-be removed features) 
+
+### Removed (for now removed features)
+
+### Fixed
+
+### Security (in case of vulnerabilities)
+
+## [0.3.0] - 2021-02-10
+
+### Added (for new features, dependecies etc.)
+
+- The versioning model has a new styling and can show and tsv-export the full
+  version history now.
 - Module `ext_bookmarks` which allows users to bookmark entities, store them in
   a link or export them to TSV.
 - table previews in the bottom line module
@@ -17,11 +34,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
   button (edit_mode).
 * Plotly preview has an additional parameter for a config object,
   e.g., for disabling the plotly logo
-- After a SELECT statement now also all referenced files can be downloaded.
+- The map can now show entities that have no geo location but are related to
+  entities that have one. This also effects the search from the map.
+* `getPropertyValues` function which generates a table of property values from
+  a xml representation of entities.
+* After a SELECT statement now also all referenced files can be downloaded.
+* Automated documentation builds: `make doc`
+- documentation on queries
 
 ### Changed (for changes in existing functionality)
-- enabled and enhanced autocompletion
 
+* ext_map version bumped to 0.4
+- enabled and enhanced autocompletion
 * Login form is hidden behind another button.
 
 ### Deprecated (for soon-to-be removed features) 
@@ -30,6 +54,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Fixed
 
+- #144 (Select with ANY VERSION OF).
 - #136 (adding reference properties to entities in edit mode)
 - exclude configuration files when reading files from build.properties.d
 - summaries when opening preview
diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md
new file mode 100644
index 0000000000000000000000000000000000000000..037dc9973705005ea2ee656e8381444c38ba5c60
--- /dev/null
+++ b/DEPENDENCIES.md
@@ -0,0 +1,2 @@
+* CaosDB Server == 0.3
+* Make 4.2.0
diff --git a/makefile b/Makefile
similarity index 98%
rename from makefile
rename to Makefile
index d44625927fbef317ce4afa072a616ada36df96ce..1f182e7864a2252e4c1263cde7fa4238d31a4269 100644
--- a/makefile
+++ b/Makefile
@@ -139,8 +139,10 @@ install-sss:
 	popd ; \
 	./install-sss.sh $(SRC_SSS_DIR) $(SSS_BIN_DIR)
 
-PYTEST ?= pytest-3
+PYTEST ?= pytest
+PIP ?= pip3
 test-sss: install-sss
+	$(PIP) freeze
 	$(PYTEST) -vv $(TEST_SSS_DIR)
 
 
@@ -303,7 +305,13 @@ unzip:
 	for f in $(LIBS_ZIP); do unzip -q -o -d libs $$f; done
 
 
-PYLINT ?= pylint3
+PYLINT ?= pylint
 PYTHON_FILES = $(subst $(ROOT_DIR)/,,$(shell find $(ROOT_DIR)/ -iname "*.py"))
 pylint: $(PYTHON_FILES)
 	for f in $(PYTHON_FILES); do $(PYLINT) -d all -e E,F $$f || exit 1; done
+
+
+# Compile the standalone documentation
+.PHONY: doc
+doc:
+	$(MAKE) -C src/doc html
diff --git a/README_SETUP.md b/README_SETUP.md
index 127549ec3a273b0cca08ee6a824601fbbd3172bf..485835fdae10239cb4329b0f9b6708c1b020fa2a 100644
--- a/README_SETUP.md
+++ b/README_SETUP.md
@@ -21,12 +21,16 @@
  * ** end header
 -->
 
-# Folder Structure
+# Getting Started with the Web Interface 
+Here, we document how to install and build the CaosDB Web Interface. If you are
+only interested in how to use it, please continue [here](tutorials/first_steps.html)
 
-* The `src` folder contains all source code for the webinterface.
+## Folder Structure
+
+* The `src` folder contains all source code for the web interface.
 * The `libs` folder contains all necessary third-party libraries as zip files.
-* The `test` folder contains the unittests for the webinterface.
-* The `ext` folder contains extension for the webinterface. The make file will
+* The `test` folder contains the unittests for the web interface.
+* The `ext` folder contains extension for the web interface. The make file will
   copy all javascript files from `ext/js/` into the public folder and links the
   javascript in the `public/xsl/main.xsl`.
 * The `misc` folder contains a simple http server which is used for running the
@@ -34,7 +38,7 @@
 * The `build.properties.d/` folder contains configuration files for the build.
 
 
-# Build Configuration
+## Build Configuration
 
 The default configuration is defined in
 `build.properties.d/00_default.properties`.
@@ -46,25 +50,37 @@ All files in that directory will be sourced during `make install` and `make test
 Thus any customized configuration can also be added to that folder by just placing
 files in there which override the default values from `00_default.properties`.
 
-See `build.properties.d/00_default.properties` for more
-information.
+See `build.properties.d/00_default.properties` for more information.
 
-# Setup
+## Setup
 
-* Run `make install` to compile/copy the webinterface to a newly created
+* Run `make install` to compile/copy the web interface to a newly created
   `public` folder.
 * Also, `make install` will copy the scripts from `src/server_side_scripting/`
   to `sss_bin/`. If you want to make the server-side scripts callable for the
   server as server-side scripts you need to include the `sss_bin/` directory
   into the server property `SERVER_SIDE_SCRIPTING_BIN_DIRS`.
 
-# Test
+## Test
 
-* Run `make test` to compile/copy the webinterface and the tests to a newly
+* Run `make test` to compile/copy the web interface and the tests to a newly
   created `public` folder.
 * Run `make run-test-server` to start a python http server.
 * The test suite can be started with `firefox http://localhost:8000/`.
 
-# Clean
+## Clean
 
 * Run `make clean` to clean up everything.
+
+## Documentation #
+
+Build documentation in `build/` with `make doc`.
+
+### Requirements ##
+
+- sphinx
+- sphinx-autoapi
+- jsdoc (`npm install jsdoc`)
+- jsdoc-sphinx  (`npm install jsdoc-sphinx`)
+- sphinx-js
+- recommonmark
diff --git a/References_button.png b/References_button.png
new file mode 100644
index 0000000000000000000000000000000000000000..998ef42f7ccb17e32b0c88b8395c249b4529c97f
Binary files /dev/null and b/References_button.png differ
diff --git a/misc/versioning_test_data.py b/misc/versioning_test_data.py
index eaa83e46f61ea2f20263b487e4bb42c37678c94f..5ec7073aeaffc894916ee8a6c4cfdc82bc25a4f1 100755
--- a/misc/versioning_test_data.py
+++ b/misc/versioning_test_data.py
@@ -91,3 +91,8 @@ else:
                              str(rec1.id),
                              str(rec1.id)])
 rec4.insert()
+
+for i in range(4,11):
+    rec1.name = f"TestRecord1-{i}thVersion"
+    rec1.description = f"This is the {i}th version."
+    rec1.update()
diff --git a/model.svg b/model.svg
new file mode 100644
index 0000000000000000000000000000000000000000..2602cb43f15976305d48e6f2d5efeb3821e1d669
--- /dev/null
+++ b/model.svg
@@ -0,0 +1,632 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   contentScriptType="application/ecmascript"
+   contentStyleType="text/css"
+   height="502"
+   preserveAspectRatio="none"
+   version="1.1"
+   viewBox="0 0 407 502"
+   width="407"
+   zoomAndPan="magnify"
+   id="svg233"
+   sodipodi:docname="model.svg"
+   inkscape:version="0.92.4 5da689c313, 2019-01-14">
+  <metadata
+     id="metadata237">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="1920"
+     inkscape:window-height="1043"
+     id="namedview235"
+     showgrid="false"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0"
+     inkscape:zoom="1.1817368"
+     inkscape:cx="112.55875"
+     inkscape:cy="257"
+     inkscape:window-x="1920"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg233" />
+  <defs
+     id="defs11">
+    <filter
+       height="3"
+       id="f64vrt8w3qxjw"
+       width="3"
+       x="-1"
+       y="-1">
+      <feGaussianBlur
+         result="blurOut"
+         stdDeviation="2.0"
+         id="feGaussianBlur2" />
+      <feColorMatrix
+         in="blurOut"
+         result="blurOut2"
+         type="matrix"
+         values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .4 0"
+         id="feColorMatrix4" />
+      <feOffset
+         dx="4.0"
+         dy="4.0"
+         in="blurOut2"
+         result="blurOut3"
+         id="feOffset6" />
+      <feBlend
+         in="SourceGraphic"
+         in2="blurOut3"
+         mode="normal"
+         id="feBlend8" />
+    </filter>
+  </defs>
+  <rect
+     style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.13385832;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+     id="rect4867"
+     width="407"
+     height="502"
+     x="0"
+     y="0" />
+  <polygon
+     id="polygon13"
+     style="fill:#dddddd;stroke:#000000;stroke-width:1.5;filter:url(#f64vrt8w3qxjw)"
+     points="533.5,526 126.5,526 126.5,24 236.5,24 243.5,46.2969 533.5,46.2969 "
+     transform="translate(-126.5,-24)" />
+  <line
+     id="line15"
+     y2="22.296902"
+     y1="22.296902"
+     x2="117"
+     x1="0"
+     style="stroke:#000000;stroke-width:1.5" />
+  <text
+     style="font-weight:bold;font-size:14px;font-family:sans-serif;fill:#000000"
+     id="text17"
+     y="38.995098"
+     x="130.5"
+     textLength="104"
+     lengthAdjust="spacingAndGlyphs"
+     font-weight="bold"
+     font-size="14"
+     transform="translate(-126.5,-24)">RecordTypes</text>
+  <text
+     style="font-size:11px;font-family:sans-serif;fill:#000000"
+     id="text42"
+     y="144.7104"
+     x="461"
+     textLength="0"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="11"
+     transform="translate(-126.5,-24)" />
+  <rect
+     y="411"
+     x="16"
+     width="116"
+     style="fill:#fefece;stroke:#a80036;stroke-width:1.5;filter:url(#f64vrt8w3qxjw)"
+     id="Manufacturer"
+     height="60.804699" />
+  <circle
+     r="11"
+     id="ellipse47"
+     style="fill:#ff1111;stroke:#a80036;stroke-width:1"
+     cy="427"
+     cx="31" />
+  <path
+     inkscape:connector-curvature="0"
+     id="path49"
+     d="m 33.9688,432.6406 q -0.5782,0.2969 -1.2188,0.4375 -0.6406,0.1563 -1.3437,0.1563 -2.5,0 -3.8282,-1.6406 -1.3125,-1.6563 -1.3125,-4.7813 0,-3.125 1.3125,-4.7812 1.3282,-1.6563 3.8282,-1.6563 0.7031,0 1.3437,0.1563 0.6563,0.1562 1.2188,0.4531 v 2.7187 q -0.625,-0.5781 -1.2188,-0.8437 -0.5937,-0.2813 -1.2187,-0.2813 -1.3438,0 -2.0313,1.0782 -0.6875,1.0625 -0.6875,3.1562 0,2.0938 0.6875,3.1719 0.6875,1.0625 2.0313,1.0625 0.625,0 1.2187,-0.2656 0.5938,-0.2813 1.2188,-0.8594 z" />
+  <text
+     style="font-size:12px;font-family:sans-serif;fill:#000000"
+     id="text51"
+     y="455.1543"
+     x="171.5"
+     textLength="84"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="12"
+     transform="translate(-126.5,-24)">Manufacturer</text>
+  <line
+     id="line53"
+     y2="443"
+     y1="443"
+     x2="131"
+     x1="17"
+     style="stroke:#a80036;stroke-width:1.5" />
+  <text
+     style="font-size:11px;font-family:sans-serif;fill:#000000"
+     id="text55"
+     y="481.21039"
+     x="152.5"
+     textLength="0"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="11"
+     transform="translate(-126.5,-24)" />
+  <line
+     id="line57"
+     y2="463.80469"
+     y1="463.80469"
+     x2="131"
+     x1="17"
+     style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" />
+  <rect
+     y="235"
+     x="16"
+     width="174"
+     style="fill:#fefece;stroke:#a80036;stroke-width:1.5;filter:url(#f64vrt8w3qxjw)"
+     id="MusicalInstrument"
+     height="101.6211" />
+  <circle
+     r="11"
+     id="ellipse60"
+     style="fill:#ff1111;stroke:#a80036;stroke-width:1"
+     cy="251"
+     cx="43.600006" />
+  <path
+     inkscape:connector-curvature="0"
+     id="path62"
+     d="m 46.5688,256.6406 q -0.5782,0.2969 -1.2188,0.4375 -0.6406,0.1563 -1.3437,0.1563 -2.5,0 -3.8282,-1.6406 -1.3125,-1.6563 -1.3125,-4.7813 0,-3.125 1.3125,-4.7812 1.3282,-1.6563 3.8282,-1.6563 0.7031,0 1.3437,0.1563 0.6563,0.1562 1.2188,0.4531 v 2.7187 q -0.625,-0.5781 -1.2188,-0.8437 -0.5937,-0.2813 -1.2187,-0.2813 -1.3438,0 -2.0313,1.0782 -0.6875,1.0625 -0.6875,3.1562 0,2.0938 0.6875,3.1719 0.6875,1.0625 2.0313,1.0625 0.625,0 1.2187,-0.2656 0.5938,-0.2813 1.2188,-0.8594 z" />
+  <text
+     style="font-size:12px;font-family:sans-serif;fill:#000000"
+     id="text64"
+     y="279.1543"
+     x="186.89999"
+     textLength="114"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="12"
+     transform="translate(-126.5,-24)">MusicalInstrument</text>
+  <line
+     id="line66"
+     y2="267"
+     y1="267"
+     x2="189"
+     x1="17"
+     style="stroke:#a80036;stroke-width:1.5" />
+  <line
+     id="line68"
+     y2="281.40231"
+     y1="281.40231"
+     x2="73.5"
+     x1="17"
+     style="stroke:#a80036;stroke-width:1.5" />
+  <text
+     style="font-size:11px;font-family:sans-serif;fill:#000000"
+     id="text70"
+     y="308.71039"
+     x="200"
+     textLength="59"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="11"
+     transform="translate(-126.5,-24)">Properties</text>
+  <line
+     id="line72"
+     y2="281.40231"
+     y1="281.40231"
+     x2="189"
+     x1="132.5"
+     style="stroke:#a80036;stroke-width:1.5" />
+  <text
+     style="font-size:11px;font-family:sans-serif;fill:#000000"
+     id="text74"
+     y="341.2222"
+     x="148.5"
+     textLength="86"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="11"
+     transform="translate(-126.5,-24)">price (DOUBLE)</text>
+  <text
+     style="font-size:11px;font-family:sans-serif;fill:#000000"
+     id="text76"
+     y="354.02689"
+     x="148.5"
+     textLength="162"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="11"
+     transform="translate(-126.5,-24)">Manufacturer (Manufacturer)</text>
+  <line
+     id="line78"
+     y2="300.60941"
+     y1="300.60941"
+     x2="62"
+     x1="17"
+     style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" />
+  <text
+     style="font-size:11px;font-family:sans-serif;fill:#000000"
+     id="text80"
+     y="327.91751"
+     x="188.5"
+     textLength="82"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="11"
+     transform="translate(-126.5,-24)">recommended</text>
+  <line
+     id="line82"
+     y2="300.60941"
+     y1="300.60941"
+     x2="189"
+     x1="144"
+     style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" />
+  <rect
+     y="411"
+     x="167.5"
+     width="65"
+     style="fill:#fefece;stroke:#a80036;stroke-width:1.5;filter:url(#f64vrt8w3qxjw)"
+     id="Violin"
+     height="60.804699" />
+  <circle
+     r="11"
+     id="ellipse85"
+     style="fill:#ff1111;stroke:#a80036;stroke-width:1"
+     cy="427"
+     cx="182.5" />
+  <path
+     inkscape:connector-curvature="0"
+     id="path87"
+     d="m 185.4688,432.6406 q -0.5782,0.2969 -1.2188,0.4375 -0.6406,0.1563 -1.3437,0.1563 -2.5,0 -3.8282,-1.6406 -1.3125,-1.6563 -1.3125,-4.7813 0,-3.125 1.3125,-4.7812 1.3282,-1.6563 3.8282,-1.6563 0.7031,0 1.3437,0.1563 0.6563,0.1562 1.2188,0.4531 v 2.7187 q -0.625,-0.5781 -1.2188,-0.8437 -0.5937,-0.2813 -1.2187,-0.2813 -1.3438,0 -2.0313,1.0782 -0.6875,1.0625 -0.6875,3.1562 0,2.0938 0.6875,3.1719 0.6875,1.0625 2.0313,1.0625 0.625,0 1.2187,-0.2656 0.5938,-0.2813 1.2188,-0.8594 z" />
+  <text
+     style="font-size:12px;font-family:sans-serif;fill:#000000"
+     id="text89"
+     y="455.1543"
+     x="323"
+     textLength="33"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="12"
+     transform="translate(-126.5,-24)">Violin</text>
+  <line
+     id="line91"
+     y2="443"
+     y1="443"
+     x2="231.5"
+     x1="168.5"
+     style="stroke:#a80036;stroke-width:1.5" />
+  <text
+     style="font-size:11px;font-family:sans-serif;fill:#000000"
+     id="text93"
+     y="481.21039"
+     x="304"
+     textLength="0"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="11"
+     transform="translate(-126.5,-24)" />
+  <line
+     id="line95"
+     y2="463.80469"
+     y1="463.80469"
+     x2="231.5"
+     x1="168.5"
+     style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" />
+  <rect
+     y="397"
+     x="267.5"
+     width="119"
+     style="fill:#fefece;stroke:#a80036;stroke-width:1.5;filter:url(#f64vrt8w3qxjw)"
+     id="Guitar"
+     height="88.816399" />
+  <circle
+     r="11"
+     id="ellipse98"
+     style="fill:#ff1111;stroke:#a80036;stroke-width:1"
+     cy="413"
+     cx="304.54999" />
+  <path
+     inkscape:connector-curvature="0"
+     id="path100"
+     d="m 307.5188,418.6406 q -0.5782,0.2969 -1.2188,0.4375 -0.6406,0.1563 -1.3437,0.1563 -2.5,0 -3.8282,-1.6406 -1.3125,-1.6563 -1.3125,-4.7813 0,-3.125 1.3125,-4.7812 1.3282,-1.6563 3.8282,-1.6563 0.7031,0 1.3437,0.1563 0.6563,0.1562 1.2188,0.4531 v 2.7187 q -0.625,-0.5781 -1.2188,-0.8437 -0.5937,-0.2813 -1.2187,-0.2813 -1.3438,0 -2.0313,1.0782 -0.6875,1.0625 -0.6875,3.1562 0,2.0938 0.6875,3.1719 0.6875,1.0625 2.0313,1.0625 0.625,0 1.2187,-0.2656 0.5938,-0.2813 1.2188,-0.8594 z" />
+  <text
+     style="font-size:12px;font-family:sans-serif;fill:#000000"
+     id="text102"
+     y="441.1543"
+     x="449.95001"
+     textLength="38"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="12"
+     transform="translate(-126.5,-24)">Guitar</text>
+  <line
+     id="line104"
+     y2="429"
+     y1="429"
+     x2="385.5"
+     x1="268.5"
+     style="stroke:#a80036;stroke-width:1.5" />
+  <line
+     id="line106"
+     y2="443.40231"
+     y1="443.40231"
+     x2="297.5"
+     x1="268.5"
+     style="stroke:#a80036;stroke-width:1.5" />
+  <text
+     style="font-size:11px;font-family:sans-serif;fill:#000000"
+     id="text108"
+     y="470.71039"
+     x="424"
+     textLength="59"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="11"
+     transform="translate(-126.5,-24)">Properties</text>
+  <line
+     id="line110"
+     y2="443.40231"
+     y1="443.40231"
+     x2="385.5"
+     x1="356.5"
+     style="stroke:#a80036;stroke-width:1.5" />
+  <text
+     style="font-size:11px;font-family:sans-serif;fill:#000000"
+     id="text112"
+     y="503.2222"
+     x="400"
+     textLength="107"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="11"
+     transform="translate(-126.5,-24)">electric (BOOLEAN)</text>
+  <line
+     id="line114"
+     y2="462.60941"
+     y1="462.60941"
+     x2="286"
+     x1="268.5"
+     style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" />
+  <text
+     style="font-size:11px;font-family:sans-serif;fill:#000000"
+     id="text116"
+     y="489.91751"
+     x="412.5"
+     textLength="82"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="11"
+     transform="translate(-126.5,-24)">recommended</text>
+  <line
+     id="line118"
+     y2="462.60941"
+     y1="462.60941"
+     x2="385.5"
+     x1="368"
+     style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" />
+  <rect
+     y="255.5"
+     x="225.5"
+     width="165"
+     style="fill:#fefece;stroke:#a80036;stroke-width:1.5;filter:url(#f64vrt8w3qxjw)"
+     id="SoundQualityAnalyzer"
+     height="60.804699" />
+  <circle
+     r="11"
+     id="ellipse121"
+     style="fill:#ff1111;stroke:#a80036;stroke-width:1"
+     cy="271.5"
+     cx="240.5" />
+  <path
+     inkscape:connector-curvature="0"
+     id="path123"
+     d="m 243.4688,277.1406 q -0.5782,0.2969 -1.2188,0.4375 -0.6406,0.1563 -1.3437,0.1563 -2.5,0 -3.8282,-1.6406 -1.3125,-1.6563 -1.3125,-4.7813 0,-3.125 1.3125,-4.7812 1.3282,-1.6563 3.8282,-1.6563 0.7031,0 1.3437,0.1563 0.6563,0.1562 1.2188,0.4531 v 2.7187 q -0.625,-0.5781 -1.2188,-0.8437 -0.5937,-0.2813 -1.2187,-0.2813 -1.3438,0 -2.0313,1.0782 -0.6875,1.0625 -0.6875,3.1562 0,2.0938 0.6875,3.1719 0.6875,1.0625 2.0313,1.0625 0.625,0 1.2187,-0.2656 0.5938,-0.2813 1.2188,-0.8594 z" />
+  <text
+     style="font-size:12px;font-family:sans-serif;fill:#000000"
+     id="text125"
+     y="299.6543"
+     x="381"
+     textLength="133"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="12"
+     transform="translate(-126.5,-24)">SoundQualityAnalyzer</text>
+  <line
+     id="line127"
+     y2="287.5"
+     y1="287.5"
+     x2="389.5"
+     x1="226.5"
+     style="stroke:#a80036;stroke-width:1.5" />
+  <text
+     style="font-size:11px;font-family:sans-serif;fill:#000000"
+     id="text129"
+     y="325.71039"
+     x="362"
+     textLength="0"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="11"
+     transform="translate(-126.5,-24)" />
+  <line
+     id="line131"
+     y2="308.30469"
+     y1="308.30469"
+     x2="389.5"
+     x1="226.5"
+     style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" />
+  <rect
+     y="35"
+     x="20"
+     width="268"
+     style="fill:#fefece;stroke:#a80036;stroke-width:1.5;filter:url(#f64vrt8w3qxjw)"
+     id="Analysis"
+     height="140.0352" />
+  <circle
+     r="11"
+     id="ellipse134"
+     style="fill:#ff1111;stroke:#a80036;stroke-width:1"
+     cy="51"
+     cx="124.75" />
+  <path
+     inkscape:connector-curvature="0"
+     id="path136"
+     d="m 127.7188,56.6406 q -0.5782,0.2969 -1.2188,0.4375 -0.6406,0.1563 -1.3437,0.1563 -2.5,0 -3.8282,-1.6406 -1.3125,-1.6563 -1.3125,-4.7813 0,-3.125 1.3125,-4.7812 1.3282,-1.6563 3.8282,-1.6563 0.7031,0 1.3437,0.1563 0.6563,0.1562 1.2188,0.4531 v 2.7187 q -0.625,-0.5781 -1.2188,-0.8437 -0.5937,-0.2813 -1.2187,-0.2813 -1.3438,0 -2.0313,1.0782 -0.6875,1.0625 -0.6875,3.1562 0,2.0938 0.6875,3.1719 0.6875,1.0625 2.0313,1.0625 0.625,0 1.2187,-0.2656 0.5938,-0.2813 1.2188,-0.8594 z" />
+  <text
+     style="font-size:12px;font-family:sans-serif;fill:#000000"
+     id="text138"
+     y="79.154297"
+     x="271.75"
+     textLength="50"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="12"
+     transform="translate(-126.5,-24)">Analysis</text>
+  <line
+     id="line140"
+     y2="67"
+     y1="67"
+     x2="287"
+     x1="21"
+     style="stroke:#a80036;stroke-width:1.5" />
+  <line
+     id="line142"
+     y2="81.402298"
+     y1="81.402298"
+     x2="124.5"
+     x1="21"
+     style="stroke:#a80036;stroke-width:1.5" />
+  <text
+     style="font-size:11px;font-family:sans-serif;fill:#000000"
+     id="text144"
+     y="108.7104"
+     x="251"
+     textLength="59"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="11"
+     transform="translate(-126.5,-24)">Properties</text>
+  <line
+     id="line146"
+     y2="81.402298"
+     y1="81.402298"
+     x2="287"
+     x1="183.5"
+     style="stroke:#a80036;stroke-width:1.5" />
+  <text
+     style="font-size:11px;font-family:sans-serif;fill:#000000"
+     id="text148"
+     y="141.2222"
+     x="152.5"
+     textLength="134"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="11"
+     transform="translate(-126.5,-24)">quality_factor (DOUBLE)</text>
+  <text
+     style="font-size:11px;font-family:sans-serif;fill:#000000"
+     id="text150"
+     y="154.0269"
+     x="152.5"
+     textLength="92"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="11"
+     transform="translate(-126.5,-24)">date (DATETIME)</text>
+  <text
+     style="font-size:11px;font-family:sans-serif;fill:#000000"
+     id="text152"
+     y="166.8315"
+     x="152.5"
+     textLength="111"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="11"
+     transform="translate(-126.5,-24)">report (REFERENCE)</text>
+  <text
+     style="font-size:11px;font-family:sans-serif;fill:#000000"
+     id="text154"
+     y="179.6362"
+     x="152.5"
+     textLength="256"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="11"
+     transform="translate(-126.5,-24)">SoundQualityAnalyzer (SoundQualityAnalyzer)</text>
+  <text
+     style="font-size:11px;font-family:sans-serif;fill:#000000"
+     id="text156"
+     y="192.4409"
+     x="152.5"
+     textLength="220"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="11"
+     transform="translate(-126.5,-24)">MusicalInstrument (MusicalInstrument)</text>
+  <line
+     id="line158"
+     y2="100.6094"
+     y1="100.6094"
+     x2="113"
+     x1="21"
+     style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" />
+  <text
+     style="font-size:11px;font-family:sans-serif;fill:#000000"
+     id="text160"
+     y="127.9175"
+     x="239.5"
+     textLength="82"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="11"
+     transform="translate(-126.5,-24)">recommended</text>
+  <line
+     id="line162"
+     y2="100.6094"
+     y1="100.6094"
+     x2="287"
+     x1="195"
+     style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" />
+  <path
+     inkscape:connector-curvature="0"
+     style="fill:none;stroke:#a80036;stroke-width:1"
+     id="MusicalInstrument-Violin"
+     d="m 145.51,354.27 c 12.48,19.76 25.51,40.37 35.69,56.48" />
+  <polygon
+     id="polygon211"
+     style="fill:none;stroke:#a80036;stroke-width:1"
+     points="261.26,361.26 277.86,374.43 266.03,381.91 "
+     transform="translate(-126.5,-24)" />
+  <path
+     inkscape:connector-curvature="0"
+     style="fill:none;stroke:#a80036;stroke-width:1"
+     id="MusicalInstrument-Guitar"
+     d="m 192.64,348.42 c 25.04,17.17 51.64,35.39 74.51,51.06" />
+  <polygon
+     id="polygon214"
+     style="fill:none;stroke:#a80036;stroke-width:1"
+     points="302.54,361.05 322.99,366.58 315.08,378.13 "
+     transform="translate(-126.5,-24)" />
+  <path
+     inkscape:connector-curvature="0"
+     style="fill:none;stroke:#a80036;stroke-width:1"
+     id="MusicalInstrument-Manufacturer"
+     d="m 91.08,350.09 c -3.97,21 -8.2,43.41 -11.46,60.66" />
+  <polygon
+     id="polygon217"
+     style="fill:#a80036;stroke:#a80036;stroke-width:1"
+     points="220,361.26 214.9551,366.4126 217.771,373.0512 222.8159,367.8986 "
+     transform="translate(-126.5,-24)" />
+  <path
+     inkscape:connector-curvature="0"
+     style="fill:none;stroke:#a80036;stroke-width:1"
+     id="Analysis-SoundQualityAnalyzer"
+     d="m 222.3,185.39 c 21.35,24.82 43.61,50.69 60.09,69.84" />
+  <polygon
+     id="polygon220"
+     style="fill:#a80036;stroke:#a80036;stroke-width:1"
+     points="340.04,199.21 340.9231,206.3668 347.869,208.3043 346.9859,201.1475 "
+     transform="translate(-126.5,-24)" />
+  <path
+     inkscape:connector-curvature="0"
+     style="fill:none;stroke:#a80036;stroke-width:1"
+     id="Analysis-MusicalInstrument"
+     d="m 130.66,187.93 c -4.56,16 -9.21,32.33 -13.37,46.92" />
+  <polygon
+     id="polygon223"
+     style="fill:#a80036;stroke:#a80036;stroke-width:1"
+     points="260.78,199.21 255.287,203.8819 257.4868,210.7493 262.9798,206.0774 "
+     transform="translate(-126.5,-24)" />
+</svg>
diff --git a/src/core/css/webcaosdb.css b/src/core/css/webcaosdb.css
index 57399e5787c28a6ecdf0c5fe7ce505b3d23f90d5..69a700376423a44bcb28a9920f1f3d15ef9a3b90 100644
--- a/src/core/css/webcaosdb.css
+++ b/src/core/css/webcaosdb.css
@@ -27,6 +27,35 @@ body {
     flex-direction: column;
 }
 
+
+div.export-data {
+    display: none;
+}
+
+tr:not(:hover) .caosdb-v-entity-version-hint-cur {
+    color: #DDD;
+}
+
+tr:hover .caosdb-v-entity-version-hint {
+    color: unset;
+}
+
+.caosdb-v-entity-version-hint {
+    color: #DDD;
+}
+
+tbody:not(:hover) tr .caosdb-v-entity-version-hint-cur {
+    color: unset;
+}
+
+.caosdb-v-entity-version-no-related {
+    color: #DDD;
+}
+
+.caosdb-v-entity-version-no-related:hover {
+    color: unset;
+}
+
 #top-navbar>ul>li>a {
     margin: 8px 0px;
     padding: 6px 12px;
diff --git a/src/core/js/caosdb.js b/src/core/js/caosdb.js
index 7fe4e9441822bf6c706a09c95aa65f0a2fef06a8..0c63fbe038908903dcf426d1c711d4fad6d3bdd2 100644
--- a/src/core/js/caosdb.js
+++ b/src/core/js/caosdb.js
@@ -1,30 +1,32 @@
 /*
-* ** header v3.0
-* This file is a part of the CaosDB Project.
-*
-* Copyright (C) 2019 IndiScale GmbH
-*
-* 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
-*/
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2018-2020 Alexander Schlemmer
+ * Copyright (C) 2018 Research Group Biomedical Physics,
+ * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+ * Copyright (C) 2019-2020 IndiScale GmbH (info@indiscale.com)
+ * Copyright (C) 2019-2020 Timm Fitschen (t.fitschen@indiscale.com)
+ *
+ * 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';
 
 /**
  * JavaScript client for CaosDB
- * A. Schlemmer, 08/2018
- * T. Fitschen, 02/2019
  *
  * Dependency: jquery
  * Dependency: webcaosdb
@@ -69,8 +71,7 @@ function getUserRealm() {
  * @return Array containing the roles of the user.
  */
 function getUserRoles() {
-    return Array.from(document.getElementsByClassName("caosdb-user-role")
-                     ).map(el => el.innerText);
+    return Array.from(document.getElementsByClassName("caosdb-user-role")).map(el => el.innerText);
 }
 
 /**
@@ -189,9 +190,9 @@ function getPropertyDatatype(element) {
         x => x.classList.contains("caosdb-property-datatype"),
         x => x.classList.contains("caosdb-preview-container"));
 
-    if(dt_elem.length == 1){
+    if (dt_elem.length == 1) {
         return $(dt_elem[0]).text();
-    } else if (dt_elem.length > 1){
+    } else if (dt_elem.length > 1) {
         throw new Error("The datatype of this property could not uniquely be determined.");
     }
 
@@ -219,14 +220,63 @@ function getEntityName(element) {
 }
 
 /**
- * Return the path of element.
+ * Return the path of an entity.
+ *
+ * This attribute is always set for file entities.
+ *
  * 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.
+ *
+ * @param {HTMLElement} entity - entity in HTML representation.
+ * @return A string containing the path of the entity.
  */
 function getEntityPath(element) {
+    const path = $(element).find('.caosdb-f-entity-path').val();
+    if (typeof path !== 'undefined') {
+        return path;
+    }
+
     return getEntityHeadingAttribute(element, "path");
 }
 
+/**
+ * Return the checksum of an entity.
+ *
+ * This attribute is always set for file entities.
+ *
+ * If the corresponding label can not be found or the label is ambigious undefined is returned.
+ *
+ * @param {HTMLElement} entity - entity in HTML representation.
+ * @return A string containing the checksum of the entity.
+ */
+function getEntityChecksum(element) {
+    const checksum = $(element).find('.caosdb-f-entity-checksum').val();
+    if (typeof checksum !== 'undefined') {
+        return checksum;
+    }
+
+    return getEntityHeadingAttribute(element, "checksum");
+}
+
+/**
+ * Return the size of element. This attribute is always set for file entities.
+ * If the corresponding label can not be found or the label is ambigious undefined is returned.
+ * @return A string containing the size of the element.
+ */
+function getEntitySize(element) {
+    // TODO: check if this if block is needed
+    //       it is analogous to getEntityDescription
+    // if ($(element).find('[data-entity-size]').length == 1) {
+    //     return $(element).find('[data-entity-size]')[0].dataset.entitySize;
+    // }
+
+    if (typeof $(element).find('.caosdb-f-entity-size').val() !== 'undefined') {
+        // This is needed for the edit mode to work properly:
+        return $(element).find('.caosdb-f-entity-size').val();
+    }
+
+    return getEntityHeadingAttribute(element, "size");
+}
+
 /**
  * Return the id of an entity.
  * @param element The element holding the entity.
@@ -286,6 +336,32 @@ function input2caosdbDate(date, time) {
     return date + "T" + time;
 }
 
+/**
+ * Return true if the current user has the given permission for the given
+ * entity.
+ *
+ * @param {HTMLElement} entity
+ * @return {boolean}
+ */
+var hasEntityPermission = function (entity, permission) {
+    if (userHasRole("administration")) {
+        // administration is a special role. It has * permissions.
+        return true;
+    }
+    const permissions = getAllEntityPermissions(entity);
+    return permissions.indexOf(permission.toUpperCase()) > -1;
+}
+
+/**
+ * Get all permissions the current user has for this entity.
+ * @param {HTMLElement} entity
+ * @return {string[]} array of permissions.
+ */
+var getAllEntityPermissions = function (entity) {
+    const permissions = $(entity).find("[data-permission]").toArray().map(x => x.getAttribute("data-permission"));
+    return permissions;
+}
+
 /**
  * Take a datetime from caosdb and return a date and a time
  * suitable for html inputs.
@@ -355,6 +431,7 @@ 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') {
+        // This is needed for the edit mode to work properly:
         return $(element).find('.caosdb-f-entity-description').val();
     }
 
@@ -423,7 +500,7 @@ function getEntityXML(ent_element) {
 
 function getPropertyName(element) {
     var name_element = element.getElementsByClassName("caosdb-property-name");
-    if(name_element.length > 0) {
+    if (name_element.length > 0) {
         return name_element[0].textContent;
     } else if ($(element).is("[data-property-name]")) {
         return $(element).attr("data-property-name");
@@ -522,7 +599,7 @@ function getPropertyFromElement(propertyelement, names = undefined) {
     var value_string = undefined;
     if (valel && valel.textContent.length > 0) {
         value_string = valel.textContent;
-    } else if (valel && valel.value &&  valel.value.length > 0 ) {
+    } else if (valel && valel.value && valel.value.length > 0) {
         value_string = valel.value;
     }
 
@@ -539,7 +616,7 @@ function getPropertyFromElement(propertyelement, names = undefined) {
 
     if (typeof value_string !== "undefined") {
         // This is set to true, when there is a reference or a list of references:
-        if(typeof property.reference === "undefined") {
+        if (typeof property.reference === "undefined") {
             property.reference = (valel.getElementsByClassName("caosdb-id").length > 0);
         }
 
@@ -620,6 +697,91 @@ function getProperties(element) {
     return list;
 }
 
+
+/**
+ * Construct XPath expression from selectors.
+ *
+ * Used by getPropertyValues.
+ *
+ * @param {String[][]} selectors
+ * @return {String[]} XPath expressions.
+ */
+var _constructXpaths = function (selectors) {
+    const xpaths = [];
+    for (let sel of selectors) {
+        var expr = "Property";
+        if (sel[0] == "id") {
+            expr = "";
+        }
+        for (let i = 0; i < sel.length; i++) {
+            const segment = sel[i];
+            if (segment == "id") {
+                expr += `@id`;
+            } else if (segment) {
+                expr += `[@name='${segment}']`;
+            }
+
+            if (i+1 < sel.length) {
+                expr += "//Property"
+            }
+        }
+        xpaths.push(expr);
+    }
+    return xpaths;
+}
+
+/**
+ * Return a table where each row represents an entity and each column a property.
+ *
+ * This also works for entities from select queries, where the properties
+ * are deeply nested, e.g. when each entity references a "Geo Location"
+ * record which have latitude and longitude properties:
+ *
+ * `getPropertyValues(entities, [["Geo Location", "latitude"], ["Geo Location", "longitude"]])`
+ *
+ * Use empty strings for selector elements when the property name is irrelevant:
+ *
+ * `getPropertyValues(entities, [["", "latitude"], ["", "longitude"]])`
+ *
+ * Limitations:
+ *
+ * 1. Currently, this implementation assumes that properties (and subproperties
+ *    for that matter) have unique names, entity-wide and do have a LIST
+ *    datatype.
+ *
+ * 2. It only handles one of the many special cases, which is "id". Other
+ *    special cases ("name", "description", "unit", etc.) are to be added when
+ *    needed.
+ *
+ * @param {XMLElement[]) entities
+ * @param {String[][]} selectors
+ * @return {String[][]} A table of the property values for each entity.
+ */
+var getPropertyValues = function (entities, selectors) {
+    const entity_iter = entities.evaluate("/Response/Record", entities);
+
+    const table = [];
+    const xpaths = _constructXpaths(selectors)
+
+    var current_entity = entity_iter.iterateNext();
+    while (current_entity) {
+        const row = [];
+        for (let expr of xpaths) {
+            const property = entities.evaluate(expr, current_entity).iterateNext();
+            if (typeof property != "undefined" && property != null) {
+                row.push(property.textContent.trim());
+            } else {
+                row.push(undefined)
+            }
+
+        }
+        table.push(row);
+        current_entity = entity_iter.iterateNext();
+    }
+
+    return table;
+}
+
 /**
  * Sets a property with some basic type checking.
  *
@@ -663,8 +825,8 @@ function setPropertySafe(valueelement, property, propold) {
         }
     } else {
         /* DEPRECATED css class .caosdb-property-text-value - Use
-        * .caosdb-f-property-single-raw-value or introduce new
-        * .caosdb-v-property-text-value */
+         * .caosdb-f-property-single-raw-value or introduce new
+         * .caosdb-v-property-text-value */
         valueelement.innerHTML = "<span class='caosdb-property-text-value'>" + property.value + "</span>";
     }
 }
@@ -711,7 +873,7 @@ function setProperty(element, property) {
  *     equivalent).
  * @returns {string} The value of the the property with property_name or `undefined` when this property is not available for this entity.
  */
-function getProperty(element, property_name, case_sensitive=true) {
+function getProperty(element, property_name, case_sensitive = true) {
     var props;
     if (case_sensitive) {
         props = getProperties(element).filter(el => el.name == property_name);
@@ -808,7 +970,7 @@ function appendProperty(doc, element, property, append_datatype = false) {
  *
  * @param {string} root - the new root element.
  * @returns {(Document|DocumentFragement)} the new document.
- */ 
+ */
 function _createDocument(root) {
     var doc = undefined;
     if (window.DocumentFragment) {
@@ -827,17 +989,20 @@ function _createDocument(root) {
  * This function uses the object notation.
  * @see getProperties
  * @see getParents
- * @param role Record, RecordType or Property
+ * @param role Record, RecordType, Property or File (in case of files the three file arguments must be used!)
  * @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.
+ * @param description A description for this entity.
  * @return {Document|DocumentFragment} - An xml document holding the newly
  *         created entity.
  *
  */
 function createEntityXML(role, name, id, properties, parents,
-    append_datatypes = false, datatype = undefined, description = undefined, unit = undefined) {
+    append_datatypes = false, datatype = undefined, description = undefined,
+    unit = undefined,
+    file_path = undefined, file_checksum = undefined, file_size = undefined) {
 
     var doc = _createDocument(role);
     var nelnode = doc.children[0];
@@ -869,9 +1034,49 @@ function createEntityXML(role, name, id, properties, parents,
             appendProperty(doc, nelnode, properties[i], append_datatypes);
         }
     }
+
+    if (role.toLowerCase() == "file") {
+        /*
+          File path, checksum and size are needed for File entities.
+
+          An error is raised when these arguments are not set.
+        */
+        if (file_path === undefined || file_checksum === undefined || file_size === undefined) {
+            throw "Path, checksum and size must not be undefined in case of file entities.";
+        }
+
+        $(nelnode).attr("path", file_path);
+        $(nelnode).attr("checksum", file_checksum);
+        $(nelnode).attr("size", file_size);
+    }
     return doc;
 }
 
+/**
+ * Create an XML for a file entity.
+ * This is a convenience function for creating XML from file entities.
+ * This function uses the object notation.
+ * @see getProperties
+ * @see getParents
+ * @param name The name of the entity. Can be undefined.
+ * @param id The id of the entity. Can be undefined.
+ * @param parents A list of parents.
+ * @param file_path The path of the file in the CaosDB file system.
+ * @param file_checksum The checksum of the file.
+ * @param file_size The size of the file in bytes.
+ * @param description A description for this entity.
+ * @return {Document|DocumentFragment} - An xml document holding the newly
+ *         created entity.
+ *
+ */
+function createFileXML(name, id, parents,
+    file_path, file_checksum, file_size,
+    description = undefined) {
+    return createEntityXML("File", name, id, {}, parents,
+        false, undefined, description, undefined,
+        file_path, file_checksum, file_size);
+}
+
 /**
  * Helper function to wrap xml documents into another node which could e.g. be
  * Update, Response, Delete.
@@ -885,7 +1090,7 @@ function wrapXML(root, xmls) {
     caosdb_utils.assert_string(root, "param `root`");
 
     var doc = _createDocument(root);
-    for (var i=0; i < xmls.length; i++) {
+    for (var i = 0; i < xmls.length; i++) {
         doc.firstElementChild.appendChild(xmls[i].firstElementChild);
     }
 
diff --git a/src/core/js/edit_mode.js b/src/core/js/edit_mode.js
index e81381643bead77c849741c3fc2cb076803d121d..9df7505f1e81fd0234aaa4461444535846921885 100644
--- a/src/core/js/edit_mode.js
+++ b/src/core/js/edit_mode.js
@@ -412,8 +412,20 @@ var edit_mode = new function() {
      */
     this.form_to_xml = function(entity_form) {
         const obj = form_elements.form_to_object($(entity_form).find("form")[0]);
+        var entityRole = getEntityRole(entity_form);
+        var file_path = undefined;
+        var file_checksum = undefined;
+        var file_size = undefined;
+        if (entityRole.toLowerCase() == "file") {
+            file_path = getEntityPath(entity_form);
+            file_checksum = getEntityChecksum(entity_form);
+            file_size = getEntitySize(entity_form);
+            console.log(file_path);
+            console.log(file_checksum);
+            console.log(file_size);
+        }
         return createEntityXML(
-            getEntityRole(entity_form),
+            entityRole,
             getEntityName(entity_form),
             getEntityID(entity_form),
             edit_mode.getProperties(entity_form),
@@ -422,6 +434,7 @@ var edit_mode = new function() {
             edit_mode.get_datatype_str(obj),
             getEntityDescription(entity_form),
             obj.unit,
+            file_path, file_checksum, file_size
         );
     }
 
@@ -630,6 +643,8 @@ var edit_mode = new function() {
             header.attr("title", "");
         } else if (getEntityRole(roleElem[0]) == "File") {
             inputs.push(this.make_input("path", getEntityPath(entity)));
+            inputs.push(this.make_input("checksum", getEntityChecksum(entity)));
+            inputs.push(this.make_input("size", getEntitySize(entity)));
         }
         // remove other stuff
         header.children().remove();
diff --git a/src/core/js/ext_autocomplete.js b/src/core/js/ext_autocomplete.js
index a60ad945f76c0e99ef70713aef82970a137f7973..9d99241485245ca6232a7626c04f7998579174e8 100644
--- a/src/core/js/ext_autocomplete.js
+++ b/src/core/js/ext_autocomplete.js
@@ -47,6 +47,7 @@ var ext_autocomplete = new function () {
         "STORED AT",
         "HAS A PROPERTY",
         "HAS BEEN",
+        "ANY VERSION OF",
     ];
     this.version = "0.1";
 
diff --git a/src/core/js/ext_bookmarks.js b/src/core/js/ext_bookmarks.js
index 07de3014825de6c64040e98e3da143d4ad5b8228..d99ff362d8a26ee4818a76a624c80f55f757c419 100644
--- a/src/core/js/ext_bookmarks.js
+++ b/src/core/js/ext_bookmarks.js
@@ -29,7 +29,7 @@
  * all entities and resetting the bookmarks.
  *
  * @module ext_bookmarks
- * @version 0.1
+ * @version 0.2
  *
  * @param jQuery - well-known library.
  * @param log - singleton from loglevel library or javascript console.
@@ -217,14 +217,25 @@ var ext_bookmarks = function ($, logger, config) {
      * Generate the TSV data for the export callback with all current
      * bookmarks.
      *
+     * TODO merge with caosdb_utils.create_tsv_table.
+     *
      * @param {string[]} bookmarks - array of ids.
-     */
-    const get_export_table = async function (bookmarks, preamble, tab, newline) {
+     * @param {string} [preamble="data:text/csv;charset=utf-8,"] - the preamble
+     *     which is used for generating tables which can be downloaded by
+     *     browsers.
+     * @param {string} [tab="%09"] - the tab string.
+     * @param {string} [newline="%0A"] - the newline string.
+     * @param {string[]} [leading_comments] - comment lines which are to be put
+     * even before the header line. They should be appropriately escaped (e.g.
+     * with "%23").
+     */
+    const get_export_table = async function (bookmarks, preamble, tab, newline, leading_comments) {
         // TODO merge with related code in the module "caosdb_table_export".
         preamble = ((typeof preamble == 'undefined') ? "data:text/csv;charset=utf-8,": preamble);
         tab = tab || "%09";
         newline = newline || "%0A";
-        const header = tsv_columns.join(tab) + newline;
+        leading_comments = (leading_comments ? leading_comments.join(newline) + newline : "");
+        const header = leading_comments + tsv_columns.join(tab) + newline;
         const rows = [];
         for (let i = 0; i < bookmarks.length; i++) {
             rows.push((await get_export_table_row(bookmarks[i])).join(tab));
@@ -232,6 +243,23 @@ var ext_bookmarks = function ($, logger, config) {
         return `${preamble}${header}${rows.join(newline)}`;
     }
 
+    /**
+     * Download the table with a given filename.
+     *
+     * This method adds a temporay <A> element to the dom tree and triggers
+     * "click" because otherwise the filename cannot be set.
+     *
+     * See also:
+     * https://stackoverflow.com/questions/21177078/javascript-download-csv-as-file
+     */
+    const download = function (table, filename) {
+        console.log("download");
+        const link = $(`<a style="display: none" download="${filename}" href="${table}"/>`);
+        $("body").append(link);
+        link[0].click();
+        link.remove();
+    }
+
     /**
      * Trigger the download of the TSV table with all current bookmarks.
      *
@@ -239,8 +267,18 @@ var ext_bookmarks = function ($, logger, config) {
      */
     const export_bookmarks = async function () {
         const ids = get_bookmarks();
-        const export_table = await get_export_table(ids);
-        window.location.href = export_table;
+        const versioned_bookmarks = []
+        for (let id of ids) {
+            if (id.indexOf("@") > -1) {
+                versioned_bookmarks.push(id);
+            } else {
+                versioned_bookmarks.push(id + "@" + (await get_bookmark_data(id, "Version")));
+            }
+        }
+        const uri = get_collection_link(ids);
+        const leading_comments = [encodeURIComponent(`#Link to all entities: ${uri}`)];
+        const export_table = await get_export_table(ids, undefined, undefined, undefined, leading_comments);
+        download(export_table, "bookmarked_entities.tsv");
     }
 
     /**
@@ -387,8 +425,10 @@ var ext_bookmarks = function ($, logger, config) {
      */
     const collect_bookmark_data = function (id) {
         for (let data_key in data_getters) {
-            // do nothing, only trigger the fetching
-            get_bookmark_data(id, data_key)
+            if (data_no_cache.indexOf(data_key) == -1) {
+                // do nothing, only trigger the fetching
+                get_bookmark_data(id, data_key)
+            }
         }
     }
 
@@ -625,25 +665,57 @@ $(document).ready(function () {
         // This getter retrieves a file's path from the page or, if necessary,
         // from the server.
         const get_path = async function (id) {
-            const entity = $(`[id='${id}']`);
-            if (entity.length > 0) {
-                return getEntityPath(entity[0]) || "";
+            if (id.indexOf("@") > -1) {
+              const entity = $(`[data-bmval='${id}']`);
+              if (entity.length > 0) {
+                  return getEntityPath(entity[0]) || "";
+              }
             }
             return $(await transaction.retrieveEntityById(id)).attr("path");
         }
 
+        // This retrieves the head version id
+        const get_version = async function (id) {
+            return $(await transaction.retrieveEntityById(id)).find("Version").attr("id");
+        }
+
+        const get_name = async function (id) {
+            if (id.indexOf("@") > -1) {
+              const entity = $(`[data-bmval='${id}']`);
+              if (entity.length > 0) {
+                  return getEntityName(entity[0]) || "";
+              }
+            }
+            return $(await transaction.retrieveEntityById(id)).attr("name");
+        }
+
+        const get_rt = async function (id) {
+            if (id.indexOf("@") > -1) {
+              const entity = $(`[data-bmval='${id}']`);
+              if (entity.length > 0) {
+                  return getParents(entity[0]).join("/");
+              }
+            }
+            const parent_names = $(await transaction.retrieveEntityById(id))
+                .find("Parent").toArray().map(x => x.getAttribute("name"))
+            return parent_names.join("/");
+        }
+
         // these columns will be in the export
-        const tsv_columns = ["ID", "Version", "URI", "Path"];
+        const tsv_columns = ["ID", "Version", "URI", "Path", "Name", "RecordType"];
         // functions for collecting the export data for a particular bookmarked id.
         const data_getters = {
-            "ID": (id) => id.split("@")[0],
-            "Version": (id) => id.split("@")[1],
+            "ID": (id) => id.indexOf("@") > -1 ? id.split("@")[0] : id,
+            "Version": async (id) => id.indexOf("@") > -1 ? id.split("@")[1] : await get_version(id),
             "Path": get_path,
-            "URI": (id) => get_context_root() + id,
+            "URI": async (id) => get_context_root() + id + (id.indexOf("@") > -1 ? "" : ("@" + await get_version(id))),
+            "Name": get_name,
+            "RecordType": get_rt,
         };
-        // no need to cache these because they can be calculated on-the-fly and
-        // we don't want to polute the bookmark_storage.
-        const data_no_cache = ["ID", "Version", "URI"];
+
+        // we cannot cache these because the the values might change unnoticed
+        // when the head moves to a newer version.
+        const data_no_cache = ["ID", "Version", "URI", "Path", "Name", "RecordType"];
 
         const config = {
             get_context_root: get_context_root,
diff --git a/src/core/js/ext_bottom_line.js b/src/core/js/ext_bottom_line.js
index ccd65744c9faa38a438c8fae2128c5b8465ecbb6..afeecfa0893957ede661f6c0ae560a39fa92bbd3 100644
--- a/src/core/js/ext_bottom_line.js
+++ b/src/core/js/ext_bottom_line.js
@@ -23,6 +23,28 @@
 
 'use strict';
 
+/**
+ * @typedef {BottomLineConfig}
+ * @property {string|HTMLElement} fallback - Fallback content if none of
+ *     the creators are applicable.
+ * @property {string} version - the version of the configuration which must
+ *     match this module's version.
+ * @property {CreatorConfig[]} creators - an array of creators.
+ */
+
+/**
+ * @typedef {CreatorConfig}
+ * @property {string} [id] - a unique id for the creator. optional, for
+ *     debuggin purposes.
+ * @property {function|string} is_applicable - If this is a string this has
+ *     to be valid javascript! An asynchronous function which accepts one
+ *     parameter, an entity in html representation, and which returns true
+ *     iff this creator is applicable for the given entity.
+ * @property {string} create - This has to be valid javascript! An
+ *     asynchronous function which accepts one parameter, an entity in html
+ *     representation. It returns a HTMLElement or text node which will be
+ *     shown in the bottom line container iff the creator is applicable.
+ */
 
 /**
  * Add a special section to each entity one the current page where a thumbnail,
@@ -59,28 +81,7 @@ var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntit
      *     (entity) Note: This property can as well be a
      *     javascript string which evaluates to a function.
      */
-    /**
-     * @type {BottomLineConfig}
-     * @property {string|HTMLElement} fallback - Fallback content if none of
-     *     the creators are applicable.
-     * @property {string} version - the version of the configuration which must
-     *     match this module's version.
-     * @property {CreatorConfig[]} creators - an array of creators.
-     */
 
-    /**
-     * @type {CreatorConfig}
-     * @property {string} [id] - a unique id for the creator. optional, for
-     *     debuggin purposes.
-     * @property {function|string} is_applicable - If this is a string this has
-     *     to be valid javascript! An asynchronous function which accepts one
-     *     parameter, an entity in html representation, and which returns true
-     *     iff this creator is applicable for the given entity.
-     * @property {string} create - This has to be valid javascript! An
-     *     asynchronous function which accepts one parameter, an entity in html
-     *     representation. It returns a HTMLElement or text node which will be
-     *     shown in the bottom line container iff the creator is applicable.
-     */
 
 
     /**
@@ -140,7 +141,7 @@ var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntit
      * Tiff files are decompressed if necessary and converted into png by UTIF library.
      *
      * @param {HTMLElement} entity
-     * @return {Promise for HTMLElement} Promise for an IMG element.
+     * @return {Promise | HTMLElement} Promise for an IMG element.
      */
     const _create_tiff_preview = function(entity) {
         const path = connection.getFileSystemPath() + getEntityPath(entity);
@@ -427,6 +428,9 @@ var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntit
         }
     }
 
+    /**
+     * @exports ext_bottom_line
+     */
     return {
         contentReadyEvent: contentReadyEvent,
         contentShownEvent: contentShownEvent,
diff --git a/src/core/js/ext_map.js b/src/core/js/ext_map.js
index 67c7bfe799c8c96eb945ec9334ad87fa3f7d70f9..3b89f72be05d2307cdcc2e624e4b8a69684d591f 100644
--- a/src/core/js/ext_map.js
+++ b/src/core/js/ext_map.js
@@ -25,7 +25,7 @@
 
 /**
  * @module caosdb_map
- * @version 0.3
+ * @version 0.4
  *
  * For displaying a geographical map which shows entities at their associated
  * geolocation.
@@ -34,7 +34,7 @@
  * `conf/ext/json/ext_map.json` and comply with the {@link MapConfig} type
  * which is described below.
  *
- * The current version is 0.3. It is not considered to be stable because
+ * The current version is 0.4. It is not considered to be stable because
  * implementation of the graticule still is not satisfactory.
  *
  * Apart from that the specification of the configuration and the
@@ -43,7 +43,7 @@
 var caosdb_map = new function () {
 
     var logger = log.getLogger("caosdb_map");
-    this.version = "0.3";
+    this.version = "0.4";
     this.dependencies = ["log", {
         "L": ["latlngGraticule", "Proj"]
     }, "navbar", "caosdb_utils"];
@@ -84,10 +84,14 @@ var caosdb_map = new function () {
     /**
      * The SelectConfig object configures the custom {@link select_handler}
      * plugin for the Leaflet.js module, especially the query generation (for
-     * searching for Entities in the selected area).
+     * searching for Entities in the selected area) and when retrieving
+     * entities to be shown.
      *
-     * The generated query has the pattern <code>FIND {@link query.role} {@link
-     * query.entity} WITH ...<code>. The dots stand for the area filter here.
+     * The generated query for a selected area has the pattern 
+     * <code>FIND {@link query.role} {@link query.entity} WITH PATH AREA<code>. 
+     * PATH can be empty or represent a configured path to some entity, e.g. 
+     * `WITH RT1 WITH RT2`.
+     * AREA stand for the area filter here.
      *
      * The default values of the {@link query} result in queries for any Record
      * in the selected map area.
@@ -98,6 +102,8 @@ var caosdb_map = new function () {
      *     are to be searched in the selected ares.
      * @property {string} [query.entity] The (parent) entity to be searched
      *     for in the area. Defaults to empty string.
+     * @property {object} [paths] - A dictionary of paths that define from
+     *     which entities the geographic location shall be taken.
      */
 
     /**
@@ -332,6 +338,7 @@ var caosdb_map = new function () {
                 "role": "RECORD",
                 "entity": "",
             },
+            "paths": {},
         },
     }
 
@@ -373,21 +380,190 @@ var caosdb_map = new function () {
      */
 
     /**
-     * Implements {@link mapEntityGetter}.
+     * Generates a Property Operator Value (POV) expression by chaining the
+     * provided arguments with "WITH".
+     *
+     * @param {string[]} props - array with the names of RecordTypes
+     * @returns {string} string with the the filter
+     */
+    this._get_with_POV = function (props) {
+        var pov = ""
+        for (let p of props) {
+            pov = pov + ` WITH ${p} `;
+        }
+        return pov;
+    }
+
+    /**
+     * Generates a Property Operator Value (POV) by joining ids with OR.
+     *
+     * @param {number[]} ids - array of ids for the filter
+     * @returns {string} string with the the filter
+     */
+    this._get_id_POV = function (ids) {
+        ids = ids.map(x => "id=" + x);
+        return "WITH " + ids.join(" or ")
+    }
+
+    /**
+     * Generates a SELECT query string that applies the provided path of 
+     * properties as POV and as selector
+     *
+     * If ids is provided, the condition is not created from the path, but
+     * from ids.
+     *
+     * @param {DataModelConfig} datamodel - datamodel of the entities to be returned.
+     * @param {string[]} path - array with the names of RecordTypes
+     * @param {number[]} ids - array of ids for the filter
+     * @returns {string} query string
      */
-    this._get_current_page_entities = function (
-        datamodel, north, south, west, east) {
-        const container = $(".caosdb-f-main-entities")[0];
+    this._get_select_with_path = function (datamodel, path, ids) {
+        if (typeof datamodel === "undefined") {
+            throw new Error("Supply the datamodel.")
+        }
+        if (typeof path === "undefined" || path.length == 0) {
+            throw new Error("Supply at least a RecordType.")
+        }
+        const recordtype = path[0];
+        const props = path.slice(1, path.length)
+        var selector = props.join(".")
+        if (selector != "") {
+            selector = selector + "."
+        }
+        var pov = undefined;
+        if (typeof ids === "undefined") {
+            pov = (caosdb_map._get_with_POV(props) +
+                ` WITH ${datamodel.lat} AND ${datamodel.lng}`);
+
+        } else {
+            pov = caosdb_map._get_id_POV(ids);
+        }
+        return `SELECT parent,${selector}${datamodel.lat},${selector}${datamodel.lng} FROM ENTITY ${recordtype} ${pov} `;
+
+    }
+
+
+    /**
+     * Returns a dictionary where for each top level record in the xmldoc 
+     * The long and lat is assigned.
+     *
+     * depth: the depth of the tree including the top level record type:
+     * e.g. RT1->prop1->prop2->lat/long would be a depth=3
+     *
+     * @param {XMLDocument} xmldoc - xml document containing the entities
+     * @param {number} depth - the depth at which the properties shall be taken
+     * @param {DataModelConfig} datamodel - datamodel of the entities to be returned.
+     * @returns {Object} a dictionary where for each id as key the location ist
+     *                   stored as [lat, lng]
+     */
+    this._get_leaf_prop = function (xmldoc, depth, datamodel) {
+        const paths = [
+            ["id"],
+            // The following creates a list: ["", "", ... (depth times), lat/long]
+            ("__split__".repeat(depth) + datamodel.lat).split("__split__"),
+            ("__split__".repeat(depth) + datamodel.lng).split("__split__"),
+        ];
+        const propertyValues = getPropertyValues(xmldoc, paths);
+
+        const leaves = {};
+        for (let row of propertyValues) {
+            leaves[row[0]] = [row[1], row[2]];
+        }
+        return leaves;
+    }
+
+    /**
+     * Template for {@link mapEntityGetter}.
+     *
+     * This implementation has a single additional parameter which is not
+     * defined by {@link mapEntityGetter}:
+     *
+     * @param {string[]} path - array of strings defining the path to the
+     *                          related entity
+     */
+    this._generic_get_current_page_entities = async function (
+        datamodel, north, south, west, east, path) {
+        var container = $(".caosdb-f-main-entities")[0];
+
+        if (typeof path !== "undefined" && path.length) {
+            var ids = []
+            for (let rec of getEntities(container)) {
+                ids.push(getEntityID(rec))
+            }
+            if (ids.length) {
+                const qs = caosdb_map._get_select_with_path(datamodel, path, ids);
+                let entities = await connection.get("Entity/?query=" + qs);
+                caosdb_map._set_subprops_at_top(entities, path.length - 1, datamodel);
+                let results = await transformation.transformEntities(entities);
+                container = $('<div>').append(results)[0];
+            } else {
+                return [];
+            }
+        }
+        // it is possible, the the page contains entities which do not have
+        // lat/lng and there doesn't exist any related entity with lat/lng.
         return caosdb_map.get_map_entities(container, datamodel);
     }
 
     /**
-     * Implements {@link mapEntityGetter}.
+     * Returns a top level record entity from xml.
+     *
+     * @param {XMLDocument} entities - xml document containing the entities
+     * @param {number} rec_id - id of the record to be returned
+     * @returns {XMLDocument} the corresponding record
+     */
+    this._get_toplvl_rec_with_id = function (entities, rec_id) {
+        let tmp = $(entities).find(`Response >[id='${rec_id}']`);
+        if (tmp.length != 1) {
+            throw new Error("There should be exactly one result record. Not " +
+                tmp.length)
+        }
+        return tmp[0];
+    }
+
+    /**
+     * Set the longitude/latitude from subproperties to the top level 
+     * records in the xml and convert everything to html.
+     *
+     * @param {XMLDocument} entities - xml document containing the entities
+     * @param {number} depth - the depth of the path (full: including the first
+     *                         recordtype)
+     */
+    this._set_subprops_at_top = function (entities, depth, datamodel) {
+        var latlong = caosdb_map._get_leaf_prop(entities, depth, datamodel);
+
+        for (let rec_id in latlong) {
+            let tmp_rec = caosdb_map._get_toplvl_rec_with_id(entities, rec_id);
+            tmp_rec.append(str2xml(`<Property name="${datamodel.lat}">${latlong[rec_id][0]}</Property>`).firstElementChild);
+            tmp_rec.append(str2xml(`<Property name="${datamodel.lng}">${latlong[rec_id][1]}</Property>`).firstElementChild);
+        }
+    }
+
+    /**
+     * Template for {@link mapEntityGetter}.
+     *
+     * This implementation has a single additional parameter which is not
+     * defined by {@link mapEntityGetter}:
+     *
+     * @param {string[]} path - array of strings defining the path to the
+     *                          related entity
      */
-    this._query_all_entities = async function (
-        datamodel, north, south, west, east) {
-        const results = await caosdb_map.query(`FIND ENTITY WITH ${datamodel.lat} AND ${datamodel.lng}`);
+    this._generic_query_all_entities = async function (
+        datamodel, north, south, west, east, path) {
+        var results = undefined;
+        if (typeof path !== "undefined" && path.length) {
+            const qs = caosdb_map._get_select_with_path(datamodel, path);
+            let entities = await connection.get("Entity/?query=" + qs);
+            caosdb_map._set_subprops_at_top(entities, path.length - 1, datamodel);
+            results = await transformation.transformEntities(entities);
+        } else {
+            results = await caosdb_map.query(
+                `FIND ENTITY WITH ${datamodel.lat} AND ${datamodel.lng}`);
+        }
         const container = $('<div>').append(results)[0];
+
+        // As soon as the SELECT query can handle subtyping, the results don't
+        // have to filtered anymore.
         return caosdb_map.get_map_entities(container, datamodel);
     }
 
@@ -410,7 +586,12 @@ var caosdb_map = new function () {
         const name = caosdb_map.make_entity_name_label(entity);
         const dms_lat = L.NumberFormatter.toDMS(lat);
         const dms_lng = L.NumberFormatter.toDMS(lng);
-        const loc = $(`<div class="small text-muted">
+        let extra_loc_hint = "";
+        let path = caosdb_map._get_current_path();
+        if (path && path.length > 1) {
+            extra_loc_hint = `<div>Location of related ${path[path.length-1]}<div>`;
+        }
+        const loc = $(`<div class="small text-muted">${extra_loc_hint}
             Lat: ${dms_lat} Lng: ${dms_lng}
             </div>`);
         const ret = $('<div/>')
@@ -421,6 +602,18 @@ var caosdb_map = new function () {
         return ret[0];
     }
 
+    /**
+     * Returns the path from the config corresponding to the value stored in
+     * the session storage (i.e. the storage should be updated before calling
+     * this method if the value changes).
+     *
+     * @returns {string[]} path - array of strings defining the path to the
+     *                          related entity
+     */
+    this._get_current_path = function () {
+        return caosdb_map.config.select.paths[sessionStorage["caosdb_map.display_path"]];
+    }
+
 
     /**
      * Default entities layers configuration with two layers:
@@ -430,43 +623,52 @@ var caosdb_map = new function () {
      *
      * @type {EntityLayerConfig[]}
      */
-    this._default_entity_layer_config = [{
-        "id": "current_page_entities",
-        "name": "Entities on the current page.",
-        "description": "Show all entities on the current page.",
-        "icon": {
-            html: '<span style="color: #00F; font-size: 20px" class="glyphicon glyphicon-map-marker"></span>',
-            iconAnchor: [10, 19],
-            className: "",
-        },
-        "zIndexOffset": 1000,
-        "datamodel": {
-            "lat": "latitude",
-            "lng": "longitude",
-            "role": "ENTITY",
-            "entity": "",
-        },
-        "get_entities": this._get_current_page_entities,
-        "make_popup": this._make_map_popup,
-    }, {
-        "id": "all_map_entities",
-        "name": "All entities",
-        "description": "Show all entities with coordinates.",
-        "icon": {
-            html: '<span style="color: #F00; font-size: 20px" class="glyphicon glyphicon-map-marker"></span>',
-            iconAnchor: [10, 19],
-            className: "",
+    this._default_entity_layer_config = {
+        "current_page_entities": {
+            "name": "Entities on the current page.",
+            "description": "Show all entities on the current page.",
+            "icon": {
+                html: '<span style="color: #00F; font-size: 20px" class="glyphicon glyphicon-map-marker"></span>',
+                iconAnchor: [10, 19],
+                className: "",
+            },
+            "zIndexOffset": 1000,
+            "datamodel": {
+                "lat": "latitude",
+                "lng": "longitude",
+                "role": "ENTITY",
+                "entity": "",
+            },
+            "get_entities": (datamodel, north, south, west, east) => {
+                let path = caosdb_map._get_current_path()
+                return caosdb_map._generic_get_current_page_entities(
+                    datamodel, north, south, west, east, path)
+            },
+            "make_popup": this._make_map_popup,
         },
-        "zIndexOffset": 0,
-        "datamodel": {
-            "lat": "latitude",
-            "lng": "longitude",
-            "role": "ENTITY",
-            "entity": "",
+        "all_map_entities": {
+            "name": "All entities",
+            "description": "Show all entities with coordinates.",
+            "icon": {
+                html: '<span style="color: #F00; font-size: 20px" class="glyphicon glyphicon-map-marker"></span>',
+                iconAnchor: [10, 19],
+                className: "",
+            },
+            "zIndexOffset": 0,
+            "datamodel": {
+                "lat": "latitude",
+                "lng": "longitude",
+                "role": "ENTITY",
+                "entity": "",
+            },
+            "get_entities": (datamodel, north, south, west, east) => {
+                let path = caosdb_map._get_current_path()
+                return caosdb_map._generic_query_all_entities(
+                    datamodel, north, south, west, east, path)
+            },
+            "make_popup": this._make_map_popup,
         },
-        "get_entities": this._query_all_entities,
-        "make_popup": this._make_map_popup,
-    }, ];
+    };
 
 
     /**
@@ -730,6 +932,22 @@ var caosdb_map = new function () {
             throw new Error("Could not find view " + id);
         }
 
+        /**
+         * Reload layers.
+         */
+        this._reload_layers = function () {
+            caosdb_map._show_load_info()
+            const promises = []
+            for (const layer of caosdb_map.layers) {
+                promises.push(caosdb_map._fill_layer(layer.layer_group,
+                    caosdb_map._default_entity_layer_config[layer.id]));
+            }
+            Promise.all(promises).then((val) => {
+                caosdb_map._hide_load_info()
+            })
+        }
+
+
 
         /** Initialize the caosdb_map module.
          *
@@ -807,16 +1025,36 @@ var caosdb_map = new function () {
                         view_config);
 
                     // init entity layers
-                    var layers = this.init_entity_layers(this._default_entity_layer_config);
+                    this.layers = this.init_entity_layers(this._default_entity_layer_config);
                     var layerControl = L.control.layers();
-                    for (const layer of layers) {
+
+                    const promises = []
+                    for (const layer of this.layers) {
+
+                        promises.push(caosdb_map._fill_layer(layer.layer_group,
+                            this._default_entity_layer_config[layer.id]));
                         layerControl.addOverlay(layer.layer_group, layer.chooser_html.outerHTML);
                         layer.layer_group.addTo(this._map);
                     }
+                    Promise.all(promises).then((val) => {
+                        caosdb_map._hide_load_info()
+                    })
                     layerControl.addTo(this._map);
 
                     // initialize handlers
                     this.add_select_handler(this._map);
+
+
+                    this.path_ddm = this._get_path_ddm(
+                        (event) => {
+                            sessionStorage["caosdb_map.display_path"] = event.target.value;
+                            caosdb_map._reload_layers();
+                        },
+                        this.config.select.paths
+                    );
+                    this._map.addControl(this.path_ddm);
+
+
                     this.add_view_change_handler(
                         this._map,
                         config.views,
@@ -858,12 +1096,33 @@ var caosdb_map = new function () {
          */
         this.init_entity_layers = function (configs) {
             var ret = []
-            for (const conf of configs) {
-                ret.push(this.init_entity_layer(conf));
+            for (let name in configs) {
+                configs[name]["id"] = name;
+                ret.push(this._init_single_entity_layer(configs[name]));
             }
             return ret;
         }
 
+        /**
+         * Initialize an entity layer.
+         *
+         * @param {EntityLayerConfig} config
+         * @return {_EntityLayer}
+         */
+        this._fill_layer = async function (layer_group, config) {
+            // in case load is called on a filled layer: clear first
+            layer_group.clearLayers();
+
+            var entities = await config.get_entities(config.datamodel);
+            layer_group.entities = entities;
+            var markers = caosdb_map.create_entity_markers(
+                entities, config.datamodel, config.make_popup,
+                config.zIndexOffset, config.icon);
+
+            for (const marker of markers) {
+                layer_group.addLayer(marker);
+            }
+        };
 
         /**
          * Initialize an entity layer.
@@ -871,24 +1130,11 @@ var caosdb_map = new function () {
          * @param {EntityLayerConfig} config
          * @return {_EntityLayer}
          */
-        this.init_entity_layer = function (config) {
-            logger.trace("enter init_entity_layer", config);
+        this._init_single_entity_layer = function (config) {
+            logger.trace("enter _init_single_entity_layer", config);
 
             var layer_group = L.layerGroup();
 
-            // load all entities into layer group
-            var _load = async function (layer_group, config) {
-                var entities = await config.get_entities(config.datamodel);
-                var markers = caosdb_map.create_entitiy_markers(
-                    entities, config.datamodel, config.make_popup,
-                    config.zIndexOffset, config.icon);
-
-                for (const marker of markers) {
-                    layer_group.addLayer(marker);
-                }
-            };
-            _load(layer_group, config);
-
             var ret = {
                 "id": config.id,
                 "active": typeof config.active === "undefined" || config.active,
@@ -975,7 +1221,6 @@ var caosdb_map = new function () {
                 L.Handler.extend(this.select_handler);
         }
 
-
         /**
          * Show the query panel if not visible, collapse the query shortcuts
          * if visible and fill the query string into the text input of the
@@ -1048,9 +1293,18 @@ var caosdb_map = new function () {
         this.generate_query_from_bounds = function (north, south, west,
             east) {
             const role = this.config.select.query.role;
-            const entity = this.config.select.query.entity;
+            var entity = this.config.select.query.entity;
             const lat = this.config.datamodel.lat;
             const lng = this.config.datamodel.lng;
+            let path = caosdb_map._get_current_path();
+            if (path && path.length > 0 && entity == "") {
+                entity = path[0];
+            }
+            var additional_path = ""
+            if (path && path.length > 1) {
+                additional_path = caosdb_map._get_with_POV(
+                    path.slice(1, path.length))
+            }
 
             const query_filter = " ( " + lat + " < '" + north +
                 "' AND " + lat +
@@ -1058,7 +1312,7 @@ var caosdb_map = new function () {
                 "' AND " +
                 lng + " < '" + east + "' ) ";
 
-            const query = "FIND " + role + " " + entity +
+            const query = "FIND " + role + " " + entity + additional_path +
                 " WITH " + query_filter;
             return query
         }
@@ -1201,7 +1455,7 @@ var caosdb_map = new function () {
 
             const entity_on_page = $(`#${id}`).length > 0;
             const href = entity_on_page ? `#${id}` : connection.getBasePath() + `Entity/${id}`
-            const link_title = entity_on_page ? "Jump to this entitiy." : "Browse to this entity.";
+            const link_title = entity_on_page ? "Jump to this entity." : "Browse to this entity.";
             const link = $(`<a title="${link_title}" href="${href}"/>`)
                 .addClass("pull-right")
                 .append(`<span class="glyphicon glyphicon-share-alt"/></a>`);
@@ -1239,8 +1493,8 @@ var caosdb_map = new function () {
          * @param {DivIcon_options} icon_options
          * @returns {L.Marker[]} an array of markers for the map.
          */
-        this.create_entitiy_markers = function (entities, datamodel, make_popup, zIndexOffset, icon_options) {
-            logger.trace("enter create_entitiy_markers", entities, datamodel, zIndexOffset, icon_options);
+        this.create_entity_markers = function (entities, datamodel, make_popup, zIndexOffset, icon_options) {
+            logger.trace("enter create_entity_markers", entities, datamodel, zIndexOffset, icon_options);
 
             var ret = []
             for (const map_entity of entities) {
@@ -1440,6 +1694,83 @@ var caosdb_map = new function () {
             },
         }
 
+        /**
+         * Shows the loading information div of the map
+         */
+        this._show_load_info = function () {
+            $(".caosdb-f-map-loading").attr("style", "display:inherit");
+        }
+
+        /**
+         * Hides the loading information div of the map
+         */
+        this._hide_load_info = function () {
+            $(".caosdb-f-map-loading").attr("style", "display:None");
+        }
+
+        /**
+         * Return a new leaflet control for setting paths to use for geo location
+         *
+         * @param {function} callback - a callback applies the effect of a
+         *                              changed path
+         * @returns {L.Control} the drop down menu button.
+         */
+        this._get_path_ddm = function (callback, paths) {
+
+            // TODO flatten the structure of the code and possibly merge it with the query_button code.
+            var path_ddm = L.Control.extend({
+                options: {
+                    position: "bottomright"
+                },
+
+                onAdd: function (m) {
+                    return this.button;
+                },
+
+                button: function () {
+                    // TODO refactor to make_map_control function
+                    var button = L.DomUtil
+                        .create("div",
+                            "leaflet-bar leaflet-control leaflet-control-custom"
+                        );
+                    button.title = `Show the location of related entities.
+By default ('Same Entity') entities are shown that have
+a geographic location. The other options allow to show
+entities on the map using the location of a related
+entity.`;
+                    button.style.backgroundColor = "white";
+                    button.style.textAlign = "center";
+                    // Distance to zoom buttons:
+                    button.style.marginTop = "10px";
+                    // TODO implement helper for pictures
+                    let tmp_html = ('<div class="caosdb-f-map-loading" style="display:inherit">Loading Entities...</div><select><option value="same">Same Entity</option>');
+                    for (let pa in paths) {
+                        tmp_html += `<option value="${pa}">${pa}</option>`;
+                    }
+                    tmp_html += '</select>';
+                    button.innerHTML = tmp_html;
+                    const select = $(button).find('select');
+                    select.on("change", callback);
+
+                    const current_path = sessionStorage["caosdb_map.display_path"] || "same";
+                    sessionStorage["caosdb_map.display_path"] = current_path;
+                    select[0].value = current_path
+
+                    $(button).on("mousedown", (
+                        event) => {
+                        event
+                            .stopPropagation();
+                    });
+                    $(button).on("mouseup", (
+                        event) => {
+                        event
+                            .stopPropagation();
+                    });
+                    return button;
+                }(),
+            });
+            return new path_ddm();
+        }
 
         /**
          * Plug-in for leaflet which lets the user select an area in the map
diff --git a/src/core/js/ext_xls_download.js b/src/core/js/ext_xls_download.js
index fbedd0c6cd10c75c3d0b2914de1a1eef9cb7b1f0..d80bf4efa539f483811ca1d3b7f85b817489a572 100644
--- a/src/core/js/ext_xls_download.js
+++ b/src/core/js/ext_xls_download.js
@@ -71,7 +71,7 @@ var caosdb_table_export = new function () {
      * @return {string} cleaned up content
      */
     this._clean_cell = function(raw) {
-        return raw.replaceAll("\t"," ").replaceAll("\n"," ")
+        return raw.replaceAll("\t"," ").replaceAll("\n"," ").replaceAll("\r"," ").replaceAll("\x1E"," ").replaceAll("\x15"," ")
     }
 
     /**
@@ -83,7 +83,7 @@ var caosdb_table_export = new function () {
         const table = $('.caosdb-select-table');
         return table.find("th").toArray()
             .map(e => caosdb_table_export._clean_cell(e.textContent))
-            .filter(e => e.length > 0);
+            .filter(e => e.length > 0 && e.toLowerCase() != "id" && e.toLowerCase() != "version");
     }
 
     /**
@@ -113,6 +113,8 @@ var caosdb_table_export = new function () {
     /**
      * Convert all entities to an encoded tsv string with the given columns.
      *
+     * TODO merge with caosdb_utils.create_tsv_table.
+     *
      * @param {HTMLElement[]} entities - entities which are converted to rows
      *     of the tsv string.
      * @param {string[]} columns - array of property names.
@@ -122,7 +124,7 @@ var caosdb_table_export = new function () {
      */
     this._create_tsv_string = function (entities, columns, raw) {
         logger.trace("enter _create_tsv_string ", entities, columns);
-        var header = "ID\t" + columns.join("\t") + "\n"
+        var header = "ID\tVersion\t" + columns.join("\t") + "\n"
         var rows = [];
         for (const table_row of entities) {
             rows.push(caosdb_table_export._get_entity_row(table_row, columns, raw).join("\t"));
@@ -144,7 +146,7 @@ var caosdb_table_export = new function () {
      * @return {string[]}
      */
     this._get_entity_row = function (entity, columns, raw) {
-        var cells = [getEntityID(entity)];
+        var cells = [getEntityID(entity), getEntityVersion(entity)];
         var properties = getProperties(entity);
 
         for (const column of columns) {
diff --git a/src/core/js/footer.js b/src/core/js/footer.js
index 79f3ed0ecb382534d7c997ce0bc230178792f677..c48c5cf19c405aea326b36aba2868008466a8329 100644
--- a/src/core/js/footer.js
+++ b/src/core/js/footer.js
@@ -26,7 +26,7 @@
  * Call initially.
  * 
  * TODO refactor to async function for better readability.
- * @return
+ * @return something
  */
 function footer_initOnDocumentReady() {
 
diff --git a/src/core/js/form_elements.js b/src/core/js/form_elements.js
index 47c8e0ce0cf0df2599c6ea1f2afee941edfab4cc..d4cd4234a28953140fcc1f62104e2c43a3460cdb 100644
--- a/src/core/js/form_elements.js
+++ b/src/core/js/form_elements.js
@@ -25,8 +25,6 @@
  * form_elements module for reusable form elemenst which already have a basic
  * css styling.
  *
- * @version 0.2
- *
  * IMPORTANCE CONCEPTS
  *
  * FIELD - an HTMLElement which wraps a LABEL element (the fields name) and the
@@ -48,24 +46,53 @@
  * SUBFORM - an HTMLElement which contains FIELDS and other SUBFORMS. SUBFORMS
  * can be used to nest FIELDS, which is not supported by HTML5 but allows only
  * for flat key-value pairs.
- */
-
-/**
- * The configuration for double, integer, date input elements.
- *
- * @typedef {object} input_config
- * @property {string} name
- * @property {string} type
- * @property {string} label
- */
-
-/**
- * The configuration for reference_select input fields
- *
- * TODO
  *
+ * @version 0.2
+ * @exports form_elements
  */
 var form_elements = new function () {
+    /**
+     * Config for an alert
+     *
+     * @typedef {object} AlertConfig
+     * @property {string} [title] - an optional title for the alert.
+     * @property {string} [severity="danger"] - a bootstrap class suffix. Other
+     *     examples: warning, info
+     * @property {string} message - informs the user what they are about to do.
+     * @property {function} proceed_callback - the function which is called
+     *     then the user hits the "Proceed" button.
+     * @property {function} [cancel_callback] - a callback which is called then
+     *     the cancel button is clicked. By default, only the alert is being
+     *     closed an nothing happens.
+     * @property {string} [proceed_text="Proceed"] - the text on the proceed button.
+     * @property {string} [cancel_text="Cancel"] - the text on the cancel button.
+     * @property {string} [remember_my_decision_id] - if this parameter is
+     *     present, a checkbox is appended to the alert ("Don't ask me
+     *     again."). If the checkbox is checked the next time the make_alert
+     *     function is called with the same remember_my_decision_id is created,
+     *     the alert won't show up and the proceed_callback is called without
+     *     any user interaction.
+     * @property {string} [remember_my_decision_text="Don't ask me again."] -
+     *     label text for the checkbox.
+     * @property {HTMLElement} [proceed_button] - an optional custom proceed
+     *     button.
+     * @property {HTMLElement} [cancel_button] - an optional custom cancel
+     *     button.
+     */
+
+
+    /**
+     * The configuration for double, integer, date input elements.
+     *
+     * There are specializations of this configuration object. See
+     * {@link ReferenceDropDownConfig}
+     *
+     * @typedef {object} FieldConfig
+     * @property {string} name
+     * @property {string} type
+     * @property {string} label
+     * @see {@link ReferenceDropDownConfig}
+     */
 
     this.version = "0.1";
     this.dependencies = ["log", "caosdb_utils", "markdown"];
@@ -171,33 +198,6 @@ var form_elements = new function () {
         localStorage["form_elements.alert_decision." + key] = val;
     }
 
-    /**
-     * @type {AlertConfig}
-     * @property {string} [title] - an optional title for the alert.
-     * @property {string} [severity="danger"] - a bootstrap class suffix. Other
-     *     examples: warning, info
-     * @property {string} message - informs the user what they are about to do.
-     * @property {function} proceed_callback - the function which is called
-     *     then the user hits the "Proceed" button.
-     * @property {function} [cancel_callback] - a callback which is called then
-     *     the cancel button is clicked. By default, only the alert is being
-     *     closed an nothing happens.
-     * @property {string} [proceed_text="Proceed"] - the text on the proceed button.
-     * @property {string} [cancel_text="Cancel"] - the text on the cancel button.
-     * @property {string} [remember_my_decision_id] - if this parameter is
-     *     present, a checkbox is appended to the alert ("Don't ask me
-     *     again."). If the checkbox is checked the next time the make_alert
-     *     function is called with the same remember_my_decision_id is created,
-     *     the alert won't show up and the proceed_callback is called without
-     *     any user interaction.
-     * @property {string} [remember_my_decision_text="Don't ask me again."] -
-     *     label text for the checkbox.
-     * @property {HTMLElement} [proceed_button] - an optional custom proceed
-     *     button.
-     * @property {HTMLElement] [cancel_button] - an optional custom cancel
-     *     button.
-     */
-
     /**
      * Make an alert, that is a dialog which can intercept a function call and
      * asks the user to proceed or cancel.
@@ -270,37 +270,39 @@ var form_elements = new function () {
         return _alert[0];
     }
 
+
+    this.init = function () {
+        this.logger.trace("enter init");
+    }
+
     /**
-     * (Re-)set this module's functions to standard implementation.
+     * Return an OPTION element with entity reference.
+     *
+     * The OPTION element for a SELECT form input shows a short
+     * summary/description of an entity and has the entity's id as value.
+     *
+     * If the `desc` parameter is undefined, the entity_id is shown
+     * instead.
+     *
+     * @param {string} entity_id - the entity's id.
+     * @param {string} [desc] - the description for the entity.
+     * @returns {HTMLElement} OPTION element.
      */
-    this._init_functions = function () {
-
-        this.init = function () {
-            this.logger.trace("enter init");
+    this.make_reference_option = function (entity_id, desc) {
+        caosdb_utils.assert_string(entity_id, "param `entity_id`");
+        if (typeof desc == "undefined") {
+            desc = entity_id;
         }
+        var opt_str = '<option value="' + entity_id + '">' + desc +
+            "</option>";
+        return $(opt_str)[0];
+    }
 
-        /**
-         * Return an OPTION element with entity reference.
-         *
-         * The OPTION element for a SELECT form input shows a short
-         * summary/description of an entity and has the entity's id as value.
-         *
-         * If the `desc` parameter is undefined, the entity_id is shown
-         * instead.
-         *
-         * @param {string} entity_id - the entity's id.
-         * @param {string} [desc] - the description for the entity.
-         * @returns {HTMLElement} OPTION element.
-         */
-        this.make_reference_option = function (entity_id, desc) {
-            caosdb_utils.assert_string(entity_id, "param `entity_id`");
-            if (typeof desc == "undefined") {
-                desc = entity_id;
-            }
-            var opt_str = '<option value="' + entity_id + '">' + desc +
-                "</option>";
-            return $(opt_str)[0];
-        }
+
+    /**
+     * (Re-)set this module's functions to standard implementation.
+     */
+    this._init_functions = function () {
 
         /**
          * Return SELECT form element with entity references.
@@ -350,8 +352,6 @@ var form_elements = new function () {
         }
 
         /**
-         * @typedef {option} ReferenceDropDownConfig
-         *
          * Configuration object for a drop down menu for selecting references.
          * `make_reference_drop_down` generates such a drop down menu using a
          * SELECT input with the references as its OPTION elements.
@@ -369,6 +369,10 @@ var form_elements = new function () {
          * defined by `label`. If the `label` property is undefined, the `name`
          * is shown instead.
          *
+         * The ReferenceDropDownConfig is a specialisation of a
+         * {@link FieldConfig}.
+         *
+         * @typedef {option} ReferenceDropDownConfig
          * @property {string} name - The name of the select input.
          * @property {string} query - Query for entities.
          * @property {function} [make_value] - Call-back for the generation of
@@ -383,129 +387,9 @@ var form_elements = new function () {
          *     undefined. This property is used by `make_form_field` to decide
          *     which type of field is to be generated.
          *
+         * @see {@link FieldConfig}
          */
 
-        /**
-         * Search and retrieve entities and create a SELECT from element.
-         *
-         * @param {ReferenceDropDownConfig} config - all necessary parameters
-         *     for the configuration.
-         * @returns {HTMLElement} SELECT element.
-         */
-        this.make_reference_drop_down = function (config) {
-            let ret = $(this._make_field_wrapper(config.name));
-            let label = this._make_input_label_str(config);
-            let loading = $('<div class="caosdb-f-field-not-ready">loading...</div>');
-            let input_col = $('<div class="col-sm-9"/>');
-
-            input_col.append(loading);
-            this._query(config.query).then(async function (entities) {
-                let select = $(await form_elements.make_reference_select(
-                    entities, config.make_desc, config.make_value, config.multiple,
-                    config.value));
-                select.attr("name", config.name);
-                loading.remove();
-                input_col.append(select);
-                form_elements.init_select_picker(ret[0], config.value);
-                ret[0].dispatchEvent(form_elements.field_ready_event);
-                select.change(function () {
-                    ret[0].dispatchEvent(form_elements.field_changed_event);
-                });
-            }).catch(err => {
-                form_elements.logger.error(err);
-                loading.remove();
-                input_col.append(err);
-                ret[0].dispatchEvent(form_elements.field_error_event);
-            });
-
-            return ret.append(label, input_col)[0];
-        }
-
-
-        this.init_select_picker = function (field, value) {
-            caosdb_utils.assert_html_element(field, "parameter `field`");
-            const select = $(field).find("select")[0];
-            const select_picker_options = {};
-            if ($(select).prop("multiple")) {
-                select_picker_options["actionsBox"] = true;
-            }
-            if ($(select).find("option").length > 8) {
-                select_picker_options["liveSearch"] = true;
-                select_picker_options["liveSearchNormalize"] = true;
-                select_picker_options["liveSearchPlaceholder"] = "search...";
-            }
-            $(select).selectpicker(select_picker_options);
-            $(select).selectpicker("val", value);
-            this.init_actions_box(field);
-        }
-
-
-        this.init_actions_box = function (field) {
-            this.logger.trace("enter init_actions_box", field);
-            caosdb_utils.assert_html_element(field, "parameter `field`");
-            const select = $(field).find("select");
-            var actions_box = select.siblings().find(".bs-actionsbox");
-            if (actions_box.length === 0) {
-                actions_box = $(`<div class="bs-actionsbox">
-                            <div class="btn-group btn-group-sm btn-block">
-                                <button type="button" class="actions-btn btn-default bs-deselect-all btn btn-light">None</button>
-                            </div>
-                        </div>`)
-                    .hide();
-
-                select
-                    .siblings(".dropdown-menu")
-                    .prepend(actions_box);
-
-                field.addEventListener(
-                    form_elements.field_changed_event.type,
-                    (e) => {
-                        if (form_elements.is_set(field)) {
-                            actions_box.show();
-                        } else {
-                            actions_box.hide();
-                        }
-                    }, true);
-
-                actions_box
-                    .find(".bs-deselect-all")
-                    .click((e) => {
-                        select.val(null)
-                            .selectpicker("render")
-                            .parent().toggleClass("open", false);
-                        select[0].dispatchEvent(form_elements.field_changed_event);
-                    });
-            }
-        }
-
-        /**
-         * Return a promise which resolves with the field when the field is ready.
-         *
-         * This function is especially useful if the caller can not be sure if
-         * the field_ready_event has been dispatched already and the field is
-         * ready or if the fields creation is still pending.
-         *
-         * @param {HTMLElement} field
-         * @return {Promise} the field-ready promise
-         */
-        this.field_ready = function (field) {
-            // TODO add support for field name (string) as field parameter
-            // TODO check type of param field (not an array!)
-            caosdb_utils.assert_html_element(field, "parameter `field`");
-            return new Promise(function (resolve, reject) {
-                try {
-                    if (!$(field).hasClass("caosdb-f-field-not-ready") && $(field).find(".caosdb-f-field-not-ready").length === 0) {
-                        resolve(field);
-                    } else {
-                        field.addEventListener(form_elements.field_ready_event.type,
-                            (e) => resolve(e.target), true);
-                    }
-                } catch (err) {
-                    reject(err);
-                }
-            });
-        }
-
         this._query = async function (q) {
             const result = await query(q);
             this.logger.debug("query returned", result);
@@ -527,802 +411,978 @@ var form_elements = new function () {
             return this.parse_script_result(result);
         }
 
-        this.parse_script_result = function (result) {
-            console.log(result);
-            const scriptNode = result.evaluate("/Response/script", result, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
-            const code = result.evaluate("@code", scriptNode, null, XPathResult.STRING_TYPE, null).stringValue;
+    }
 
-            const call = result.evaluate("call", scriptNode, null, XPathResult.STRING_TYPE, null).stringValue;
-            const stderr = result.evaluate("stderr", scriptNode, null, XPathResult.STRING_TYPE, null).stringValue;
-            const stdout = result.evaluate("stdout", scriptNode, null, XPathResult.STRING_TYPE, null).stringValue;
+    /**
+     * @typedef {object} ScriptingResult
+     * @property {string} code
+     * @property {string} call
+     * @property {string} stdout
+     * @property {string} stderr
+     */
 
-            return {
-                "code": code,
-                "call": call,
-                "stdout": stdout,
-                "stderr": stderr
-            };
+    /**
+     * Bla, TODO
+     *
+     * @param {XMLDocument} result
+     * @return {ScriptingResult}
+     */
+    this.parse_script_result = function (result) {
+        console.log(result);
+        const scriptNode = result.evaluate("/Response/script", result, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
+        const code = result.evaluate("@code", scriptNode, null, XPathResult.STRING_TYPE, null).stringValue;
+
+        const call = result.evaluate("call", scriptNode, null, XPathResult.STRING_TYPE, null).stringValue;
+        const stderr = result.evaluate("stderr", scriptNode, null, XPathResult.STRING_TYPE, null).stringValue;
+        const stdout = result.evaluate("stdout", scriptNode, null, XPathResult.STRING_TYPE, null).stringValue;
+
+        const ret = {
+            "code": code,
+            "call": call,
+            "stdout": stdout,
+            "stderr": stderr
+        };
+
+        return ret;
+    }
+
+    /**
+     * Search and retrieve entities and create a SELECT from element.
+     *
+     * @param {ReferenceDropDownConfig} config - all necessary parameters
+     *     for the configuration.
+     * @returns {HTMLElement} SELECT element.
+     */
+    this.make_reference_drop_down = function (config) {
+        let ret = $(this._make_field_wrapper(config.name));
+        let label = this._make_input_label_str(config);
+        let loading = $('<div class="caosdb-f-field-not-ready">loading...</div>');
+        let input_col = $('<div class="col-sm-9"/>');
+
+        input_col.append(loading);
+        this._query(config.query).then(async function (entities) {
+            let select = $(await form_elements.make_reference_select(
+                entities, config.make_desc, config.make_value, config.multiple,
+                config.value));
+            select.attr("name", config.name);
+            loading.remove();
+            input_col.append(select);
+            form_elements.init_select_picker(ret[0], config.value);
+            ret[0].dispatchEvent(form_elements.field_ready_event);
+            select.change(function () {
+                ret[0].dispatchEvent(form_elements.field_changed_event);
+            });
+        }).catch(err => {
+            form_elements.logger.error(err);
+            loading.remove();
+            input_col.append(err);
+            ret[0].dispatchEvent(form_elements.field_error_event);
+        });
+
+        return ret.append(label, input_col)[0];
+    }
+
+
+    /**
+     * Test 16
+     */
+    this.init_select_picker = function (field, value) {
+        caosdb_utils.assert_html_element(field, "parameter `field`");
+        const select = $(field).find("select")[0];
+        const select_picker_options = {};
+        if ($(select).prop("multiple")) {
+            select_picker_options["actionsBox"] = true;
         }
+        if ($(select).find("option").length > 8) {
+            select_picker_options["liveSearch"] = true;
+            select_picker_options["liveSearchNormalize"] = true;
+            select_picker_options["liveSearchPlaceholder"] = "search...";
+        }
+        $(select).selectpicker(select_picker_options);
+        $(select).selectpicker("val", value);
+        this.init_actions_box(field);
+    }
 
-        /**
-         * generate a java script object representation of a form
-         */
-        this.form_to_object = function (form) {
-            this.logger.trace("entity form_to_json", form);
-            caosdb_utils.assert_html_element(form, "parameter `form`");
 
-            const _to_json = (element, data) => {
-                this.logger.trace("enter element_to_json", element, data);
+    /**
+     * Test 17
+     */
+    this.init_actions_box = function (field) {
+        this.logger.trace("enter init_actions_box", field);
+        caosdb_utils.assert_html_element(field, "parameter `field`");
+        const select = $(field).find("select");
+        var actions_box = select.siblings().find(".bs-actionsbox");
+        if (actions_box.length === 0) {
+            actions_box = $(`<div class="bs-actionsbox">
+                        <div class="btn-group btn-group-sm btn-block">
+                            <button type="button" class="actions-btn btn-default bs-deselect-all btn btn-light">None</button>
+                        </div>
+                    </div>`)
+                .hide();
+
+            select
+                .siblings(".dropdown-menu")
+                .prepend(actions_box);
+
+            field.addEventListener(
+                form_elements.field_changed_event.type,
+                (e) => {
+                    if (form_elements.is_set(field)) {
+                        actions_box.show();
+                    } else {
+                        actions_box.hide();
+                    }
+                }, true);
 
-                for (const child of element.children) {
-                    // ignore disabled fields and subforms
-                    if ($(child).hasClass("caosdb-f-field-disabled")) {
-                        continue;
+            actions_box
+                .find(".bs-deselect-all")
+                .click((e) => {
+                    select.val(null)
+                        .selectpicker("render")
+                        .parent().toggleClass("open", false);
+                    select[0].dispatchEvent(form_elements.field_changed_event);
+                });
+        }
+    }
+
+    /**
+     * Return a promise which resolves with the field when the field is ready.
+     *
+     * This function is especially useful if the caller can not be sure if
+     * the field_ready_event has been dispatched already and the field is
+     * ready or if the fields creation is still pending.
+     *
+     * @param {HTMLElement} field
+     * @return {Promise} the field-ready promise
+     */
+    this.field_ready = function (field) {
+        // TODO add support for field name (string) as field parameter
+        // TODO check type of param field (not an array!)
+        caosdb_utils.assert_html_element(field, "parameter `field`");
+        return new Promise(function (resolve, reject) {
+            try {
+                if (!$(field).hasClass("caosdb-f-field-not-ready") && $(field).find(".caosdb-f-field-not-ready").length === 0) {
+                    resolve(field);
+                } else {
+                    field.addEventListener(form_elements.field_ready_event.type,
+                        (e) => resolve(e.target), true);
+                }
+            } catch (err) {
+                reject(err);
+            }
+        });
+    }
+
+    /**
+     * generate a java script object representation of a form
+     *
+     * @function
+     */
+    this.form_to_object = function (form) {
+        this.logger.trace("entity form_to_json", form);
+        caosdb_utils.assert_html_element(form, "parameter `form`");
+
+        const _to_json = (element, data) => {
+            this.logger.trace("enter element_to_json", element, data);
+
+            for (const child of element.children) {
+                // ignore disabled fields and subforms
+                if ($(child).hasClass("caosdb-f-field-disabled")) {
+                    continue;
+                }
+                const name = $(child).attr("name");
+                const is_subform = $(child).hasClass("caosdb-f-form-elements-subform");
+                if (is_subform) {
+                    const subform = $(child).data("subform-name");
+                    // recursive
+                    var subform_obj = _to_json(child, {});
+                    if (typeof data[subform] === "undefined") {
+                        data[subform] = subform_obj;
+                    } else if (Array.isArray(data[subform])) {
+                        data[subform].push(subform_obj);
+                    } else {
+                        data[subform] = [data[subform], subform_obj]
                     }
-                    const name = $(child).attr("name");
-                    const is_subform = $(child).hasClass("caosdb-f-form-elements-subform");
-                    if (is_subform) {
-                        const subform = $(child).data("subform-name");
-                        // recursive
-                        var subform_obj = _to_json(child, {});
-                        if (typeof data[subform] === "undefined") {
-                            data[subform] = subform_obj;
-                        } else if (Array.isArray(data[subform])) {
-                            data[subform].push(subform_obj);
-                        } else {
-                            data[subform] = [data[subform], subform_obj]
-                        }
-                    } else if (name && name !== "") {
-                        // input elements
-                        const not_checkbox = !$(child).is(":checkbox");
-                        if (not_checkbox || $(child).is(":checked")) {
-                            // checked or not a checkbox
-                            var value = $(child).val();
-                            if (typeof data[name] === "undefined") {
-                                data[name] = value;
-                            } else if (Array.isArray(data[name])) {
-                                data[name].push(value);
-                            } else {
-                                data[name] = [data[name], value]
-                            }
+                } else if (name && name !== "") {
+                    // input elements
+                    const not_checkbox = !$(child).is(":checkbox");
+                    if (not_checkbox || $(child).is(":checked")) {
+                        // checked or not a checkbox
+                        var value = $(child).val();
+                        if (typeof data[name] === "undefined") {
+                            data[name] = value;
+                        } else if (Array.isArray(data[name])) {
+                            data[name].push(value);
                         } else {
-                            // TODO checkbox
+                            data[name] = [data[name], value]
                         }
-                    } else if (child.children.length > 0) {
-                        // recursive
-                        _to_json(child, data);
+                    } else {
+                        // TODO checkbox
                     }
+                } else if (child.children.length > 0) {
+                    // recursive
+                    _to_json(child, data);
                 }
+            }
 
-                this.logger.trace("leave element_to_json", element, data);
-                return data;
-            };
-
-            const ret = _to_json(form, {});
-            this.logger.trace("leave form_to_json", ret);
-            return ret;
-        }
+            this.logger.trace("leave element_to_json", element, data);
+            return data;
+        };
 
-        this.make_submit_button = function () {
-            var ret = $('<button class="caosdb-f-form-elements-submit-button btn btn-primary" type="submit">Submit</button>');
-            return ret[0];
-        }
+        const ret = _to_json(form, {});
+        this.logger.trace("leave form_to_json", ret);
+        return ret;
+    }
 
-        this.make_cancel_button = function (form) {
-            var ret = $('<button class="caosdb-f-form-elements-cancel-button btn btn-primary" type="button">Cancel</button>');
-            ret.on("click", e => {
-                this.logger.debug("cancel form", e, form);
-                form.dispatchEvent(this.cancel_form_event);
-            });
-            return ret[0];
-        }
+    this.make_submit_button = function () {
+        var ret = $('<button class="caosdb-f-form-elements-submit-button btn btn-primary" type="submit">Submit</button>');
+        return ret[0];
+    }
 
-        /**
-         * TODO make syncronous
-         */
-        this.make_form_field = async function (config) {
-            caosdb_utils.assert_type(config, "object", "param `config`");
-            caosdb_utils.assert_string(config.type, "`config.type` of param `config`");
-
-            var field = undefined;
-            const type = config.type;
-            if (type === "date") {
-                field = this.make_date_input(config);
-            } else if (type === "checkbox") {
-                field = this.make_checkbox_input(config);
-            } else if (type === "text") {
-                field = this.make_text_input(config);
-            } else if (type === "double") {
-                field = this.make_double_input(config);
-            } else if (type === "integer") {
-                field = this.make_integer_input(config);
-            } else if (type === "range") {
-                field = await this.make_range_input(config);
-            } else if (type === "reference_drop_down") {
-                field = this.make_reference_drop_down(config);
-            } else if (type === "subform") {
-                // TODO handle cache and required for subforms
-                return await this.make_subform(config);
-            } else {
-                throw new TypeError("undefined field type `" + type + "`");
-            }
+    this.make_cancel_button = function (form) {
+        var ret = $('<button class="caosdb-f-form-elements-cancel-button btn btn-primary" type="button">Cancel</button>');
+        ret.on("click", e => {
+            this.logger.debug("cancel form", e, form);
+            form.dispatchEvent(this.cancel_form_event);
+        });
+        return ret[0];
+    }
 
-            if (config.required) {
-                this.set_required(field);
-            }
-            if (config.cached) {
-                this.set_cached(field);
-            }
-            if (config.help) {
-                this.add_help(field, config.help);
-            }
+    /**
+     * TODO make syncronous
+     *
+     * @return {HTMLElement}
+     */
+    this.make_form_field = async function (config) {
+        caosdb_utils.assert_type(config, "object", "param `config`");
+        caosdb_utils.assert_string(config.type, "`config.type` of param `config`");
+
+        var field = undefined;
+        const type = config.type;
+        if (type === "date") {
+            field = this.make_date_input(config);
+        } else if (type === "checkbox") {
+            field = this.make_checkbox_input(config);
+        } else if (type === "text") {
+            field = this.make_text_input(config);
+        } else if (type === "double") {
+            field = this.make_double_input(config);
+        } else if (type === "integer") {
+            field = this.make_integer_input(config);
+        } else if (type === "range") {
+            field = await this.make_range_input(config);
+        } else if (type === "reference_drop_down") {
+            field = this.make_reference_drop_down(config);
+        } else if (type === "subform") {
+            // TODO handle cache and required for subforms
+            return await this.make_subform(config);
+        } else {
+            throw new TypeError("undefined field type `" + type + "`");
+        }
 
-            return field;
+        if (config.required) {
+            this.set_required(field);
+        }
+        if (config.cached) {
+            this.set_cached(field);
+        }
+        if (config.help) {
+            this.add_help(field, config.help);
         }
 
+        return field;
+    }
 
-        this.add_help = function (field, config) {
-            var help_button = $('<span data-trigger="click focus" data-toggle="popover" class="caosdb-f-form-help pull-right glyphicon glyphicon-info-sign"/>')
-                .css({
-                    "cursor": "pointer"
-                });
 
-            if (typeof config === "string" || config instanceof String) {
-                help_button.attr("data-content", config);
-                help_button.popover();
-            } else {
-                help_button.popover(config);
-            }
+    this.add_help = function (field, config) {
+        var help_button = $('<span data-trigger="click focus" data-toggle="popover" class="caosdb-f-form-help pull-right glyphicon glyphicon-info-sign"/>')
+            .css({
+                "cursor": "pointer"
+            });
 
+        if (typeof config === "string" || config instanceof String) {
+            help_button.attr("data-content", config);
+            help_button.popover();
+        } else {
+            help_button.popover(config);
+        }
 
-            var label = $(field).children("label");
-            if (label.length > 0) {
-                help_button.css({
-                    "margin-left": "4px"
-                });
-                label.first().append(help_button);
-            } else {
-                $(field).append(help_button);
-            }
+
+        var label = $(field).children("label");
+        if (label.length > 0) {
+            help_button.css({
+                "margin-left": "4px"
+            });
+            label.first().append(help_button);
+        } else {
+            $(field).append(help_button);
         }
+    }
 
-        this.make_heading = function (config) {
-            if (typeof config.header === "undefined") {
-                return;
-            } else if (typeof config.header === "string" || config.header instanceof String) {
-                return $('<div class="caosdb-f-form-header h3">' + config.header + '</div>')[0];
-            }
-            caosdb_utils.assert_html_element(config.header, "member `header` of parameter `config`");
-            return config.header;
+    this.make_heading = function (config) {
+        if (typeof config.header === "undefined") {
+            return;
+        } else if (typeof config.header === "string" || config.header instanceof String) {
+            return $('<div class="caosdb-f-form-header h3">' + config.header + '</div>')[0];
         }
+        caosdb_utils.assert_html_element(config.header, "member `header` of parameter `config`");
+        return config.header;
+    }
 
-        this.make_form_wrapper = function (form, config) {
-            var wrapper = $('<div class="caosdb-f-form-wrapper"/>');
+    this.make_form_wrapper = function (form, config) {
+        var wrapper = $('<div class="caosdb-f-form-wrapper"/>');
 
-            var header = this.make_heading(config);
-            wrapper.append(header);
+        var header = this.make_heading(config);
+        wrapper.append(header);
 
-            var loading = $('<div>loading...</div>');
-            var logger = this.logger;
-            var cancel = (e) => {
-                logger.trace("cancel form", e);
-                wrapper.remove();
-            };
+        var loading = $('<div>loading...</div>');
+        var logger = this.logger;
+        var cancel = (e) => {
+            logger.trace("cancel form", e);
+            wrapper.remove();
+        };
 
-            wrapper.append(loading);
+        wrapper.append(loading);
 
-            Promise.resolve(form).then(form => {
-                // form ready
-                loading.remove();
-                wrapper.append(form);
-                wrapper[0].dispatchEvent(this.form_ready_event);
+        Promise.resolve(form).then(form => {
+            // form ready
+            loading.remove();
+            wrapper.append(form);
+            wrapper[0].dispatchEvent(this.form_ready_event);
 
-            }).catch(err => {
-                logger.error("form loading error", err);
-                loading.remove();
-                wrapper.append(err);
-            });
+        }).catch(err => {
+            logger.error("form loading error", err);
+            loading.remove();
+            wrapper.append(err);
+        });
 
-            wrapper[0].addEventListener(this.cancel_form_event.type, cancel, true);
+        wrapper[0].addEventListener(this.cancel_form_event.type, cancel, true);
 
-            return wrapper[0];
-        }
+        return wrapper[0];
+    }
 
-        this.make_form = function (config) {
-            var form = undefined;
+    /**
+     * Configuration objects which are passed to {@link make_form}.
+     *
+     * Note: either the `script` or the `name` property must be defined. If the former is defined, the latter will be overriden.
+     *
+     * @typedef {object} FormConfig
+     *
+     * @property {FieldConfig[]} fields - array of fields. The order is the
+     *     order in which they appear in the resulting form.
+     * @property {string} [script] - if present the form will call a
+     *     server-side script on submission.
+     * @property {string} [name] - The name of the form. This is being
+     *     overridden by the `script` parameter if present.
+     * @property {function} [submit] - a callback which handles the submission
+     *     of the form. This parameter is being overridden if the `script`
+     *     parameter is present.
+     */
 
-            if (config.script) {
-                form = this.make_script_form(config, config.script);
-            } else {
-                form = this.make_generic_form(config);
-            }
-            var wrapper = this.make_form_wrapper(form, config);
-            return wrapper;
-        }
+    /**
+     * Create a form.
+     *
+     * The returned element is a container which will eventually contain a HTML
+     * form element. The container emits a {@link form_ready_event} when the
+     * form is ready.
+     *
+     * @param {FormConfig} config
+     * @return {HTMLElement}
+     */
+    this.make_form = function (config) {
+        var form = undefined;
 
-        /**
-         * TODO make syncronous
-         */
-        this.make_subform = async function (config) {
-            this.logger.trace("enter make_subform");
-            caosdb_utils.assert_type(config, "object", "param `config`");
-            caosdb_utils.assert_string(config.name, "`config.name` of param `config`");
-            caosdb_utils.assert_array(config.fields, "`config.fields` of param `config`");
+        if (config.script) {
+            form = this.make_script_form(config, config.script);
+        } else {
+            form = this.make_generic_form(config);
+        }
+        var wrapper = this.make_form_wrapper(form, config);
+        return wrapper;
+    }
 
-            const name = config.name;
-            var form = $('<div data-subform-name="' + name + '" class="caosdb-f-form-elements-subform"/>');
+    /**
+     * TODO make syncronous
+     */
+    this.make_subform = async function (config) {
+        this.logger.trace("enter make_subform");
+        caosdb_utils.assert_type(config, "object", "param `config`");
+        caosdb_utils.assert_string(config.name, "`config.name` of param `config`");
+        caosdb_utils.assert_array(config.fields, "`config.fields` of param `config`");
+
+        const name = config.name;
+        var form = $('<div data-subform-name="' + name + '" class="caosdb-f-form-elements-subform"/>');
+
+        for (let field of config.fields) {
+            this.logger.trace("add subform field", field);
+            let elem = await this.make_form_field(field);
+            form.append(elem);
+        }
 
-            for (let field of config.fields) {
-                this.logger.trace("add subform field", field);
-                let elem = await this.make_form_field(field);
-                form.append(elem);
-            }
+        this.logger.trace("leave make_subform", form[0]);
+        return form[0];
+    }
 
-            this.logger.trace("leave make_subform", form[0]);
-            return form[0];
+    this.dismiss_form = function (form) {
+        if (form.tagName === "FORM") {
+            form.dispatchEvent(this.cancel_form_event);
         }
-
-        this.dismiss_form = function (form) {
-            if (form.tagName === "FORM") {
-                form.dispatchEvent(this.cancel_form_event);
-            }
-            var _form = $(form).find("form");
-            if (_form.length > 0) {
-                _form[0].dispatchEvent(this.cancel_form_event);
-            }
+        var _form = $(form).find("form");
+        if (_form.length > 0) {
+            _form[0].dispatchEvent(this.cancel_form_event);
         }
+    }
 
-        this.enable_group = function (form, group) {
-            this.enable_fields(this.get_group_fields(form, group));
-        }
+    this.enable_group = function (form, group) {
+        this.enable_fields(this.get_group_fields(form, group));
+    }
 
-        this.disable_group = function (form, group) {
-            this.disable_fields(this.get_group_fields(form, group));
-        }
+    this.disable_group = function (form, group) {
+        this.disable_fields(this.get_group_fields(form, group));
+    }
 
-        this.get_group_fields = function (form, group) {
-            return $(form).find(".caosdb-f-field[data-groups*='(" + group + ")']").toArray();
-        }
+    this.get_group_fields = function (form, group) {
+        return $(form).find(".caosdb-f-field[data-groups*='(" + group + ")']").toArray();
+    }
 
-        /**
-         * Return an array of field with name
-         *
-         * @param {string} name - the field name
-         * @return {HTMLElement[]} array of fields
-         */
-        this.get_fields = function (form, name) {
-            caosdb_utils.assert_html_element(form, "parameter `form`");
-            caosdb_utils.assert_string(name, "parameter `name`");
-            return $(form).find(".caosdb-f-field[data-field-name='" + name + "']").toArray();
-        }
+    /**
+     * Return an array of field with name
+     *
+     * @param {string} name - the field name
+     * @return {HTMLElement[]} array of fields
+     */
+    this.get_fields = function (form, name) {
+        caosdb_utils.assert_html_element(form, "parameter `form`");
+        caosdb_utils.assert_string(name, "parameter `name`");
+        return $(form).find(".caosdb-f-field[data-field-name='" + name + "']").toArray();
+    }
 
-        this.add_field_to_group = function (field, group) {
-            this.logger.trace("enter add_field_to_group", field, group);
-            var groups = ($(field).attr("data-groups") ? $(field).attr("data-groups") : "") + "(" + group + ")";
-            $(field).attr("data-groups", groups);
-        }
+    this.add_field_to_group = function (field, group) {
+        this.logger.trace("enter add_field_to_group", field, group);
+        var groups = ($(field).attr("data-groups") ? $(field).attr("data-groups") : "") + "(" + group + ")";
+        $(field).attr("data-groups", groups);
+    }
 
-        this.disable_fields = function (fields) {
-            $(fields).toggleClass("caosdb-f-field-disabled", true).hide();
-            for (const field of $(fields)) {
-                field.dispatchEvent(this.field_disabled_event);
-            }
+    this.disable_fields = function (fields) {
+        $(fields).toggleClass("caosdb-f-field-disabled", true).hide();
+        for (const field of $(fields)) {
+            field.dispatchEvent(this.field_disabled_event);
         }
+    }
 
-        this.enable_fields = function (fields) {
-            $(fields).toggleClass("caosdb-f-field-disabled", false).show();
-            for (const field of $(fields)) {
-                field.dispatchEvent(this.field_enabled_event);
-            }
+    this.enable_fields = function (fields) {
+        $(fields).toggleClass("caosdb-f-field-disabled", false).show();
+        for (const field of $(fields)) {
+            field.dispatchEvent(this.field_enabled_event);
         }
+    }
 
-        this.enable_name = function (form, name) {
-            this.enable_fields($(form).find(".caosdb-f-field[data-field-name='" + name + "']").toArray());
-        }
+    this.enable_name = function (form, name) {
+        this.enable_fields($(form).find(".caosdb-f-field[data-field-name='" + name + "']").toArray());
+    }
 
-        this.disable_name = function (form, name) {
-            this.disable_fields($(form).find(".caosdb-f-field[data-field-name='" + name + "']").toArray());
-        }
+    this.disable_name = function (form, name) {
+        this.disable_fields($(form).find(".caosdb-f-field[data-field-name='" + name + "']").toArray());
+    }
 
-        this.make_script_form = async function (config, script) {
-            this.logger.trace("enter make_script_form");
+    this.make_script_form = async function (config, script) {
+        this.logger.trace("enter make_script_form");
 
-            const submit_callback = async function (form) {
-                form = $(form);
+        const submit_callback = async function (form) {
+            form = $(form);
 
 
-                // actually submit the form
-                var response = await form_elements._run_script(script, form);
-                var result = [];
+            // actually submit the form
+            var response = await form_elements._run_script(script, form);
+            var result = [];
 
-                if (response.code === "0") {
-                    // handle success
-                    result.push(form_elements.make_success_message(response.stdout));
-                    return result;
+            if (response.code === "0") {
+                // handle success
+                result.push(form_elements.make_success_message(response.stdout));
+                return result;
 
-                } else {
-                    // handle scripting error
-                    result.push(form_elements.make_error_message(response.call));
-                    result.push(form_elements.make_error_message(response.stderr));
-                    throw result;
-                }
-            };
+            } else {
+                // handle scripting error
+                result.push(form_elements.make_error_message(response.call));
+                result.push(form_elements.make_error_message(response.stderr));
+                throw result;
+            }
+        };
+
+        this.logger.trace("leave make_script_form");
+        const new_config = $.extend({}, {
+            name: script,
+            submit: submit_callback
+        }, config);
+        return await this.make_generic_form(new_config);
+    }
 
-            this.logger.trace("leave make_script_form");
-            const new_config = $.extend({}, {
-                name: script,
-                submit: submit_callback
-            }, config);
-            return await this.make_generic_form(new_config);
-        }
+    /**
+     * Return a generic form, bind the config.submit to the submit event
+     * of the form.
+     *
+     * The `config.fields` array may contain `form_elements.field_config`
+     * objects or HTMLElements.
+     *
+     * TODO
+     */
+    this.make_generic_form = async function (config) {
+        this.logger.trace("enter make_generic_form");
 
-        /**
-         * Return a generic form, bind the config.submit to the submit event
-         * of the form.
-         *
-         * The `config.fields` array may contain `form_elements.field_config`
-         * objects or HTMLElements.
-         *
-         * TODO
-         */
-        this.make_generic_form = async function (config) {
-            this.logger.trace("enter make_generic_form");
+        caosdb_utils.assert_type(config, "object", "param `config`");
+        caosdb_utils.assert_string(config.name, "`config.name` of param `config`", true);
+        caosdb_utils.assert_array(config.fields, "`config.fields` of param `config`");
 
-            caosdb_utils.assert_type(config, "object", "param `config`");
-            caosdb_utils.assert_string(config.name, "`config.name` of param `config`", true);
-            caosdb_utils.assert_array(config.fields, "`config.fields` of param `config`");
+        const form = $('<form class="form-horizontal" action="#" method="post" />');
 
-            const form = $('<form class="form-horizontal" action="#" method="post" />');
+        // set name
+        if (config.name) {
+            form.attr("name", config.name);
+        }
 
-            // set name
-            if (config.name) {
-                form.attr("name", config.name);
+        // add fields
+        for (let field of config.fields) {
+            this.logger.trace("add field", field);
+            if (field instanceof HTMLElement) {
+                form.append(field);
+            } else {
+                let elem = await this.make_form_field(field);
+                form.append(elem);
             }
+        }
+
+        // set groups
+        if (config.groups) {
+            for (let group of config.groups) {
+                this.logger.trace("add group", group);
+                for (let fieldname of group.fields) {
+                    let field = form.find(".caosdb-f-field[data-field-name='" + fieldname + "']");
+                    this.logger.trace("set group", field, group);
+                    this.add_field_to_group(field, group.name)
 
-            // add fields
-            for (let field of config.fields) {
-                this.logger.trace("add field", field);
-                if (field instanceof HTMLElement) {
-                    form.append(field);
+                }
+                // disable if necessary
+                if (typeof group.enabled === "undefined" || group.enabled) {
+                    this.enable_group(form, group.name);
                 } else {
-                    let elem = await this.make_form_field(field);
-                    form.append(elem);
+                    this.disable_group(form, group.name);
                 }
             }
+        }
 
-            // set groups
-            if (config.groups) {
-                for (let group of config.groups) {
-                    this.logger.trace("add group", group);
-                    for (let fieldname of group.fields) {
-                        let field = form.find(".caosdb-f-field[data-field-name='" + fieldname + "']");
-                        this.logger.trace("set group", field, group);
-                        this.add_field_to_group(field, group.name)
+        const footer = this.make_footer();
+        form.append(footer);
 
-                    }
-                    // disable if necessary
-                    if (typeof group.enabled === "undefined" || group.enabled) {
-                        this.enable_group(form, group.name);
-                    } else {
-                        this.disable_group(form, group.name);
-                    }
-                }
+        if (!(typeof config.submit === 'boolean' && config.submit === false)) {
+            // add submit button unless config.submit is false
+            footer.append(this.make_submit_button());
+        }
+        form[0].addEventListener("submit", (e) => {
+            e.preventDefault();
+            e.stopPropagation();
+            if (form.find(".caosdb-f-form-submitting").length > 0) {
+                // do not submit twice
+                return;
             }
 
-            const footer = this.make_footer();
-            form.append(footer);
-
-            if (!(typeof config.submit === 'boolean' && config.submit === false)) {
-                // add submit button unless config.submit is false
-                footer.append(this.make_submit_button());
-            }
-            form[0].addEventListener("submit", (e) => {
-                e.preventDefault();
-                e.stopPropagation();
-                if (form.find(".caosdb-f-form-submitting").length > 0) {
-                    // do not submit twice
-                    return;
-                }
+            this.logger.debug("submit form", e);
 
-                this.logger.debug("submit form", e);
+            form[0].dispatchEvent(this.submit_form_event);
 
-                form[0].dispatchEvent(this.submit_form_event);
+            form.find(":input").prop("disabled", true);
+            var submitting = form_elements.make_submitting_info();
+            form.find(".caosdb-f-form-elements-footer").before(submitting);
 
-                form.find(":input").prop("disabled", true);
-                var submitting = form_elements.make_submitting_info();
-                form.find(".caosdb-f-form-elements-footer").before(submitting);
 
+            form[0].addEventListener(this.form_success_event.type, (e) => {
+                submitting.remove();
+            }, true);
+            form[0].addEventListener(this.form_error_event.type, (e) => {
+                submitting.remove();
+            }, true);
 
-                form[0].addEventListener(this.form_success_event.type, (e) => {
-                    submitting.remove();
-                }, true);
-                form[0].addEventListener(this.form_error_event.type, (e) => {
-                    submitting.remove();
-                }, true);
 
+            // remove old messages
+            const error_handler = config.error;
+            const success_handler = config.success;
+            const submit_callback = config.submit;
+            form.find(".caosdb-f-form-elements-message").remove();
+            if (typeof config.submit === "function") {
+                // wrap callback in async function
+                const _wrap_callback = async function () {
+                    try {
+                        var results = await submit_callback(form[0]);
 
-                // remove old messages
-                const error_handler = config.error;
-                const success_handler = config.success;
-                const submit_callback = config.submit;
-                form.find(".caosdb-f-form-elements-message").remove();
-                if (typeof config.submit === "function") {
-                    // wrap callback in async function
-                    const _wrap_callback = async function () {
-                        try {
-                            var results = await submit_callback(form[0]);
-
-                            // success_handler
-                            if (typeof success_handler === "function") {
-                                var processed = await success_handler(form[0], results);
-                                if (typeof processed !== "undefined") {
-                                    form_elements.show_results(form[0], processed);
-                                }
-                            } else {
-                                form_elements.show_results(form[0], results);
+                        // success_handler
+                        if (typeof success_handler === "function") {
+                            var processed = await success_handler(form[0], results);
+                            if (typeof processed !== "undefined") {
+                                form_elements.show_results(form[0], processed);
                             }
+                        } else {
+                            form_elements.show_results(form[0], results);
+                        }
 
-                            form[0].dispatchEvent(form_elements.form_success_event);
-                        } catch (err) {
-
-                            // error_handler
-                            if (typeof error_handler === "function") {
-                                var processed = await error_handler(form[0], err);
-                                if (typeof processed !== "undefined") {
-                                    form_elements.show_results(form[0], processed);
-                                }
-                            } else {
-                                form_elements.show_errors(form[0], err);
-                            }
+                        form[0].dispatchEvent(form_elements.form_success_event);
+                    } catch (err) {
 
-                            form[0].dispatchEvent(form_elements.form_error_event);
+                        // error_handler
+                        if (typeof error_handler === "function") {
+                            var processed = await error_handler(form[0], err);
+                            if (typeof processed !== "undefined") {
+                                form_elements.show_results(form[0], processed);
+                            }
+                        } else {
+                            form_elements.show_errors(form[0], err);
                         }
 
-                    }();
-                }
-                return false;
+                        form[0].dispatchEvent(form_elements.form_error_event);
+                    }
 
+                }();
+            }
+            return false;
 
-            }, true);
 
-            form[0].addEventListener(this.form_success_event.type, function (e) {
-                // remove submit button, show ok button
-                form.find("button[type='submit']").remove();
-                form.find("button:contains('Cancel')").text("Ok").prop("disabled", false);
-            }, true);
-            form[0].addEventListener(this.form_error_event.type, function (e) {
-                // reenable inputs
-                form.find(":input").prop("disabled", false);
-            }, true);
+        }, true);
 
-            // add cancel button
-            $(footer).append(this.make_cancel_button(form[0]));
+        form[0].addEventListener(this.form_success_event.type, function (e) {
+            // remove submit button, show ok button
+            form.find("button[type='submit']").remove();
+            form.find("button:contains('Cancel')").text("Ok").prop("disabled", false);
+        }, true);
+        form[0].addEventListener(this.form_error_event.type, function (e) {
+            // reenable inputs
+            form.find(":input").prop("disabled", false);
+        }, true);
 
-            // init caching for this form
-            form_elements.init_form_caching(config, form[0]);
+        // add cancel button
+        $(footer).append(this.make_cancel_button(form[0]));
 
-            // init validation
-            form_elements.init_validator(form[0]);
+        // init caching for this form
+        form_elements.init_form_caching(config, form[0]);
 
-            this.logger.trace("leave make_generic_form");
-            return form[0];
-        }
+        // init validation
+        form_elements.init_validator(form[0]);
 
-        this.init_form_caching = function (config, form) {
-            var default_config = {
-                "cache_event": form_elements.submit_form_event.type,
-                "cache_storage": localStorage
-            };
-            var lconfig = $.extend({}, default_config, config);
+        this.logger.trace("leave make_generic_form");
+        return form[0];
+    }
 
-            this.logger.trace("init_form_caching", lconfig, form);
+    this.init_form_caching = function (config, form) {
+        var default_config = {
+            "cache_event": form_elements.submit_form_event.type,
+            "cache_storage": localStorage
+        };
+        var lconfig = $.extend({}, default_config, config);
 
-            form.addEventListener(lconfig.cache_event, (e) => {
-                form_elements.cache_form(lconfig.cache_storage, form);
-            }, true);
-            form_elements.load_cached(lconfig.cache_storage, form);
-        }
+        this.logger.trace("init_form_caching", lconfig, form);
 
-        this.show_results = function (form, results) {
-            $(form).append(results);
-        }
+        form.addEventListener(lconfig.cache_event, (e) => {
+            form_elements.cache_form(lconfig.cache_storage, form);
+        }, true);
+        form_elements.load_cached(lconfig.cache_storage, form);
+    }
 
-        this.show_errors = function (form, errors) {
-            $(form).append(errors);
-        }
+    this.show_results = function (form, results) {
+        $(form).append(results);
+    }
 
-        this.make_footer = function () {
-            return $('<div class="text-right caosdb-f-form-elements-footer"/>')
-                .css({
-                    "margin": "20px",
-                }).append(this.make_required_marker())
-                .append('<span style="margin-right: 4px; font-size: 11px">required field</span>')[0];
-        }
+    this.show_errors = function (form, errors) {
+        $(form).append(errors);
+    }
 
-        this.make_error_message = function (message) {
-            return this.make_message(message, "error");
-        }
+    this.make_footer = function () {
+        return $('<div class="text-right caosdb-f-form-elements-footer"/>')
+            .css({
+                "margin": "20px",
+            }).append(this.make_required_marker())
+            .append('<span style="margin-right: 4px; font-size: 11px">required field</span>')[0];
+    }
 
-        this.make_success_message = function (message) {
-            return this.make_message(message, "success");
-        }
+    this.make_error_message = function (message) {
+        return this.make_message(message, "error");
+    }
 
-        this.make_submitting_info = function () {
-            // TODO styling
-            return $(this.make_message("Submitting... please wait. This might take some time.", "info"))
-                .toggleClass("h3", true)
-                .toggleClass("caosdb-f-form-submitting", true)
-                .toggleClass("text-right", true)[0];
-        }
+    this.make_success_message = function (message) {
+        return this.make_message(message, "success");
+    }
 
-        this.make_message = function (message, type) {
-            var ret = $('<div class="caosdb-f-form-elements-message"/>');
-            if (type) {
-                ret.addClass("caosdb-f-form-elements-message-" + type);
-            }
-            return ret.append(markdown.textToHtml(message))[0];
+    this.make_submitting_info = function () {
+        // TODO styling
+        return $(this.make_message("Submitting... please wait. This might take some time.", "info"))
+            .toggleClass("h3", true)
+            .toggleClass("caosdb-f-form-submitting", true)
+            .toggleClass("text-right", true)[0];
+    }
+
+    this.make_message = function (message, type) {
+        var ret = $('<div class="caosdb-f-form-elements-message"/>');
+        if (type) {
+            ret.addClass("caosdb-f-form-elements-message-" + type);
         }
+        return ret.append(markdown.textToHtml(message))[0];
+    }
 
-        /**
-         * TODO make syncronous
-         */
-        this.make_range_input = async function (config) {
-
-            // TODO 
-            // 1. wrapp both inputs to separate it from the label into a container
-            // 2. make two rows for each input
-            // 3. make inline-block for all included elements
-            const from_config = $.extend({}, {
-                cached: config.cached,
-                required: config.required,
-                type: "double"
-            }, config.from);
-            const to_config = $.extend({}, {
-                cached: config.cached,
-                required: config.required,
-                type: "double"
-            }, config.to);
-
-            const from_input = await this.make_form_field(from_config);
-            const to_input = await this.make_form_field(to_config);
-
-            const ret = $(this._make_field_wrapper(config.name));
-            if (config.label) {
-                ret.append(this._make_input_label_str(config));
-            }
+    /**
+     * TODO make syncronous
+     */
+    this.make_range_input = async function (config) {
+
+        // TODO 
+        // 1. wrapp both inputs to separate it from the label into a container
+        // 2. make two rows for each input
+        // 3. make inline-block for all included elements
+        const from_config = $.extend({}, {
+            cached: config.cached,
+            required: config.required,
+            type: "double"
+        }, config.from);
+        const to_config = $.extend({}, {
+            cached: config.cached,
+            required: config.required,
+            type: "double"
+        }, config.to);
+
+        const from_input = await this.make_form_field(from_config);
+        const to_input = await this.make_form_field(to_config);
+
+        const ret = $(this._make_field_wrapper(config.name));
+        if (config.label) {
+            ret.append(this._make_input_label_str(config));
+        }
 
-            ret.append(from_input);
-            ret.append(to_input);
+        ret.append(from_input);
+        ret.append(to_input);
 
-            // styling
-            $(from_input).toggleClass("form-group", false);
-            $(from_input).find(".col-sm-3").toggleClass("col-sm-3", false).toggleClass("col-sm-1");
-            $(from_input).find(".col-sm-9").toggleClass("col-sm-9", false).toggleClass("col-sm-3");
-            $(to_input).toggleClass("form-group", false);
-            $(to_input).find(".col-sm-3").toggleClass("col-sm-3", false).toggleClass("col-sm-1").toggleClass("col-sm-offset-1");
-            $(to_input).find(".col-sm-9").toggleClass("col-sm-9", false).toggleClass("col-sm-3");
+        // styling
+        $(from_input).toggleClass("form-group", false);
+        $(from_input).find(".col-sm-3").toggleClass("col-sm-3", false).toggleClass("col-sm-1");
+        $(from_input).find(".col-sm-9").toggleClass("col-sm-9", false).toggleClass("col-sm-3");
+        $(to_input).toggleClass("form-group", false);
+        $(to_input).find(".col-sm-3").toggleClass("col-sm-3", false).toggleClass("col-sm-1").toggleClass("col-sm-offset-1");
+        $(to_input).find(".col-sm-9").toggleClass("col-sm-9", false).toggleClass("col-sm-3");
 
-            return ret[0];
-        }
+        return ret[0];
+    }
 
-        /**
-         * Return a DIV with class `caosdb-f-field` and a data attribute
-         * `data-field-name` which contains the name.
-         *
-         * The DIV is used to wrap LABEL and INPUT elements of a form together.
-         *
-         * @param {string} name - the name of the field.
-         * @returns {HTMLElement} a DIV.
-         */
-        this._make_field_wrapper = function (name) {
-            caosdb_utils.assert_string(name, "param `name`");
-            return $('<div class="form-group caosdb-f-field" data-field-name="' + name + '" />')
-                .css({"padding": "0"})[0];
-        }
+    /**
+     * Return a DIV with class `caosdb-f-field` and a data attribute
+     * `data-field-name` which contains the name.
+     *
+     * The DIV is used to wrap LABEL and INPUT elements of a form together.
+     *
+     * @param {string} name - the name of the field.
+     * @returns {HTMLElement} a DIV.
+     */
+    this._make_field_wrapper = function (name) {
+        caosdb_utils.assert_string(name, "param `name`");
+        return $('<div class="form-group caosdb-f-field" data-field-name="' + name + '" />')
+            .css({"padding": "0"})[0];
+    }
 
-        this.make_date_input = function (config) {
-            return this._make_input(config);
-        }
+    this.make_date_input = function (config) {
+        return this._make_input(config);
+    }
 
-        this.make_text_input = function (config) {
-            return this._make_input(config);
-        }
+    this.make_text_input = function (config) {
+        return this._make_input(config);
+    }
 
 
-        /**
-         * Return an input field which accepts double values.
-         *
-         * `config.type` is set to "number" and overrides any other type.
-         *
-         * @param {form_elements.input_config} config.
-         * @returns {HTMLElement} a double form field.
-         */
-        this.make_double_input = function (config) {
-            var clone = $.extend({}, config, {
-                type: "number"
-            });
-            var ret = $(this._make_input(clone))
-            ret.find("input").attr("step", "any");
-            return ret[0];
-        }
+    /**
+     * Return an input field which accepts double values.
+     *
+     * `config.type` is set to "number" and overrides any other type.
+     *
+     * @param {form_elements.input_config} config.
+     * @returns {HTMLElement} a double form field.
+     */
+    this.make_double_input = function (config) {
+        var clone = $.extend({}, config, {
+            type: "number"
+        });
+        var ret = $(this._make_input(clone))
+        ret.find("input").attr("step", "any");
+        return ret[0];
+    }
 
 
-        /**
-         * Return an input field which accepts integers.
-         *
-         * `config.type` is set to "number" and overrides any other type.
-         *
-         * @param {form_elements.input_config} config.
-         * @returns {HTMLElement} an integer form field.
-         */
-        this.make_integer_input = function (config) {
-            var ret = $(this.make_double_input(config));
-            ret.find("input").attr("step", "1");
-            return ret[0];
-        }
+    /**
+     * Return an input field which accepts integers.
+     *
+     * `config.type` is set to "number" and overrides any other type.
+     *
+     * @param {form_elements.input_config} config.
+     * @returns {HTMLElement} an integer form field.
+     */
+    this.make_integer_input = function (config) {
+        var ret = $(this.make_double_input(config));
+        ret.find("input").attr("step", "1");
+        return ret[0];
+    }
 
 
-        /**
-         * Return a checkbox input field.
-         *
-         * @param {form_elements.checkbox_config} config.
-         * @returns {HTMLElement} a checkbox form field.
-         */
-        this.make_checkbox_input = function (config) {
-            var clone = $.extend({}, config, {
-                type: "checkbox"
-            });
-            var ret = $(this._make_input(clone));
-            ret.find("input:checkbox").prop("checked", false);
-            ret.find("input:checkbox").toggleClass("form-control", false);
-            if (config.checked) {
-                ret.find("input:checkbox").prop("checked", true);
-                ret.find("input:checkbox").attr("checked", "checked");
-            }
-            if (config.value) {
-                ret.find("input:checkbox").attr("value", config.value);
-            }
-            return ret[0];
+    /**
+     * Return a checkbox input field.
+     *
+     * @param {form_elements.checkbox_config} config.
+     * @returns {HTMLElement} a checkbox form field.
+     */
+    this.make_checkbox_input = function (config) {
+        var clone = $.extend({}, config, {
+            type: "checkbox"
+        });
+        var ret = $(this._make_input(clone));
+        ret.find("input:checkbox").prop("checked", false);
+        ret.find("input:checkbox").toggleClass("form-control", false);
+        if (config.checked) {
+            ret.find("input:checkbox").prop("checked", true);
+            ret.find("input:checkbox").attr("checked", "checked");
+        }
+        if (config.value) {
+            ret.find("input:checkbox").attr("value", config.value);
         }
+        return ret[0];
+    }
 
 
-        /**
-         * Add `caosdb-f-form-field-required` class to form field.
-         *
-         * @param {HTMLElement} field - the required form field.
-         */
-        this.set_required = function (field) {
-            $(field).toggleClass("caosdb-f-form-field-required", true);
-            $(field).find(":input").prop("required", true);
-            $(field).find("label").prepend(this.make_required_marker());
-        }
+    /**
+     * Add `caosdb-f-form-field-required` class to form field.
+     *
+     * @param {HTMLElement} field - the required form field.
+     */
+    this.set_required = function (field) {
+        $(field).toggleClass("caosdb-f-form-field-required", true);
+        $(field).find(":input").prop("required", true);
+        $(field).find("label").prepend(this.make_required_marker());
+    }
 
-        /**
-         * Return a span which is to be inserted before a field's label text
-         * and which marks that field as required.
-         *
-         * @returns {HTMLElement} span element.
-         */
-        this.make_required_marker = function () {
-            // TODO create class and move to css file
-            return $('<span>*</span>')
-                .css({
-                    "font-size": "10px",
-                    "color": "red",
-                    "margin-right": "4px",
-                    "font-weight": "100",
-                })[0];
-        }
+    /**
+     * Return a span which is to be inserted before a field's label text
+     * and which marks that field as required.
+     *
+     * @returns {HTMLElement} span element.
+     */
+    this.make_required_marker = function () {
+        // TODO create class and move to css file
+        return $('<span>*</span>')
+            .css({
+                "font-size": "10px",
+                "color": "red",
+                "margin-right": "4px",
+                "font-weight": "100",
+            })[0];
+    }
 
 
-        this.get_enabled_required_fields = function (form) {
-            return $(this.get_enabled_fields(form))
-                .filter(".caosdb-f-form-field-required")
-                .toArray();
-        }
+    this.get_enabled_required_fields = function (form) {
+        return $(this.get_enabled_fields(form))
+            .filter(".caosdb-f-form-field-required")
+            .toArray();
+    }
 
 
-        this.get_enabled_fields = function (form) {
-            return $(form)
-                .find(".caosdb-f-field")
-                .filter(function (idx) {
-                    // remove disabled fields from results
-                    return !$(this).hasClass("caosdb-f-field-disabled");
-                })
-                .toArray();
-        }
+    this.get_enabled_fields = function (form) {
+        return $(form)
+            .find(".caosdb-f-field")
+            .filter(function (idx) {
+                // remove disabled fields from results
+                return !$(this).hasClass("caosdb-f-field-disabled");
+            })
+            .toArray();
+    }
 
 
-        this.all_required_fields_set = function (form) {
-            const req = form_elements.get_enabled_required_fields(form);
-            for (const field of req) {
-                if (!form_elements.is_set(field)) {
-                    return false;
-                }
+    this.all_required_fields_set = function (form) {
+        const req = form_elements.get_enabled_required_fields(form);
+        for (const field of req) {
+            if (!form_elements.is_set(field)) {
+                return false;
             }
-            return true;
         }
+        return true;
+    }
 
-        /**
-         * @param {HTMLElement} form - the form be validated.
-         */
-        this.is_valid = function (form) {
-            return form_elements.all_required_fields_set(form);
-        }
+    /**
+     * @param {HTMLElement} form - the form be validated.
+     */
+    this.is_valid = function (form) {
+        return form_elements.all_required_fields_set(form);
+    }
 
 
-        this.toggle_submit_button_form_valid = function (form, submit) {
-            // TODO do not change the submit button directly. change the
-            // `submittable` state of the form and handle the case where a form
-            // is submitting when this function is called.
-            if (form_elements.is_valid(form)) {
-                $(submit).prop("disabled", false);
-            } else {
-                $(submit).prop("disabled", true);
-            }
+    this.toggle_submit_button_form_valid = function (form, submit) {
+        // TODO do not change the submit button directly. change the
+        // `submittable` state of the form and handle the case where a form
+        // is submitting when this function is called.
+        if (form_elements.is_valid(form)) {
+            $(submit).prop("disabled", false);
+        } else {
+            $(submit).prop("disabled", true);
         }
+    }
 
 
-        this.init_validator = function (form) {
-            const submit = $(form).find(":input[type='submit']")[0];
-            if (submit) {
-                form.addEventListener("caosdb.field.changed", (e) => form_elements.toggle_submit_button_form_valid(form, submit), true);
-                form.addEventListener("caosdb.field.enabled", (e) => form_elements.toggle_submit_button_form_valid(form, submit), true);
-                form.addEventListener("input", (e) => form_elements.toggle_submit_button_form_valid(form, submit), true);
-            }
+    this.init_validator = function (form) {
+        const submit = $(form).find(":input[type='submit']")[0];
+        if (submit) {
+            form.addEventListener("caosdb.field.changed", (e) => form_elements.toggle_submit_button_form_valid(form, submit), true);
+            form.addEventListener("caosdb.field.enabled", (e) => form_elements.toggle_submit_button_form_valid(form, submit), true);
+            form.addEventListener("input", (e) => form_elements.toggle_submit_button_form_valid(form, submit), true);
         }
+    }
 
 
-        /**
-         * Return an input and a label, wrapped in a div with class
-         * `caosdb-f-field`.
-         *
-         * @param {object} config - config object with `name`, `type` and
-         *      optional `label`
-         * @returns {HTMLElement} a form field.
-         */
-        this._make_input = function (config) {
-            caosdb_utils.assert_string(config.name, "the name of a form field");
-            let ret = $(this._make_field_wrapper(config.name));
-            let name = config.name;
-            let label = this._make_input_label_str(config);
-            let type = config.type || "text";
-            let value = config.value;
-            let input = $('<input class="form-control caosdb-f-property-single-raw-value" type="' + type +
-                '" name="' + name +
-                '" />');
-            input.change(function () {
-                ret[0].dispatchEvent(form_elements.field_changed_event);
-            });
-            let input_col = $('<div class="caosdb-f-property-value col-sm-9"/>');
-            input_col.append(input);
-            if (value) {
-                input.val(value);
-            }
-            return ret.append(label, input_col)[0];
-        }
-
-        /**
-         * Return a string representation of a LABEL element, ready for parsing.
-         *
-         * This function is used by other functions to generate a LABEL element.
-         *
-         * The config's `name` goes to the `for` attribute, the `label` is the
-         * text node of the resulting LABEL element.
-         *
-         * @param {object} config - a config object with `name` and `label`.
-         * @returns {string} a html string for a LABEL element.
-         */
-        this._make_input_label_str = function (config) {
-            let name = config.name;
-            let label = config.label;
-            return label ? '<label for="' + name +
-                '" data-property-name="' + name +
-                '" class="control-label col-sm-3">' + label +
-                '</label>' : "";
+    /**
+     * Return an input and a label, wrapped in a div with class
+     * `caosdb-f-field`.
+     *
+     * @param {object} config - config object with `name`, `type` and
+     *      optional `label`
+     * @returns {HTMLElement} a form field.
+     */
+    this._make_input = function (config) {
+        caosdb_utils.assert_string(config.name, "the name of a form field");
+        let ret = $(this._make_field_wrapper(config.name));
+        let name = config.name;
+        let label = this._make_input_label_str(config);
+        let type = config.type || "text";
+        let value = config.value;
+        let input = $('<input class="form-control caosdb-f-property-single-raw-value" type="' + type +
+            '" name="' + name +
+            '" />');
+        input.change(function () {
+            ret[0].dispatchEvent(form_elements.field_changed_event);
+        });
+        let input_col = $('<div class="caosdb-f-property-value col-sm-9"/>');
+        input_col.append(input);
+        if (value) {
+            input.val(value);
         }
+        return ret.append(label, input_col)[0];
+    }
 
+    /**
+     * Return a string representation of a LABEL element, ready for parsing.
+     *
+     * This function is used by other functions to generate a LABEL element.
+     *
+     * The config's `name` goes to the `for` attribute, the `label` is the
+     * text node of the resulting LABEL element.
+     *
+     * @param {object} config - a config object with `name` and `label`.
+     * @returns {string} a html string for a LABEL element.
+     */
+    this._make_input_label_str = function (config) {
+        let name = config.name;
+        let label = config.label;
+        return label ? '<label for="' + name +
+            '" data-property-name="' + name +
+            '" class="control-label col-sm-3">' + label +
+            '</label>' : "";
     }
+
     this._init_functions();
 }
 
diff --git a/src/core/js/preview.js b/src/core/js/preview.js
index f74fd13ffd2fe373543c025866e91cfe6b6fd9eb..b85f7b56dc13ff94aa2c3de90eb1b464d7f1e9d6 100644
--- a/src/core/js/preview.js
+++ b/src/core/js/preview.js
@@ -211,8 +211,8 @@ var preview = new function() {
     /**
      * 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.
+     * @param {Promise | XMLDocument} xml - A Promise for the servers xml response.
+     * @return {Promise | HTMLElement[]} A Promise for an array of entities.
      */
     this.processPreviewResponse = function(xml) {
         let xsl = preview.getEntityXsl();
@@ -222,7 +222,7 @@ var preview = new function() {
     /**
      * Retrieve the XSL script for entities from the server.
      *
-     * @return {Promise for XMLDocument} A Promise for the XSL script.
+     * @return {Promise | XMLDocument} A Promise for the XSL script.
      */
     this.getEntityXsl = async function _getEntityXsl() {
         return transformation.retrieveEntityXsl();
@@ -679,7 +679,7 @@ var preview = new function() {
      * 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.
+     * @return {Promise | HTMLElement[]} A Promise for an array of entities.
      */
     this.retrievePreviewEntities = async function _rPE(entityIds) {
         try {
@@ -711,9 +711,9 @@ var preview = new function() {
     /**
      * 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.
+     * @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);
diff --git a/src/core/js/tour.js b/src/core/js/tour.js
index 7b47dd37239c29b5c5e7edb67f1d16fe43bf8ad6..cfe65519eef0f34f708461bca3658e76f5530eff 100644
--- a/src/core/js/tour.js
+++ b/src/core/js/tour.js
@@ -630,6 +630,7 @@ var tour = new function() {
                 content: markdown_content,
                 placement: placement,
                 html: true,
+                sanitize: false,
                 trigger: 'manual',
                 template: popover_template,
             });
@@ -934,8 +935,6 @@ var tour = new function() {
                 tour_overview.append(next);
             }
 
-            panel.hover(undefined, ()=>{panel.collapse('hide');});
-
             panel.append(tour_overview);
 
             this.leave_tour_button.on("click", () => {this.deactivate();});
@@ -981,6 +980,9 @@ var tour = new function() {
             tour._instance.set_tour_button_text("Tour");
         }
         $('#caosdb-query-panel').before(tour._instance.panel);
+        // hide, when the mouse leaves the navbar
+        $('nav.navbar').hover(undefined, ()=>{$(tour._instance.panel).collapse('hide');});
+
     }
 
 
diff --git a/src/core/js/webcaosdb.js b/src/core/js/webcaosdb.js
index ac4d908bca32e0f8bf0f0645a6f4d5bc9d628ada..55337a36a97d6a7ad42aadfe673e429d6d942a0a 100644
--- a/src/core/js/webcaosdb.js
+++ b/src/core/js/webcaosdb.js
@@ -327,6 +327,30 @@ this.caosdb_utils = new function () {
         }
         throw new TypeError(name + " is expected to be an array, was " + typeof obj);
     }
+
+    /**
+     * Create a tsv table as string.
+     *
+     * The data must be appropriately encoded (e.g. urlencoded).
+     *
+     * With `tab=","` it is also possible to create csv tables.
+     *
+     * @param {string[][]} data - An array of rows which contain arrays of
+     *     cells.
+     * @param {string} [preamble="data:text/csv;charset=utf-8,"] - a prefix for
+     *     the the resulting string. The default is suitable for creating
+     *     downloadable href attributes of links.
+     * @param {string} [tab="%09"] - the cell separator.
+     * @param {string} [newline="%0A"] - the row separator.
+     * @return {string} a tsv table as a string.
+     */
+    this.create_tsv_table = function(data, preamble, tab, newline) {
+        preamble = ((typeof preamble == 'undefined') ? "data:text/csv;charset=utf-8,": preamble);
+        tab = tab || "%09";
+        newline = newline || "%0A";
+        const rows = data.map(x => x.join(tab))
+        return `${preamble}${rows.join(newline)}`;
+    }
 }
 
 /**
@@ -651,7 +675,7 @@ this.transaction = new function () {
      */
     this.retrieveEntitiesById = async function _rEBIs(entityIds) {
         const response = await connection.get(this.generateEntitiesUri(entityIds));
-        return $(response).find('Response [id]').toArray();
+        return $(response).find('Response > [id]').toArray();
     }
 
     /** Sends a PUT request with an xml representation of entities and
@@ -964,6 +988,117 @@ this.transaction = new function () {
     }
 }
 
+/**
+ * This module provides the functionality to load the full version history (for
+ * privileged users) and export it to tsv.
+ */
+var version_history = new function () {
+
+    this._get = connection.get;
+    /**
+     * Retrieve the version history of an entity and return a table with the
+     * history.
+     *
+     * @param {string} entity - the entity id with or without version id.
+     * @return {HTMLElement} A table with the version history.
+     */
+    this.retrieve_history = async function(entity) {
+        const xml = this._get(transaction
+            .generateEntitiesUri([entity]) + "?H");
+        const html = (await transformation.transformEntities(xml))[0];
+        const history_table = $(html).find(".caosdb-f-entity-version-history");
+        return history_table[0];
+    }
+
+    /**
+     * Initalize the buttons for loading the version history.
+     *
+     * The buttons are visible when the entity has only the normal version info
+     * attached and the current user has the permissions to retrieve the
+     * version history.
+     *
+     * The buttons trigger the retrieval of the version history and append the
+     * version history to the version info modal.
+     */
+    this.init_load_history_buttons = function () {
+        for (let entity of $(".caosdb-entity-panel")) {
+            const is_permitted = hasEntityPermission(entity, "RETRIEVE:HISTORY");
+            if (!is_permitted) {
+                continue;
+            }
+            const entity_id_version = getEntityIdVersion(entity);
+            const version_info = $(entity)
+                .find(".caosdb-f-entity-version-info");
+            const button = $(version_info)
+                .find(".caosdb-f-entity-version-load-history-btn");
+            button.show();
+            button
+                .click(async () => {
+                    button.prop("disabled", true);
+                    const wait = createWaitingNotification("Retrieving full history. Please wait.");
+                    const sparse = $(version_info)
+                        .find(".caosdb-f-entity-version-history");
+                    sparse.find(".modal-body *").replaceWith(wait);
+
+                    const history_table = await version_history
+                        .retrieve_history(entity_id_version);
+                    sparse.replaceWith(history_table);
+                    version_history.init_export_history_buttons(entity);
+                });
+        }
+    }
+
+    /**
+     * Transform the HTML table with the version history to tsv.
+     *
+     * @param {HTMLElement} history_table - the HTML representation of the
+     *     version history.
+     * @return {string} the version history as downloadable tsv string,
+     *     suitable for the href attribute of a link or window.location.
+     */
+    this.get_history_tsv = function (history_table) {
+        const rows = [];
+        for (let row of $(history_table).find("tr")) {
+          const cells = $(row).find(".export-data").toArray().map(x => x.textContent);
+          rows.push(cells);
+        }
+        return caosdb_utils.create_tsv_table(rows);
+    }
+
+    /**
+     * Initialize the export buttons of `entity`.
+     *
+     * The buttons are only visible when the version history is visible and
+     * trigger a download of a tsv file which contains the version history.
+     *
+     * The buttons trigger the download of a tsv file with the version history.
+     *
+     * @param {HTMLElement} [entity] - if undefined, the export buttons of all
+     *     page entities are initialized.
+     */
+    this.init_export_history_buttons = function (entity) {
+        entity = entity || $(".caosdb-entity-panel");
+        for (let version_info of $(entity)
+            .find(".caosdb-f-entity-version-info")) {
+            $(version_info).find(".caosdb-f-entity-version-export-history-btn")
+                .click(async () => {
+                    const html_table = $(version_info).find("table")[0];
+                    const history_tsv = this.get_history_tsv(html_table);
+                    version_history._download_tsv(history_tsv);
+                });
+        }
+    }
+
+    this._download_tsv = function(tsv_link) {
+        window.location.href = tsv_link;
+    }
+
+
+    this.init = function () {
+        this.init_load_history_buttons();
+        this.init_export_history_buttons();
+    }
+}
 
 var paging = new function () {
 
@@ -1571,13 +1706,13 @@ function insertParam(xsl, name, value = null) {
 /**
  * When the page is scrolled down 100 pixels, the scroll-back button appears.
  * 
- * @return
+ * @return FIXME
  */
 
 /**
  * Every initial function calling is done here.
  * 
- * @return
+ * @return TODO
  */
 function initOnDocumentReady() {
     hintMessages.init();
@@ -1602,6 +1737,7 @@ function initOnDocumentReady() {
     }
     caosdb_modules.init();
     navbar.init();
+    version_history.init();
 
 }
 
diff --git a/src/core/xsl/entity.xsl b/src/core/xsl/entity.xsl
index 826961b9e3f748daf73017715348fa3682bcbb1a..cac9e87ea40bf6a687a19a1942b5534276c8faf7 100644
--- a/src/core/xsl/entity.xsl
+++ b/src/core/xsl/entity.xsl
@@ -103,6 +103,7 @@
       <xsl:attribute name="data-entity-id">
         <xsl:value-of select="@id"/>
       </xsl:attribute>
+      <xsl:apply-templates mode="entity-permissions" select="Permissions"/>
       <!-- A page-unique ID for this entity -->
       <xsl:variable name="entityid" select="concat('entity_',generate-id())"/>
       <div class="panel-heading caosdb-entity-panel-heading">
@@ -158,7 +159,11 @@
               </span>
               <button class="btn btn-link caosdb-v-bookmark-button">
                 <xsl:attribute name="data-bmval">
-                  <xsl:value-of select="@id"/>@<xsl:value-of select="Version/@id"/>
+                  <xsl:value-of select="@id"/>
+                  <xsl:if test="Version/Successor">
+                    <!-- this is not the head -->
+                    <xsl:value-of select="concat('@', Version/@id)"/>
+                  </xsl:if>
                 </xsl:attribute>
                 <span class="glyphicon glyphicon-bookmark"/>
               </button>
@@ -524,78 +529,197 @@
       </xsl:attribute>
       <span class="glyphicon glyphicon-time"/>
     </button>
+
     <!-- the following div.modal is the window that pops up when the user clicks on the clock button -->
     <div class="caosdb-f-entity-version-info modal fade" tabindex="-1" role="dialog">
       <xsl:attribute name="id"><xsl:value-of select="$versionModalId"/></xsl:attribute>
+      <xsl:attribute name="data-entity-versioned-id"><xsl:value-of select="concat($entityId, '@', @id)"/></xsl:attribute>
       <div class="modal-dialog modal-lg" role="document">
         <div class="modal-content text-left">
+          <!-- modal-header start -->
           <div>
             <xsl:attribute name="class">
               modal-header
-              <xsl:if test="Successor">
+              <xsl:if test="not(@head='true')">
                 <!-- indicate old version by color -->
                 <xsl:value-of select="' bg-danger'"/>
               </xsl:if>
             </xsl:attribute>
             <button type="button" class="close" data-dismiss="modal" aria-label="Close" title="Close"><span aria-hidden="true">×</span></button>
             <h4 class="modal-title">Version Info</h4>
-            <p class="caosdb-entity-heading-attr">
+              <p class="caosdb-entity-heading-attr">
               <em class="caosdb-entity-heading-attr-name">
               This is
-              <xsl:if test="Successor"><b>not</b></xsl:if>
+              <xsl:if test="not(@head='true')"><b>not</b></xsl:if>
               the latest version of this entity.
+              <xsl:apply-templates mode="entity-version-modal-head" select="Successor">
+                <xsl:with-param name="entityId" select="$entityId"/>
+              </xsl:apply-templates>
               </em>
             </p>
           </div>
-          <div class="modal-body">
-            <xsl:apply-templates mode="entity-version-modal-head" select="Successor">
+          <!-- modal-header end -->
+          <div class="caosdb-f-entity-version-history">
+            <!-- modal-body and modal-footer are added by this template -->
+            <xsl:apply-templates select="." mode="entity-version-history-table">
               <xsl:with-param name="entityId" select="$entityId"/>
             </xsl:apply-templates>
-            <xsl:apply-templates mode="entity-version-modal-successor" select="Successor">
+          </div>
+        </div>
+      </div>
+    </div>
+  </xsl:template>
+
+  <xsl:template match="Version[@completeHistory='true']" mode="entity-version-history-table">
+    <!-- contains the table of the full version history -->
+    <xsl:param name="entityId"/>
+    <div class="modal-body">
+      <table class="table table-hover">
+        <thead>
+          <tr><div class="export-data">Entity ID</div><th/>
+            <th class="export-data">Version ID</th>
+            <th class="export-data">Date</th>
+            <th class="export-data">User</th>
+            <div class="export-data">URI</div>
+          </tr></thead>
+        <tbody>
+          <xsl:apply-templates mode="entity-version-modal-successor" select="Successor">
+            <xsl:with-param name="entityId" select="$entityId"/>
+          </xsl:apply-templates>
+          <tr>
+            <div class="export-data"><xsl:value-of select="$entityId"/></div>
+            <td class="caosdb-v-entity-version-hint caosdb-v-entity-version-hint-cur">This Version</td>
+            <td><xsl:apply-templates select="@id" mode="entity-version-id"/>
+            </td><td>
+              <xsl:apply-templates select="@date" mode="entity-version-date"/>
+            </td><td class="export-data">
+              <xsl:value-of select="@username"/>@<xsl:value-of select="@realm"/>
+            </td>
+            <div class="export-data"><xsl:value-of select="concat($entitypath, $entityId, '@', @id)"/></div>
+          </tr>
+          <xsl:apply-templates mode="entity-version-modal-predecessor" select="Predecessor">
+            <xsl:with-param name="entityId" select="$entityId"/>
+          </xsl:apply-templates>
+        </tbody>
+      </table>
+    </div>
+    <div class="modal-footer">
+      <button type="button" class="caosdb-f-entity-version-export-history-btn btn btn-default">Export history</button>
+    </div>
+  </xsl:template>
+
+  <xsl:template match="Version[not(@completeHistory='true')]" mode="entity-version-history-table">
+    <!-- contains the table of the simple version info (not the full history)-->
+    <xsl:param name="entityId"/>
+    <div class="modal-body">
+      <table class="table">
+        <thead><tr><th>Previous Version</th><th>This Version</th><th>Next Version</th></tr></thead>
+        <tbody>
+          <tr>
+          <td>
+            <xsl:if test="not(Predecessor)">
+              <div class="caosdb-v-entity-version-no-related">No predecessor</div>
+            </xsl:if>
+            <xsl:apply-templates select="Predecessor/@id" mode="entity-version-link-to-other-version">
               <xsl:with-param name="entityId" select="$entityId"/>
             </xsl:apply-templates>
-            <p class="caosdb-entity-heading-attr">
-              <em class="caosdb-entity-heading-attr-name">This version:</em>
-              <xsl:value-of select="@id"/> (<xsl:value-of select="@date"/>)
-            </p>
-            <xsl:apply-templates mode="entity-version-modal-predecessor" select="Predecessor">
+          </td>
+          <td>
+            <xsl:apply-templates select="@id" mode="entity-version-id"/>
+          </td>
+          <td>
+            <xsl:if test="not(Successor)">
+              <div class="caosdb-v-entity-version-no-related">No successor</div>
+            </xsl:if>
+            <xsl:apply-templates select="Successor/@id" mode="entity-version-link-to-other-version">
               <xsl:with-param name="entityId" select="$entityId"/>
             </xsl:apply-templates>
-          </div>
-        </div>
-      </div>
+          </td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+    <div class="modal-footer">
+      <button type="button" style="display: none" class="caosdb-f-entity-version-load-history-btn btn btn-default">Load full history</button>
     </div>
   </xsl:template>
+
+  <xsl:template match="@id" mode="entity-version-id">
+    <!-- a versions'id (abbreviated) -->
+    <xsl:attribute name="title">Full Version ID: <xsl:value-of select="."/></xsl:attribute>
+    <xsl:value-of select="substring(.,1,8)"/>
+    <div class="export-data"><xsl:value-of select="."/></div>
+  </xsl:template>
+
+  <xsl:template match="@date" mode="entity-version-date">
+    <!-- a version's date (abbreviated)-->
+    <xsl:attribute name="title"><xsl:value-of select="."/></xsl:attribute>
+    <xsl:value-of select="substring(.,0,11)"/>
+    <xsl:value-of select="' '"/>
+    <xsl:value-of select="substring(.,12,8)"/>
+    <div class="export-data"><xsl:value-of select="."/></div>
+  </xsl:template>
+
+  <xsl:template match="Predecessor|Successor" mode="entity-version-modal-single-history-item">
+    <!-- a single row of the version history table -->
+    <xsl:param name="entityId"/>
+    <xsl:param name="hint"/>
+    <tr>
+      <div class="export-data"><xsl:value-of select="$entityId"/></div>
+      <td class="caosdb-v-entity-version-hint"><xsl:value-of select="$hint"/></td>
+      <td>
+        <xsl:apply-templates select="@id" mode="entity-version-link-to-other-version">
+          <xsl:with-param name="entityId" select="$entityId"/>
+        </xsl:apply-templates>
+      </td><td>
+        <xsl:apply-templates select="@date" mode="entity-version-date"/>
+      </td><td class="export-data">
+        <xsl:value-of select="@username"/>@<xsl:value-of select="@realm"/>
+      </td>
+      <div class="export-data"><xsl:value-of select="concat($entitypath, $entityId, '@', @id)"/></div>
+    </tr>
+  </xsl:template>
+
+  <xsl:template match="@id" mode="entity-version-link-to-other-version">
+    <!-- link to other version (used by both version tables)-->
+    <xsl:param name="entityId"/>
+    <a><xsl:attribute name="href"><xsl:value-of select="$entityId"/>@<xsl:value-of select="."/></xsl:attribute>
+      <xsl:apply-templates select="." mode="entity-version-id"/></a>
+  </xsl:template>
+
   <xsl:template match="Predecessor" mode="entity-version-modal-predecessor">
-    <!-- content of the versioning window -->
+    <!-- content of the versioning info (not the full history) -->
     <xsl:param name="entityId"/>
-    <p class="caosdb-entity-heading-attr">
-      <em class="caosdb-entity-heading-attr-name">Previous version:</em>
-      <a><xsl:attribute name="href"><xsl:value-of select="$entityId"/>@<xsl:value-of select="@id"/></xsl:attribute>
-        <xsl:value-of select="@id"/> (<xsl:value-of select="@date"/>)
-      </a>
-    </p>
+    <xsl:apply-templates mode="entity-version-modal-single-history-item" select=".">
+      <xsl:with-param name="entityId" select="$entityId"/>
+      <xsl:with-param name="hint" select="'Older Version'"/>
+    </xsl:apply-templates>
+    <xsl:apply-templates mode="entity-version-modal-predecessor" select="Predecessor">
+      <xsl:with-param name="entityId" select="$entityId"/>
+    </xsl:apply-templates>
   </xsl:template>
+
   <xsl:template match="Successor" mode="entity-version-modal-head">
-    <!-- content of the versioning window -->
+    <!-- content of the versioning modal's header (if a newer version exists) -->
     <xsl:param name="entityId"/>
-    <p class="caosdb-entity-heading-attr">
-      <em class="caosdb-entity-heading-attr-name">Newest version:</em>
-      <a><xsl:attribute name="href"><xsl:value-of select="$entityId"/>@HEAD</xsl:attribute>
-        <xsl:value-of select="$entityId"/>@HEAD
-      </a>
-    </p>
+    View the newest version here:
+    <a><xsl:attribute name="href"><xsl:value-of select="$entityId"/>@HEAD</xsl:attribute>
+      <xsl:value-of select="$entityId"/>@HEAD
+    </a>
   </xsl:template>
+
   <xsl:template match="Successor" mode="entity-version-modal-successor">
-    <!-- content of the versioning window -->
+    <!-- content of the versioning info (not the full history) -->
     <xsl:param name="entityId"/>
-    <p class="caosdb-entity-heading-attr">
-      <em class="caosdb-entity-heading-attr-name">Next version:</em>
-      <a><xsl:attribute name="href"><xsl:value-of select="$entityId"/>@<xsl:value-of select="@id"/></xsl:attribute>
-        <xsl:value-of select="@id"/> (<xsl:value-of select="@date"/>)
-      </a>
-    </p>
+    <xsl:apply-templates mode="entity-version-modal-successor" select="Successor">
+      <xsl:with-param name="entityId" select="$entityId"/>
+    </xsl:apply-templates>
+    <xsl:apply-templates mode="entity-version-modal-single-history-item" select=".">
+      <xsl:with-param name="entityId" select="$entityId"/>
+      <xsl:with-param name="hint" select="'Newer Version'"/>
+    </xsl:apply-templates>
   </xsl:template>
+
   <xsl:template match="Version/Successor" mode="entity-action-panel-version">
     <!-- clickable warning message in the entity actions panel when there exists a newer version -->
     <xsl:param name="entityId"/>
@@ -604,13 +728,15 @@
       <strong>Warning</strong> A newer version exists!
     </a>
   </xsl:template>
+
   <xsl:template match="Version" mode="entity-version-marker">
     <!-- content of the data-version-id attribute -->
     <xsl:attribute name="data-version-id">
-        <xsl:value-of select="@id"/>
+      <xsl:value-of select="@id"/>
     </xsl:attribute>
     <xsl:apply-templates select="Successor" mode="entity-version-marker"/>
   </xsl:template>
+
   <xsl:template match="Successor" mode="entity-version-marker">
     <!-- content of the data-version-successor attribute
          This data-attribute marks entities which have a newer version.
@@ -619,4 +745,17 @@
       <xsl:value-of select="@id"/>
     </xsl:attribute>
   </xsl:template>
+
+  <!-- PERMISSIONS -->
+  <xsl:template match="Permissions" mode="entity-permissions">
+    <div style="display: none">
+      <xsl:apply-templates select="Permission" mode="entity-permissions"/>
+    </div>
+  </xsl:template>
+
+  <xsl:template match="Permission" mode="entity-permissions">
+    <div>
+      <xsl:attribute name="data-permission"><xsl:value-of select="@name"/></xsl:attribute>
+    </div>
+  </xsl:template>
 </xsl:stylesheet>
diff --git a/src/core/xsl/navbar.xsl b/src/core/xsl/navbar.xsl
index 4d83264e88bbcb27e77ca0973c4d6d788ab54ea1..6ce69e638efda10dbb95a066e8b59508854a4435 100644
--- a/src/core/xsl/navbar.xsl
+++ b/src/core/xsl/navbar.xsl
@@ -134,15 +134,15 @@
           <ul class="nav navbar-nav navbar-right">
             <li class="dropdown">
               <a class="dropdown-toggle" data-toggle="dropdown" href="#">
-                <span id="caosdb-f-bookmarks-collection-counter" class="badge">0</span> 
+                <span id="caosdb-f-bookmarks-collection-counter" class="badge">0</span>
                   Bookmarks
-                  <span class="caret"></span></a>
+                <span class="caret"></span></a>
               <ul class="dropdown-menu">
                 <li class="disabled" id="caosdb-f-bookmarks-collection-link"
                     title="Show all bookmarked entities.">
                   <a>Show all</a></li>
                 <li class="disabled" id="caosdb-f-bookmarks-export-link"
-                    title="Export all bookmarks to a file.">
+                    title="Export all bookmarks to a file. The exported file is a spread sheet with columns for the id, the version, the complete URI of the bookmarked entities and the path, if the entity is a file.">
                   <a>Export to file</a></li>
                 <li class="disabled" id="caosdb-f-bookmarks-clear"
                     title="Empty the list of bookmarks.">
diff --git a/src/core/xsl/query.xsl b/src/core/xsl/query.xsl
index ca1884aea16d59c7df92de304d3154698dc471bf..44d1c1bd6cff8f4b22138c1287af15713069ed79 100644
--- a/src/core/xsl/query.xsl
+++ b/src/core/xsl/query.xsl
@@ -149,7 +149,7 @@
             <thead>
               <tr>
                 <th></th>
-                <xsl:for-each select="Selector[@name!='id']">
+                <xsl:for-each select="Selector">
                   <th>
                     <xsl:value-of select="@name"/>
                   </th>
@@ -160,6 +160,8 @@
               <xsl:for-each select="/Response/*[@id]">
                 <xsl:call-template name="select-table-row">
                   <xsl:with-param name="entity-id" select="@id"/>
+                  <xsl:with-param name="version-id" select="Version/@id"/>
+                  <xsl:with-param name="ishead" select="Version/@head"/>
                 </xsl:call-template>
               </xsl:for-each>
             </tbody>
@@ -170,9 +172,14 @@
   </xsl:template>
   <xsl:template name="entity-link">
     <xsl:param name="entity-id"/>
+    <xsl:param name="version-id"/>
+    <xsl:param name="ishead"/>
     <a class="btn btn-default btn-sm caosdb-select-id">
       <xsl:attribute name="href">
         <xsl:value-of select="concat($entitypath, $entity-id)"/>
+        <xsl:if test="$version-id and not($ishead)">
+          <xsl:value-of select="concat('@', $version-id)"/>
+        </xsl:if>
       </xsl:attribute>
       <!-- <xsl:value-of select="$entity-id" /> -->
       <span class="caosdb-select-id-target">
@@ -183,18 +190,26 @@
 
   <xsl:template name="select-table-row">
     <xsl:param name="entity-id"/>
+    <xsl:param name="version-id"/>
+    <xsl:param name="ishead"/>
     <tr>
       <xsl:attribute name="data-entity-id">
         <xsl:value-of select="$entity-id"/>
       </xsl:attribute>
+      <xsl:attribute name="data-version-id">
+        <xsl:value-of select="$version-id"/>
+      </xsl:attribute>
       <td>
         <xsl:call-template name="entity-link">
           <xsl:with-param name="entity-id" select="$entity-id"/>
+          <xsl:with-param name="version-id" select="$version-id"/>
+          <xsl:with-param name="ishead" select="$ishead"/>
         </xsl:call-template>
       </td>
       <xsl:for-each select="/Response/Query/Selection/Selector">
         <xsl:call-template name="select-table-cell">
           <xsl:with-param name="entity-id" select="$entity-id"/>
+          <xsl:with-param name="version-id" select="$version-id"/>
           <xsl:with-param name="field-name" select="translate(@name, $uppercase, $lowercase)"/>
         </xsl:call-template>
       </xsl:for-each>
@@ -203,13 +218,15 @@
 
   <xsl:template name="select-table-cell">
     <xsl:param name="entity-id"/>
+    <xsl:param name="version-id"/>
     <xsl:param name="field-name"/>
     <td class="caosdb-f-entity-property">
       <xsl:attribute name="data-property-name">
         <xsl:value-of select="$field-name"/>
       </xsl:attribute>
       <div class="caosdb-f-property-value caosdb-v-property-value">
-        <xsl:apply-templates select="/Response/*[@id=$entity-id]" mode="walk-select-segments">
+        <xsl:apply-templates select="/Response/*[@id=$entity-id and Version/@id=$version-id]" mode="walk-select-segments">
+        <!--<xsl:apply-templates select="/Response/*[@id=$entity-id]" mode="walk-select-segments">-->
           <xsl:with-param name="first-segment">
             <xsl:value-of select="substring-before(concat($field-name, '.'), '.')"/>
           </xsl:with-param>
@@ -223,9 +240,28 @@
 
   <xsl:template match="Property" mode="walk-select-segments">
     <!-- handle properties -->
+    <xsl:param name="first-segment"/>
     <xsl:param name="next-segments"/>
 
     <xsl:choose>
+      <xsl:when test="@*[translate($first-segment, $uppercase, $lowercase)=name()]">
+        <!--handle attributes-->
+        <xsl:call-template name="single-value">
+          <xsl:with-param name="value">
+            <xsl:value-of select="@*[translate(name(), $uppercase, $lowercase)=$first-segment]"/>
+          </xsl:with-param>
+        </xsl:call-template>
+      </xsl:when>
+
+      <xsl:when test="translate($first-segment, $uppercase, $lowercase)='version'">
+        <!--handle version-->
+        <xsl:call-template name="single-value">
+          <xsl:with-param name="value">
+            <xsl:value-of select="Version/@id"/>
+          </xsl:with-param>
+        </xsl:call-template>
+      </xsl:when>
+
       <xsl:when test="$next-segments='value'">
         <!--handle value-->
         <xsl:apply-templates mode="property-value" select="."/>
@@ -274,6 +310,15 @@
         </xsl:call-template>
       </xsl:when>
 
+      <xsl:when test="translate($first-segment, $uppercase, $lowercase)='version'">
+        <!--handle version-->
+        <xsl:call-template name="single-value">
+          <xsl:with-param name="value">
+            <xsl:value-of select="Version/@id"/>
+          </xsl:with-param>
+        </xsl:call-template>
+      </xsl:when>
+
       <xsl:when test="$next-segments">
         <!-- when there is a next-segmenst -->
         <xsl:apply-templates select="Property[translate(@name, $uppercase, $lowercase)=$first-segment]" mode="walk-select-segments">
diff --git a/src/doc/Makefile b/src/doc/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..f3519f277badaf083c7f3512c64b18911ddf1f11
--- /dev/null
+++ b/src/doc/Makefile
@@ -0,0 +1,51 @@
+# ** header v3.0
+# This file is a part of the CaosDB Project.
+#
+# Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+# Copyright (C) 2020 Daniel Hornung <d.hornung@indiscale.com>
+#
+# 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
+
+# This Makefile is a wrapper for sphinx scripts.
+#
+# It is based upon the autocreated makefile for Sphinx documentation.
+
+# You can set these variables from the command line, and also
+# from the environment for the first two.
+SPHINXOPTS    ?= -a
+SPHINXBUILD   ?= sphinx-build
+# SPHINXAPIDOC  ?= javasphinx-apidoc
+SOURCEDIR      = .
+BUILDDIR       = ../../build/doc
+
+# npm is not always in the global PATH
+NPM_PATH = $(shell npm bin)
+NPM_PREFIX = $(shell npm prefix)
+
+.PHONY: doc-help Makefile api
+
+# Put it first so that "make" without argument is like "make help".
+doc-help:
+	@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile api
+	PATH=$(NPM_PATH):$$PATH $(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+#	sphinx-build -M html . ../../build/doc
+
+api:
+	PATH=$(NPM_PATH):$$PATH jsdoc -t $(NPM_PREFIX)/node_modules/jsdoc-sphinx/template -d $@ -r "../../src/core"
diff --git a/src/doc/concepts.rst b/src/doc/concepts.rst
new file mode 100644
index 0000000000000000000000000000000000000000..23e5fc4f6ddb666757fb9c79e192e07ffed8fb44
--- /dev/null
+++ b/src/doc/concepts.rst
@@ -0,0 +1,6 @@
+========================
+The concepts of pycaosdb
+========================
+
+Some text...
+
diff --git a/src/doc/conf.py b/src/doc/conf.py
new file mode 100644
index 0000000000000000000000000000000000000000..8e627dd0c26f9d760c549abfab4cbc824baa9912
--- /dev/null
+++ b/src/doc/conf.py
@@ -0,0 +1,212 @@
+# -*- coding: utf-8 -*-
+#
+# Configuration file for the Sphinx documentation builder.
+#
+# This file only contains a selection of the most common options. For a full
+# list see the documentation:
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
+
+# -- Path setup --------------------------------------------------------------
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+# import os
+# import sys
+# sys.path.insert(0, os.path.abspath('../caosdb'))
+
+
+# -- Project information -----------------------------------------------------
+
+import sphinx_rtd_theme
+
+project = 'caosdb-webui'
+copyright = '2020, IndiScale GmbH'
+author = 'Daniel Hornung'
+
+# The short X.Y version
+version = '0.X.Y'
+# The full version, including alpha/beta/rc tags
+release = '0.x.y-beta-rc2'
+
+
+# -- General configuration ---------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#
+# needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+#    'sphinx_js',
+    'sphinx.ext.todo',
+    "sphinx.ext.autodoc",
+#    'autoapi.extension',
+    "recommonmark",            # For markdown files.
+    "sphinx_rtd_theme",
+    'sphinx.ext.intersphinx',
+    'sphinx.ext.mathjax',
+    'sphinx.ext.ifconfig',
+    # 'sphinx.ext.napoleon',     # For Google style docstrings
+]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix(es) of source filenames.
+# You can specify multiple suffix as a list of string:
+#
+# source_suffix = ['.rst', '.md']
+source_suffix = '.rst'
+
+# The master toctree document.
+master_doc = 'index'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#
+# This is also used if you do content translation via gettext catalogs.
+# Usually you set "language" from the command line for these cases.
+language = None
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This pattern also affects html_static_path and html_extra_path.
+exclude_patterns = []
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = None
+
+
+# -- Options for HTML output -------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  See the documentation for
+# a list of builtin themes.
+#
+html_theme = "sphinx_rtd_theme"
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further.  For a list of options available for each theme, see the
+# documentation.
+#
+# html_theme_options = {}
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+# html_static_path = ['_static']
+
+# Custom sidebar templates, must be a dictionary that maps document names
+# to template names.
+#
+# The default sidebars (for documents that don't match any pattern) are
+# defined by theme itself.  Builtin themes are using these templates by
+# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
+# 'searchbox.html']``.
+#
+# html_sidebars = {}
+
+
+# -- Options for HTMLHelp output ---------------------------------------------
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'caosdb-webuidoc'
+
+
+# -- Options for LaTeX output ------------------------------------------------
+
+latex_elements = {
+    # The paper size ('letterpaper' or 'a4paper').
+    #
+    # 'papersize': 'letterpaper',
+
+    # The font size ('10pt', '11pt' or '12pt').
+    #
+    # 'pointsize': '10pt',
+
+    # Additional stuff for the LaTeX preamble.
+    #
+    # 'preamble': '',
+
+    # Latex figure (float) alignment
+    #
+    # 'figure_align': 'htbp',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+#  author, documentclass [howto, manual, or own class]).
+latex_documents = [
+    (master_doc, 'caosdb-webui.tex', 'caosdb-webui Documentation',
+     'IndiScale GmbH', 'manual'),
+]
+
+
+# -- Options for manual page output ------------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+    (master_doc, 'caosdb-webui', 'caosdb-webui Documentation',
+     [author], 1)
+]
+
+
+# -- Options for Texinfo output ----------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+#  dir menu entry, description, category)
+texinfo_documents = [
+    (master_doc, 'caosdb-webui', 'caosdb-webui Documentation',
+     author, 'caosdb-webui', 'One line description of project.',
+     'Miscellaneous'),
+]
+
+
+# -- Options for Epub output -------------------------------------------------
+
+# Bibliographic Dublin Core info.
+epub_title = project
+
+# The unique identifier of the text. This can be a ISBN number
+# or the project homepage.
+#
+# epub_identifier = ''
+
+# A unique identification for the text.
+#
+# epub_uid = ''
+
+# A list of files that should not be packed into the epub file.
+epub_exclude_files = ['search.html']
+
+
+# -- Extension configuration -------------------------------------------------
+
+# -- Options for sphinx-js ---------------------------------------------------
+# See also https://pypi.org/project/sphinx-js/
+
+js_source_path = '../core/js/'
+primary_domain = 'js'  # Not strictly necessary?
+
+# -- Options for intersphinx extension ---------------------------------------
+
+# Example configuration for intersphinx: refer to the Python standard library.
+intersphinx_mapping = {'https://docs.python.org/': None}
+
+# TODO Which options do we want?
+autodoc_default_options = {
+    'members': None,
+    'undoc-members': None,
+}
+
+# -- Options for sphinx-autoapi ----------------------------------------------
+# See also https://pypi.org/project/sphinx-js/
+
+autoapi_type = 'javascript'
+autoapi_dirs = ['../core/js/']
+autoapi_add_toctree_entry = False
diff --git a/src/doc/extension.rst b/src/doc/extension.rst
new file mode 100644
index 0000000000000000000000000000000000000000..18fd0f25a8ce75cd19edfa845b06f5c45bd3fd20
--- /dev/null
+++ b/src/doc/extension.rst
@@ -0,0 +1,13 @@
+
+Extending the CaosDB Web Interface
+==================================
+
+Here we collect information on how to extend the web interface as a developer.
+
+.. toctree::
+   :maxdepth: 1
+   :glob:
+
+   extension/*
+
+
diff --git a/src/doc/extension/forms.rst b/src/doc/extension/forms.rst
new file mode 100644
index 0000000000000000000000000000000000000000..1bced612b5f9517c5ec149871cbf53321b4671d4
--- /dev/null
+++ b/src/doc/extension/forms.rst
@@ -0,0 +1,80 @@
+
+Creating forms for the CaosDB Web Interface
+===========================================
+
+The ``form_elements`` module provides a library for generating forms from simple config objects. The forms are styled for the seamless integration into the CaosDB web interface and are especially useful for calling server side scripts.
+
+See also the :doc:`API documentation <../api/module-form_elements>`
+
+Examples
+--------
+
+Generating a generic form
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The following code snippet adds a form to the body of the HTML document.
+
+.. code-block:: javascript
+
+    function my_special_submit_handler (form) {
+        // handle form submision
+    };
+    const config = {
+        name: "my_form",
+        fields: [
+            { type: "reference_drop_down", name: "experiment_id", label: "Experiment", query: "FIND Record Experiment", required: true },
+            { type: "integer", name: "number", label: "A Number", required: true },
+            { type: "date", name: "date", label: "A Date", required: false },
+            { type: "text", name: "comment", label: "A Comment", required: false },
+        ],
+        submit: my_special_submit_handler
+    };
+    const form = form_elements.make_form(config);
+    $("body").append(form);
+
+The form has four fields:
+
+    1. A drop-down menu which contains all Records of type "Experiment" as options,
+    2. an integer field, labeled "A Number",
+    3. a date field, labeled "A Date", and
+    4. a text field, labeled "A Comment".
+
+The first two fields are required and the form cannot be submitted without it. The latter are optional.
+
+On submission, the function ``my_special_submit_handler`` is being called with the form element as only parameter.
+
+As the generated form is a plain HTML form, the javascript form API can be used. However, there are special methods in the ``form_elements`` module e.g. :doc:`get_fields <../api/module-form_elements>` which are especially designed to interact with the forms generated by the ``make_form`` factory.
+
+Calling a server-side script
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+If you intend to call a server-side script, the config has to be changed a litte bit and the script calling is done by the ``form_elements`` module. There is no need to define the submit_hander anymore. Instead, just name the script which is to be called.
+
+.. code-block:: javascript
+
+    const config = {
+        script: "process.py",
+        fields: [
+            { type: "reference_drop_down", name: "experiment_id", label: "Experiment", query: "FIND Record Experiment", required: true },
+            { type: "integer", name: "number", label: "A Number", required: true },
+            { type: "date", name: "date", label: "A Date", required: false },
+            { type: "text", name: "comment", label: "A Comment", required: false },
+        ],
+    };
+    const form = form_elements.make_form(config);
+    $("body").append(form);
+
+On submission, the form data will be send as a json file to the script and passed as the first parameter. The call would look like ``./process.py form.json`` and the file would contain, for example,
+
+.. code-block:: json
+
+    {
+      "experiment_id": "234234",
+      "number": "400",
+      "date": "2020-12-24",
+      "comment": "This is a comment",
+    }
+
+For more and advanced options for the form see the :doc:`API documentation <../api/module-form_elements>`
+
+
diff --git a/src/doc/genindex.rst b/src/doc/genindex.rst
new file mode 100644
index 0000000000000000000000000000000000000000..48ab71fd283bb48564ac30e4c69e62bbd463cd77
--- /dev/null
+++ b/src/doc/genindex.rst
@@ -0,0 +1,4 @@
+.. This file is a placeholder and will be replaced.
+
+Index
+=====
diff --git a/src/doc/getting_started.md b/src/doc/getting_started.md
new file mode 120000
index 0000000000000000000000000000000000000000..88332e357f5e06f3de522768ccdcd9e513c15f62
--- /dev/null
+++ b/src/doc/getting_started.md
@@ -0,0 +1 @@
+../../README_SETUP.md
\ No newline at end of file
diff --git a/src/doc/index.rst b/src/doc/index.rst
new file mode 100644
index 0000000000000000000000000000000000000000..107c9052fd6cdafecd201eb17118d8e56f3da440
--- /dev/null
+++ b/src/doc/index.rst
@@ -0,0 +1,25 @@
+
+Welcome to the documentation of CaosDB's web UI!
+================================================
+
+.. toctree::
+   :maxdepth: 2
+   :caption: Contents:
+   :hidden:
+
+   Getting started <getting_started>
+   Tutorials <tutorials/index>
+   Concepts <concepts>
+   Extending the UI <extension>
+   API <api/index>
+
+
+This documentation helps you to :doc:`get started<getting_started>`, explains the most important
+:doc:`concepts<concepts>` and offers a range of :doc:`tutorials<tutorials>`.
+
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`search`
diff --git a/src/doc/tutorials/first_steps.rst b/src/doc/tutorials/first_steps.rst
new file mode 100644
index 0000000000000000000000000000000000000000..48126b2aab6175da557919133493585eda59bbfa
--- /dev/null
+++ b/src/doc/tutorials/first_steps.rst
@@ -0,0 +1,87 @@
+First Steps
+===========
+
+Before using or even manipulating data stored in CaosDB, it is important to 
+understand the way data is structured. Here, we will briefly look at this 
+structure. You can find more details here_. In CaosDB data is stored in objects called 
+`Records`. A `Record` can have multiple `Properties`, like numbers, text or references
+to other `Records`. `RecordTypes` are kind of blue prints for `Records` and 
+provide a structure to the data. Let's look at an example:
+
+.. image:: model.svg
+
+.. The image is not good yet. Children should have properties of parents.
+
+This illustrates a simple data model used in the `demo instance`_ provided by `IndiScale`_.
+It shows that the `RecordType` Analysis has among others the `Properties` 
+`quality_factor`, a number, and `date`, you guessed it... a date. The `Property`
+`MusicalInstrumet` illustrates that a `Record` that has `Analysis` as a parent 
+`RecordType` should reference a `Record` that has the `MusicalInstrumet` `RecordType` as a parent.
+
+We recommend that you connect to the demo instance in order to try out the following
+examples (see :doc:`Getting Started secton</getting_started>`.). However, you
+can also translate the examples to the data model that you have at hand.
+
+
+
+Main Menu (WIP)
+---------------
+
+
+.. note::  
+   By default only 10 Entities are shown on one page. You can get to
+   other pages with the “Next Page” and “Previous Page” buttons.
+
+:math:`\Rightarrow` What are the differences between the options of the
+“Entities” menu?
+
+Entities, Records, Properties…What?
+
+
+-  semantic data modeling
+
+-  entries in LinkAhead are like Objects
+
+-  RecordType: blue print for data
+
+-  Record: actual data
+
+
+See also the
+`wiki <https://gitlab.com/caosdb/caosdb/wikis/Concepts/Data%20Model>`__
+or the `paper <https://www.mdpi.com/2306-5729/4/2/83>`__
+
+|image|
+
+References in two directions
+
+-  | References in LinkAhead are directed:
+   | A Record A references another Record B
+
+-  The referencing Record A has a corresponding Property.
+
+-  The referenced Record B does not.
+
+-  In order to get referencing Records in the Web Interface, click on the following button
+    (or “Backref” on older systems).
+
+|image1|
+
+File System
+-----------
+
+-  Clicking on “File System” in the main menu allows you to browse files
+   that LinkAhead knows about.
+
+-  Typically, most files will be mounted from some file server.
+
+.. note::   You will not find any Records in this view (that are not Files).
+
+
+
+.. _here: https://gitlabio.something
+.. _`demo instance`: https://demo.indiscale.com
+.. _`IndiScale`: https://indiscale.com
+.. |image| image:: model.svg
+.. |image1| image:: References_button.png
+   :width: 4em
diff --git a/src/doc/tutorials/index.rst b/src/doc/tutorials/index.rst
new file mode 100644
index 0000000000000000000000000000000000000000..dbfeafed2a09d587ebd538a62ed9002943b52aa0
--- /dev/null
+++ b/src/doc/tutorials/index.rst
@@ -0,0 +1,11 @@
+
+CaosDB Web Interface Tutorials
+==============================
+
+This chapter contains the following tutorials:
+
+.. toctree::
+   :maxdepth: 2
+   :glob:
+
+   *
diff --git a/src/doc/tutorials/model.svg b/src/doc/tutorials/model.svg
new file mode 100644
index 0000000000000000000000000000000000000000..2602cb43f15976305d48e6f2d5efeb3821e1d669
--- /dev/null
+++ b/src/doc/tutorials/model.svg
@@ -0,0 +1,632 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   contentScriptType="application/ecmascript"
+   contentStyleType="text/css"
+   height="502"
+   preserveAspectRatio="none"
+   version="1.1"
+   viewBox="0 0 407 502"
+   width="407"
+   zoomAndPan="magnify"
+   id="svg233"
+   sodipodi:docname="model.svg"
+   inkscape:version="0.92.4 5da689c313, 2019-01-14">
+  <metadata
+     id="metadata237">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="1920"
+     inkscape:window-height="1043"
+     id="namedview235"
+     showgrid="false"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0"
+     inkscape:zoom="1.1817368"
+     inkscape:cx="112.55875"
+     inkscape:cy="257"
+     inkscape:window-x="1920"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg233" />
+  <defs
+     id="defs11">
+    <filter
+       height="3"
+       id="f64vrt8w3qxjw"
+       width="3"
+       x="-1"
+       y="-1">
+      <feGaussianBlur
+         result="blurOut"
+         stdDeviation="2.0"
+         id="feGaussianBlur2" />
+      <feColorMatrix
+         in="blurOut"
+         result="blurOut2"
+         type="matrix"
+         values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .4 0"
+         id="feColorMatrix4" />
+      <feOffset
+         dx="4.0"
+         dy="4.0"
+         in="blurOut2"
+         result="blurOut3"
+         id="feOffset6" />
+      <feBlend
+         in="SourceGraphic"
+         in2="blurOut3"
+         mode="normal"
+         id="feBlend8" />
+    </filter>
+  </defs>
+  <rect
+     style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.13385832;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+     id="rect4867"
+     width="407"
+     height="502"
+     x="0"
+     y="0" />
+  <polygon
+     id="polygon13"
+     style="fill:#dddddd;stroke:#000000;stroke-width:1.5;filter:url(#f64vrt8w3qxjw)"
+     points="533.5,526 126.5,526 126.5,24 236.5,24 243.5,46.2969 533.5,46.2969 "
+     transform="translate(-126.5,-24)" />
+  <line
+     id="line15"
+     y2="22.296902"
+     y1="22.296902"
+     x2="117"
+     x1="0"
+     style="stroke:#000000;stroke-width:1.5" />
+  <text
+     style="font-weight:bold;font-size:14px;font-family:sans-serif;fill:#000000"
+     id="text17"
+     y="38.995098"
+     x="130.5"
+     textLength="104"
+     lengthAdjust="spacingAndGlyphs"
+     font-weight="bold"
+     font-size="14"
+     transform="translate(-126.5,-24)">RecordTypes</text>
+  <text
+     style="font-size:11px;font-family:sans-serif;fill:#000000"
+     id="text42"
+     y="144.7104"
+     x="461"
+     textLength="0"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="11"
+     transform="translate(-126.5,-24)" />
+  <rect
+     y="411"
+     x="16"
+     width="116"
+     style="fill:#fefece;stroke:#a80036;stroke-width:1.5;filter:url(#f64vrt8w3qxjw)"
+     id="Manufacturer"
+     height="60.804699" />
+  <circle
+     r="11"
+     id="ellipse47"
+     style="fill:#ff1111;stroke:#a80036;stroke-width:1"
+     cy="427"
+     cx="31" />
+  <path
+     inkscape:connector-curvature="0"
+     id="path49"
+     d="m 33.9688,432.6406 q -0.5782,0.2969 -1.2188,0.4375 -0.6406,0.1563 -1.3437,0.1563 -2.5,0 -3.8282,-1.6406 -1.3125,-1.6563 -1.3125,-4.7813 0,-3.125 1.3125,-4.7812 1.3282,-1.6563 3.8282,-1.6563 0.7031,0 1.3437,0.1563 0.6563,0.1562 1.2188,0.4531 v 2.7187 q -0.625,-0.5781 -1.2188,-0.8437 -0.5937,-0.2813 -1.2187,-0.2813 -1.3438,0 -2.0313,1.0782 -0.6875,1.0625 -0.6875,3.1562 0,2.0938 0.6875,3.1719 0.6875,1.0625 2.0313,1.0625 0.625,0 1.2187,-0.2656 0.5938,-0.2813 1.2188,-0.8594 z" />
+  <text
+     style="font-size:12px;font-family:sans-serif;fill:#000000"
+     id="text51"
+     y="455.1543"
+     x="171.5"
+     textLength="84"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="12"
+     transform="translate(-126.5,-24)">Manufacturer</text>
+  <line
+     id="line53"
+     y2="443"
+     y1="443"
+     x2="131"
+     x1="17"
+     style="stroke:#a80036;stroke-width:1.5" />
+  <text
+     style="font-size:11px;font-family:sans-serif;fill:#000000"
+     id="text55"
+     y="481.21039"
+     x="152.5"
+     textLength="0"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="11"
+     transform="translate(-126.5,-24)" />
+  <line
+     id="line57"
+     y2="463.80469"
+     y1="463.80469"
+     x2="131"
+     x1="17"
+     style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" />
+  <rect
+     y="235"
+     x="16"
+     width="174"
+     style="fill:#fefece;stroke:#a80036;stroke-width:1.5;filter:url(#f64vrt8w3qxjw)"
+     id="MusicalInstrument"
+     height="101.6211" />
+  <circle
+     r="11"
+     id="ellipse60"
+     style="fill:#ff1111;stroke:#a80036;stroke-width:1"
+     cy="251"
+     cx="43.600006" />
+  <path
+     inkscape:connector-curvature="0"
+     id="path62"
+     d="m 46.5688,256.6406 q -0.5782,0.2969 -1.2188,0.4375 -0.6406,0.1563 -1.3437,0.1563 -2.5,0 -3.8282,-1.6406 -1.3125,-1.6563 -1.3125,-4.7813 0,-3.125 1.3125,-4.7812 1.3282,-1.6563 3.8282,-1.6563 0.7031,0 1.3437,0.1563 0.6563,0.1562 1.2188,0.4531 v 2.7187 q -0.625,-0.5781 -1.2188,-0.8437 -0.5937,-0.2813 -1.2187,-0.2813 -1.3438,0 -2.0313,1.0782 -0.6875,1.0625 -0.6875,3.1562 0,2.0938 0.6875,3.1719 0.6875,1.0625 2.0313,1.0625 0.625,0 1.2187,-0.2656 0.5938,-0.2813 1.2188,-0.8594 z" />
+  <text
+     style="font-size:12px;font-family:sans-serif;fill:#000000"
+     id="text64"
+     y="279.1543"
+     x="186.89999"
+     textLength="114"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="12"
+     transform="translate(-126.5,-24)">MusicalInstrument</text>
+  <line
+     id="line66"
+     y2="267"
+     y1="267"
+     x2="189"
+     x1="17"
+     style="stroke:#a80036;stroke-width:1.5" />
+  <line
+     id="line68"
+     y2="281.40231"
+     y1="281.40231"
+     x2="73.5"
+     x1="17"
+     style="stroke:#a80036;stroke-width:1.5" />
+  <text
+     style="font-size:11px;font-family:sans-serif;fill:#000000"
+     id="text70"
+     y="308.71039"
+     x="200"
+     textLength="59"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="11"
+     transform="translate(-126.5,-24)">Properties</text>
+  <line
+     id="line72"
+     y2="281.40231"
+     y1="281.40231"
+     x2="189"
+     x1="132.5"
+     style="stroke:#a80036;stroke-width:1.5" />
+  <text
+     style="font-size:11px;font-family:sans-serif;fill:#000000"
+     id="text74"
+     y="341.2222"
+     x="148.5"
+     textLength="86"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="11"
+     transform="translate(-126.5,-24)">price (DOUBLE)</text>
+  <text
+     style="font-size:11px;font-family:sans-serif;fill:#000000"
+     id="text76"
+     y="354.02689"
+     x="148.5"
+     textLength="162"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="11"
+     transform="translate(-126.5,-24)">Manufacturer (Manufacturer)</text>
+  <line
+     id="line78"
+     y2="300.60941"
+     y1="300.60941"
+     x2="62"
+     x1="17"
+     style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" />
+  <text
+     style="font-size:11px;font-family:sans-serif;fill:#000000"
+     id="text80"
+     y="327.91751"
+     x="188.5"
+     textLength="82"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="11"
+     transform="translate(-126.5,-24)">recommended</text>
+  <line
+     id="line82"
+     y2="300.60941"
+     y1="300.60941"
+     x2="189"
+     x1="144"
+     style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" />
+  <rect
+     y="411"
+     x="167.5"
+     width="65"
+     style="fill:#fefece;stroke:#a80036;stroke-width:1.5;filter:url(#f64vrt8w3qxjw)"
+     id="Violin"
+     height="60.804699" />
+  <circle
+     r="11"
+     id="ellipse85"
+     style="fill:#ff1111;stroke:#a80036;stroke-width:1"
+     cy="427"
+     cx="182.5" />
+  <path
+     inkscape:connector-curvature="0"
+     id="path87"
+     d="m 185.4688,432.6406 q -0.5782,0.2969 -1.2188,0.4375 -0.6406,0.1563 -1.3437,0.1563 -2.5,0 -3.8282,-1.6406 -1.3125,-1.6563 -1.3125,-4.7813 0,-3.125 1.3125,-4.7812 1.3282,-1.6563 3.8282,-1.6563 0.7031,0 1.3437,0.1563 0.6563,0.1562 1.2188,0.4531 v 2.7187 q -0.625,-0.5781 -1.2188,-0.8437 -0.5937,-0.2813 -1.2187,-0.2813 -1.3438,0 -2.0313,1.0782 -0.6875,1.0625 -0.6875,3.1562 0,2.0938 0.6875,3.1719 0.6875,1.0625 2.0313,1.0625 0.625,0 1.2187,-0.2656 0.5938,-0.2813 1.2188,-0.8594 z" />
+  <text
+     style="font-size:12px;font-family:sans-serif;fill:#000000"
+     id="text89"
+     y="455.1543"
+     x="323"
+     textLength="33"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="12"
+     transform="translate(-126.5,-24)">Violin</text>
+  <line
+     id="line91"
+     y2="443"
+     y1="443"
+     x2="231.5"
+     x1="168.5"
+     style="stroke:#a80036;stroke-width:1.5" />
+  <text
+     style="font-size:11px;font-family:sans-serif;fill:#000000"
+     id="text93"
+     y="481.21039"
+     x="304"
+     textLength="0"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="11"
+     transform="translate(-126.5,-24)" />
+  <line
+     id="line95"
+     y2="463.80469"
+     y1="463.80469"
+     x2="231.5"
+     x1="168.5"
+     style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" />
+  <rect
+     y="397"
+     x="267.5"
+     width="119"
+     style="fill:#fefece;stroke:#a80036;stroke-width:1.5;filter:url(#f64vrt8w3qxjw)"
+     id="Guitar"
+     height="88.816399" />
+  <circle
+     r="11"
+     id="ellipse98"
+     style="fill:#ff1111;stroke:#a80036;stroke-width:1"
+     cy="413"
+     cx="304.54999" />
+  <path
+     inkscape:connector-curvature="0"
+     id="path100"
+     d="m 307.5188,418.6406 q -0.5782,0.2969 -1.2188,0.4375 -0.6406,0.1563 -1.3437,0.1563 -2.5,0 -3.8282,-1.6406 -1.3125,-1.6563 -1.3125,-4.7813 0,-3.125 1.3125,-4.7812 1.3282,-1.6563 3.8282,-1.6563 0.7031,0 1.3437,0.1563 0.6563,0.1562 1.2188,0.4531 v 2.7187 q -0.625,-0.5781 -1.2188,-0.8437 -0.5937,-0.2813 -1.2187,-0.2813 -1.3438,0 -2.0313,1.0782 -0.6875,1.0625 -0.6875,3.1562 0,2.0938 0.6875,3.1719 0.6875,1.0625 2.0313,1.0625 0.625,0 1.2187,-0.2656 0.5938,-0.2813 1.2188,-0.8594 z" />
+  <text
+     style="font-size:12px;font-family:sans-serif;fill:#000000"
+     id="text102"
+     y="441.1543"
+     x="449.95001"
+     textLength="38"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="12"
+     transform="translate(-126.5,-24)">Guitar</text>
+  <line
+     id="line104"
+     y2="429"
+     y1="429"
+     x2="385.5"
+     x1="268.5"
+     style="stroke:#a80036;stroke-width:1.5" />
+  <line
+     id="line106"
+     y2="443.40231"
+     y1="443.40231"
+     x2="297.5"
+     x1="268.5"
+     style="stroke:#a80036;stroke-width:1.5" />
+  <text
+     style="font-size:11px;font-family:sans-serif;fill:#000000"
+     id="text108"
+     y="470.71039"
+     x="424"
+     textLength="59"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="11"
+     transform="translate(-126.5,-24)">Properties</text>
+  <line
+     id="line110"
+     y2="443.40231"
+     y1="443.40231"
+     x2="385.5"
+     x1="356.5"
+     style="stroke:#a80036;stroke-width:1.5" />
+  <text
+     style="font-size:11px;font-family:sans-serif;fill:#000000"
+     id="text112"
+     y="503.2222"
+     x="400"
+     textLength="107"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="11"
+     transform="translate(-126.5,-24)">electric (BOOLEAN)</text>
+  <line
+     id="line114"
+     y2="462.60941"
+     y1="462.60941"
+     x2="286"
+     x1="268.5"
+     style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" />
+  <text
+     style="font-size:11px;font-family:sans-serif;fill:#000000"
+     id="text116"
+     y="489.91751"
+     x="412.5"
+     textLength="82"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="11"
+     transform="translate(-126.5,-24)">recommended</text>
+  <line
+     id="line118"
+     y2="462.60941"
+     y1="462.60941"
+     x2="385.5"
+     x1="368"
+     style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" />
+  <rect
+     y="255.5"
+     x="225.5"
+     width="165"
+     style="fill:#fefece;stroke:#a80036;stroke-width:1.5;filter:url(#f64vrt8w3qxjw)"
+     id="SoundQualityAnalyzer"
+     height="60.804699" />
+  <circle
+     r="11"
+     id="ellipse121"
+     style="fill:#ff1111;stroke:#a80036;stroke-width:1"
+     cy="271.5"
+     cx="240.5" />
+  <path
+     inkscape:connector-curvature="0"
+     id="path123"
+     d="m 243.4688,277.1406 q -0.5782,0.2969 -1.2188,0.4375 -0.6406,0.1563 -1.3437,0.1563 -2.5,0 -3.8282,-1.6406 -1.3125,-1.6563 -1.3125,-4.7813 0,-3.125 1.3125,-4.7812 1.3282,-1.6563 3.8282,-1.6563 0.7031,0 1.3437,0.1563 0.6563,0.1562 1.2188,0.4531 v 2.7187 q -0.625,-0.5781 -1.2188,-0.8437 -0.5937,-0.2813 -1.2187,-0.2813 -1.3438,0 -2.0313,1.0782 -0.6875,1.0625 -0.6875,3.1562 0,2.0938 0.6875,3.1719 0.6875,1.0625 2.0313,1.0625 0.625,0 1.2187,-0.2656 0.5938,-0.2813 1.2188,-0.8594 z" />
+  <text
+     style="font-size:12px;font-family:sans-serif;fill:#000000"
+     id="text125"
+     y="299.6543"
+     x="381"
+     textLength="133"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="12"
+     transform="translate(-126.5,-24)">SoundQualityAnalyzer</text>
+  <line
+     id="line127"
+     y2="287.5"
+     y1="287.5"
+     x2="389.5"
+     x1="226.5"
+     style="stroke:#a80036;stroke-width:1.5" />
+  <text
+     style="font-size:11px;font-family:sans-serif;fill:#000000"
+     id="text129"
+     y="325.71039"
+     x="362"
+     textLength="0"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="11"
+     transform="translate(-126.5,-24)" />
+  <line
+     id="line131"
+     y2="308.30469"
+     y1="308.30469"
+     x2="389.5"
+     x1="226.5"
+     style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" />
+  <rect
+     y="35"
+     x="20"
+     width="268"
+     style="fill:#fefece;stroke:#a80036;stroke-width:1.5;filter:url(#f64vrt8w3qxjw)"
+     id="Analysis"
+     height="140.0352" />
+  <circle
+     r="11"
+     id="ellipse134"
+     style="fill:#ff1111;stroke:#a80036;stroke-width:1"
+     cy="51"
+     cx="124.75" />
+  <path
+     inkscape:connector-curvature="0"
+     id="path136"
+     d="m 127.7188,56.6406 q -0.5782,0.2969 -1.2188,0.4375 -0.6406,0.1563 -1.3437,0.1563 -2.5,0 -3.8282,-1.6406 -1.3125,-1.6563 -1.3125,-4.7813 0,-3.125 1.3125,-4.7812 1.3282,-1.6563 3.8282,-1.6563 0.7031,0 1.3437,0.1563 0.6563,0.1562 1.2188,0.4531 v 2.7187 q -0.625,-0.5781 -1.2188,-0.8437 -0.5937,-0.2813 -1.2187,-0.2813 -1.3438,0 -2.0313,1.0782 -0.6875,1.0625 -0.6875,3.1562 0,2.0938 0.6875,3.1719 0.6875,1.0625 2.0313,1.0625 0.625,0 1.2187,-0.2656 0.5938,-0.2813 1.2188,-0.8594 z" />
+  <text
+     style="font-size:12px;font-family:sans-serif;fill:#000000"
+     id="text138"
+     y="79.154297"
+     x="271.75"
+     textLength="50"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="12"
+     transform="translate(-126.5,-24)">Analysis</text>
+  <line
+     id="line140"
+     y2="67"
+     y1="67"
+     x2="287"
+     x1="21"
+     style="stroke:#a80036;stroke-width:1.5" />
+  <line
+     id="line142"
+     y2="81.402298"
+     y1="81.402298"
+     x2="124.5"
+     x1="21"
+     style="stroke:#a80036;stroke-width:1.5" />
+  <text
+     style="font-size:11px;font-family:sans-serif;fill:#000000"
+     id="text144"
+     y="108.7104"
+     x="251"
+     textLength="59"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="11"
+     transform="translate(-126.5,-24)">Properties</text>
+  <line
+     id="line146"
+     y2="81.402298"
+     y1="81.402298"
+     x2="287"
+     x1="183.5"
+     style="stroke:#a80036;stroke-width:1.5" />
+  <text
+     style="font-size:11px;font-family:sans-serif;fill:#000000"
+     id="text148"
+     y="141.2222"
+     x="152.5"
+     textLength="134"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="11"
+     transform="translate(-126.5,-24)">quality_factor (DOUBLE)</text>
+  <text
+     style="font-size:11px;font-family:sans-serif;fill:#000000"
+     id="text150"
+     y="154.0269"
+     x="152.5"
+     textLength="92"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="11"
+     transform="translate(-126.5,-24)">date (DATETIME)</text>
+  <text
+     style="font-size:11px;font-family:sans-serif;fill:#000000"
+     id="text152"
+     y="166.8315"
+     x="152.5"
+     textLength="111"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="11"
+     transform="translate(-126.5,-24)">report (REFERENCE)</text>
+  <text
+     style="font-size:11px;font-family:sans-serif;fill:#000000"
+     id="text154"
+     y="179.6362"
+     x="152.5"
+     textLength="256"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="11"
+     transform="translate(-126.5,-24)">SoundQualityAnalyzer (SoundQualityAnalyzer)</text>
+  <text
+     style="font-size:11px;font-family:sans-serif;fill:#000000"
+     id="text156"
+     y="192.4409"
+     x="152.5"
+     textLength="220"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="11"
+     transform="translate(-126.5,-24)">MusicalInstrument (MusicalInstrument)</text>
+  <line
+     id="line158"
+     y2="100.6094"
+     y1="100.6094"
+     x2="113"
+     x1="21"
+     style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" />
+  <text
+     style="font-size:11px;font-family:sans-serif;fill:#000000"
+     id="text160"
+     y="127.9175"
+     x="239.5"
+     textLength="82"
+     lengthAdjust="spacingAndGlyphs"
+     font-size="11"
+     transform="translate(-126.5,-24)">recommended</text>
+  <line
+     id="line162"
+     y2="100.6094"
+     y1="100.6094"
+     x2="287"
+     x1="195"
+     style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" />
+  <path
+     inkscape:connector-curvature="0"
+     style="fill:none;stroke:#a80036;stroke-width:1"
+     id="MusicalInstrument-Violin"
+     d="m 145.51,354.27 c 12.48,19.76 25.51,40.37 35.69,56.48" />
+  <polygon
+     id="polygon211"
+     style="fill:none;stroke:#a80036;stroke-width:1"
+     points="261.26,361.26 277.86,374.43 266.03,381.91 "
+     transform="translate(-126.5,-24)" />
+  <path
+     inkscape:connector-curvature="0"
+     style="fill:none;stroke:#a80036;stroke-width:1"
+     id="MusicalInstrument-Guitar"
+     d="m 192.64,348.42 c 25.04,17.17 51.64,35.39 74.51,51.06" />
+  <polygon
+     id="polygon214"
+     style="fill:none;stroke:#a80036;stroke-width:1"
+     points="302.54,361.05 322.99,366.58 315.08,378.13 "
+     transform="translate(-126.5,-24)" />
+  <path
+     inkscape:connector-curvature="0"
+     style="fill:none;stroke:#a80036;stroke-width:1"
+     id="MusicalInstrument-Manufacturer"
+     d="m 91.08,350.09 c -3.97,21 -8.2,43.41 -11.46,60.66" />
+  <polygon
+     id="polygon217"
+     style="fill:#a80036;stroke:#a80036;stroke-width:1"
+     points="220,361.26 214.9551,366.4126 217.771,373.0512 222.8159,367.8986 "
+     transform="translate(-126.5,-24)" />
+  <path
+     inkscape:connector-curvature="0"
+     style="fill:none;stroke:#a80036;stroke-width:1"
+     id="Analysis-SoundQualityAnalyzer"
+     d="m 222.3,185.39 c 21.35,24.82 43.61,50.69 60.09,69.84" />
+  <polygon
+     id="polygon220"
+     style="fill:#a80036;stroke:#a80036;stroke-width:1"
+     points="340.04,199.21 340.9231,206.3668 347.869,208.3043 346.9859,201.1475 "
+     transform="translate(-126.5,-24)" />
+  <path
+     inkscape:connector-curvature="0"
+     style="fill:none;stroke:#a80036;stroke-width:1"
+     id="Analysis-MusicalInstrument"
+     d="m 130.66,187.93 c -4.56,16 -9.21,32.33 -13.37,46.92" />
+  <polygon
+     id="polygon223"
+     style="fill:#a80036;stroke:#a80036;stroke-width:1"
+     points="260.78,199.21 255.287,203.8819 257.4868,210.7493 262.9798,206.0774 "
+     transform="translate(-126.5,-24)" />
+</svg>
diff --git a/src/doc/tutorials/query.rst b/src/doc/tutorials/query.rst
new file mode 100644
index 0000000000000000000000000000000000000000..29d998cc1dbce5c5e17ae4cf9f60176b9f88861a
--- /dev/null
+++ b/src/doc/tutorials/query.rst
@@ -0,0 +1,143 @@
+
+Querying CaosDB
+===============
+
+You should have the web interface of a CaosDB instance at hand. If you do not
+have one, you can visit https://demo.indiscale.com
+
+Introduction
+------------
+
+The semantic data model of CaosDB allows efficient data access. The
+CaosDB Query Language (CQL) is used to search data. Queries can be entered in
+the webinterface under the respective menu entry.
+
+Let's start with a simple one::
+
+    FIND RECORD MusicalInstrument
+
+Most queries simply start with the ``FIND`` keyword and describe what we are
+looking for behind that. The ``RECORD`` keyword denotes that we are only looking
+for Records (and not Files, Properties or RecordTypes). Finally, we provided
+a RecordType name: MusicalInstrument. This means that we will get all Records
+that have this RecordType as parent. Try it out!
+
+Let's look at::
+
+    FIND Guitar
+
+When we leave out the ``RECORD`` keyword, we will get every entity that is a
+Guitar. When you submit this query you should find also a RecordType Guitar
+in the results. Using  ``FIND RecordType Guitar`` would restrict the result to
+only that RecordType.
+
+Note, that you cannot only provide RecordType names after the ``FIND``, but names
+in general: ``FIND RECORD Nice Guitar``. This will give you a Record with the
+name "Nice Guitar" (if one exists... and there should be one in the demo instance).
+
+While it does not matter whether you use capital letters or not, the names have to
+be exact. There are two features that make it easy to use names for querying
+in spite of this:
+- You can use "*" to match any string. E.g. ``FIND RECORD Nice*``
+- After typing three letters, names that start with those three are
+suggested by the auto completion.
+
+.. note::
+
+   Train yourself by trying to guess what the result will be before
+   actually executing the query.
+
+
+Searching Data  Using Properties
+--------------------------------
+
+Looking for entities with certain names or such that have certain parents is
+nice. However, the queries become really useful if we can impose further conditions
+on the results. Let's start with an example again::
+
+    FIND Guitar with price > 10000
+
+This should list expensive guitars where are in the demo instance. Thus,
+we are using a property (the price) of the Guitar Records to restrict the
+result set. In general this looks like::
+
+    FIND <Name> <Property Filter>
+
+Typically, the filter has the form ``<Property> <Operator> <Value>``,
+for example ``length >= 0.7mm``.
+There are many filters available. You can check the specification for a comprehensive description of
+those. Here, we will only look at the most common examples.
+
+
+If you only want to assure that Records have a certain Property, without imposing
+constrains on the value, you can use::
+
+   FIND RECORD MusicalInstrument WITH Manufacturer
+
+
+Similarly, to what we saw above when using incomplete names, you can use a "*"
+to match parts of text properties::
+
+     FIND RECORD WITH serialNumber like KN*
+
+There is large number of operators that can be used together with dates or
+timestamps. One of the most useful is probably::
+
+    FIND RECORD WITH date in 2019
+
+A lot of valuable information is often stored in the relations among data, i.e. in
+the references of entities. So how can we use those?::
+
+    FIND RECORD WHICH REFERENCES A Guitar
+
+This should be pretty self explanatory. And it is also possible to check for
+references in the other direction::
+
+     FIND RECORD WHICH IS REFERENCED BY A Analysis
+
+You can also simply provide the ID of the entity::
+
+   FIND RECORD WHICH IS REFERENCED BY 123``
+
+
+Using Multiple Filters
+----------------------
+
+Often, one condition is not sufficient. Thus multiple filters/conditions can be combined.
+This can for example be done using the following structure::
+
+    FIND <Name> <Property Filter> (AND|OR) <Property Filter>
+
+An example would be::
+
+    FIND Guitar WITH price>48 AND electric=TRUE
+
+Furthermore, reference conditions can be nested::
+
+    FIND <Name> WHICH REFERENCES <Name> WHICH REFERENCES <Name>
+
+
+For example::
+
+    FIND Manufacturer WHICH IS REFERENCED BY Guitar WHICH IS REFERENCED BY Analysis
+
+
+Restricting Result Information
+------------------------------
+
+Using ``COUNT`` instead of ``FIND`` will only return the number of
+entities in the result set.
+
+.. note::  This is often useful when experimenting with queries.
+
+Using ``SELECT ... FROM`` instead of ``FIND`` returns specific
+information in a table. A comma separated list of Property names can be provided behind the
+``SELECT`` keyword::
+
+   SELECT price, electric FROM Guitar
+
+Or::
+
+   SELECT quality_factor, report, date FROM  Analysis WHICH REFERENCES A Guitar WITH electric=TRUE
+
+
diff --git a/src/ext/js/fileupload.js b/src/ext/js/fileupload.js
index e21ccf7f6035a8170bd1a0f4c7f5868d56c83b37..1c069f2a8fdded69a2b7cc93f27601226e1d149c 100644
--- a/src/ext/js/fileupload.js
+++ b/src/ext/js/fileupload.js
@@ -235,8 +235,6 @@ var fileupload = new function() {
     }
 
     this.init = function() {
-        fileupload.debug("init");
-
         // add global listener for start_edit event
         document.body.addEventListener(edit_mode.start_edit.type, function(e) {
             $(e.target).find(".caosdb-properties .caosdb-f-entity-property").each(function(idx) {
diff --git a/test/core/js/modules/caosdb.js.js b/test/core/js/modules/caosdb.js.js
index 76141117ffc845917c0e5ff50a165e7718663a31..20dc4b4eee0116fecf57b0a178f622c4385f08b2 100644
--- a/test/core/js/modules/caosdb.js.js
+++ b/test/core/js/modules/caosdb.js.js
@@ -17,7 +17,7 @@ QUnit.module("caosdb.js", {
         }, err => {console.log(err);});
 
     },
-    
+
     before: function(assert) {
         var done = assert.async(3);
         this.setTestDocument("x", done, `
@@ -474,3 +474,110 @@ QUnit.test("unset_entity_references", function(assert) {
         assert.equal(getProperties(r)[0].reference, true);
     }
 });
+
+
+QUnit.test("_constructXpaths", function (assert) {
+    assert.propEqual(
+      _constructXpaths([["id"], ["longitude"], ["latitude"]]),
+      ["@id", "Property[@name='longitude']", "Property[@name='latitude']"]
+    );
+    assert.propEqual(
+      _constructXpaths([["Geo Location", "longitude"], ["latitude"]]),
+      ["Property[@name='Geo Location']//Property[@name='longitude']", "Property[@name='latitude']"]
+    );
+    assert.propEqual(
+      _constructXpaths([["", "longitude"], ["latitude"]]),
+      ["Property//Property[@name='longitude']", "Property[@name='latitude']"]
+    );
+    assert.propEqual(
+      _constructXpaths([["", "Geo Location", "", "longitude"]]),
+      ["Property//Property[@name='Geo Location']//Property//Property[@name='longitude']"]
+    );
+});
+
+
+QUnit.test("getPropertyValues", function (assert) {
+    const test_response = str2xml(`
+<Response srid="851d063d-121b-4b67-98d4-6e5ad4d31e72" timestamp="1606214042274" baseuri="https://localhost:10443" count="8">
+  <Query string="select Campaign.responsible.firstname from icecore" results="8">
+    <ParseTree>(cq select (prop_sel (prop_subsel (selector_txt   C a m p a i g n) . (prop_subsel (selector_txt r e s p o n s i b l e) . (prop_subsel (selector_txt f i r s t n a m e  ))))) from (entity icecore) &lt;EOF&gt;)</ParseTree>
+    <Role/>
+    <Entity>icecore</Entity>
+    <Selection>
+      <Selector name="Campaign.responsible.firstname"/>
+    </Selection>
+  </Query>
+  <Record id="6525" name="Test_IceCore_1">
+    <Permissions/>
+    <Property datatype="Campaign" id="6430" name="Campaign">
+      <Record id="6516" name="Test-2020_Camp1">
+        <Permissions/>
+        <Property datatype="REFERENCE" id="168" name="responsible">
+          <Record id="6515" name="Test_Scientist">
+            <Permissions/>
+            <Property datatype="DOUBLE" id="151" name="latitude" importance="FIX">
+              1.34
+              <Permissions/>
+            </Property>
+            <Property datatype="DOUBLE" id="151" name="longitude" importance="FIX">
+              2
+              <Permissions/>
+            </Property>
+          </Record>
+          <Permissions/>
+        </Property>
+      </Record>
+      <Permissions/>
+    </Property>
+  </Record>
+  <Record id="6526" name="Test_IceCore_2">
+    <Permissions/>
+    <Property datatype="Campaign" id="6430" name="Campaign">
+      <Record id="6516" name="Test-2020_Camp1">
+        <Permissions/>
+        <Property datatype="REFERENCE" id="168" name="responsible">
+          <Record id="6515" name="Test_Scientist">
+            <Permissions/>
+            <Property datatype="DOUBLE" id="151" name="latitude" importance="FIX">
+              3
+              <Permissions/>
+            </Property>
+            <Property datatype="DOUBLE" id="151" name="longitude" importance="FIX">
+              4.8345
+              <Permissions/>
+            </Property>
+          </Record>
+          <Permissions/>
+        </Property>
+      </Record>
+      <Permissions/>
+    </Property>
+  </Record>
+</Response>`);
+
+    assert.propEqual(
+      getPropertyValues(test_response, [["id"], ["", "latitude"],["", "longitude"]]),
+      [["6525" ,"1.34", "2"], ["6526", "3", "4.8345"]]);
+});
+
+// Test for bug 103
+// If role is File when creating XML for entities, checksum, path and size must be given.
+QUnit.test("unset_file_attributes", function(assert) {
+    // This should run:
+    var res1 = createEntityXML("Record", "test", 103, {}, {});
+    assert.equal(xml2str(res1), "<Record id=\"103\" name=\"test\"/>");
+    // This must throw an exception:
+    assert.throws(function () {
+        createEntityXML("File", "test", 103, {}, {});
+    });
+    // This should produce a valid XML.
+    var res2 = createEntityXML("File", "test", 103, {}, {},
+                               false, undefined, undefined, undefined,
+                               "testfile.txt", "blablabla", 0);
+    assert.equal(xml2str(res2), "<File id=\"103\" name=\"test\" path=\"testfile.txt\" checksum=\"blablabla\" size=\"0\"/>");
+
+    var res3 = createFileXML("test", 103, {},
+                             "testfile.txt", "blablabla", 0,
+                             undefined);
+    assert.equal(xml2str(res3), "<File id=\"103\" name=\"test\" path=\"testfile.txt\" checksum=\"blablabla\" size=\"0\"/>");
+});
diff --git a/test/core/js/modules/entity.xsl.js b/test/core/js/modules/entity.xsl.js
index 91a6c18c2f0bee58c272461fe4b4fe595508110c..59a2f8a6f9a03cf4990fe11bfd751d437a3ffc3e 100644
--- a/test/core/js/modules/entity.xsl.js
+++ b/test/core/js/modules/entity.xsl.js
@@ -264,6 +264,48 @@ QUnit.test("data-version-successor attribute", function(assert) {
     assert.equal($(html).find("div.caosdb-entity-panel[data-version-successor]").length, 0, "data-version-successor attribute not present");
 });
 
+QUnit.test("version full history", function (assert) {
+    var xmlstr = `
+    <Response username="user1" realm="Realm1" srid="31ce8ea1-6c9b-4a82-82ec-9f6f3edd2622" timestamp="1606225647516" baseuri="https://localhost:10443" count="1">
+  <Record id="3373" name="TestRecord1-10thVersion" description="This is the 10th version.">
+    <Permissions>
+      <Permission name="RETRIEVE:HISTORY" />
+    </Permissions>
+    <Version id="vid6" username="user1" realm="Realm1" date="date6" completeHistory="true">
+      <Predecessor id="vid5" username="user1" realm="Realm1" date="date5">
+        <Predecessor id="vid4" username="user1" realm="Realm1" date="date4">
+          <Predecessor id="vid3" username="user1" realm="Realm1" date="date3">
+            <Predecessor id="vid2" username="user1" realm="Realm1" date="date2">
+              <Predecessor id="vid1" username="user1" realm="Realm1" date="date1" />
+            </Predecessor>
+          </Predecessor>
+        </Predecessor>
+      </Predecessor>
+      <Successor id="vid7" username="user1" realm="Realm1" date="date7">
+        <Successor id="vid8" username="user1" realm="Realm1" date="date8">
+          <Successor id="vid9" username="user1" realm="Realm1" date="date9">
+            <Successor id="vid10" username="user1" realm="Realm1" date="date10" />
+          </Successor>
+        </Successor>
+      </Successor>
+    </Version>
+    <Parent id="3372" name="TestRT" />
+  </Record>
+</Response>
+`;
+    var xml = str2xml(xmlstr);
+    var html = applyTemplates(xml, this.entityXSL, "entities", "*");
+    var version_info = $(html).find(".caosdb-f-entity-version-info");
+    var table_elem = $(version_info).find("table");
+
+    var TAB = "%09", NEWL = "%0A", usr = "user1@Realm1",
+      path = "/entitypath/3373";
+    var export_table = `data:text/csv;charset=utf-8,Entity ID${TAB}Version ID${TAB}Date${TAB}User${TAB}URI${NEWL}3373${TAB}vid10${TAB}date10${TAB}${usr}${TAB}${path}@vid10${NEWL}3373${TAB}vid9${TAB}date9${TAB}${usr}${TAB}${path}@vid9${NEWL}3373${TAB}vid8${TAB}date8${TAB}${usr}${TAB}${path}@vid8${NEWL}3373${TAB}vid7${TAB}date7${TAB}${usr}${TAB}${path}@vid7${NEWL}3373${TAB}vid6${TAB}date6${TAB}${usr}${TAB}${path}@vid6${NEWL}3373${TAB}vid5${TAB}date5${TAB}${usr}${TAB}${path}@vid5${NEWL}3373${TAB}vid4${TAB}date4${TAB}${usr}${TAB}${path}@vid4${NEWL}3373${TAB}vid3${TAB}date3${TAB}${usr}${TAB}${path}@vid3${NEWL}3373${TAB}vid2${TAB}date2${TAB}${usr}${TAB}${path}@vid2${NEWL}3373${TAB}vid1${TAB}date1${TAB}${usr}${TAB}${path}@vid1`
+    assert.equal(version_info.length, 1);
+    assert.equal(table_elem.length, 1);
+    assert.equal(version_history.get_history_tsv(table_elem[0]), export_table);
+});
+
 QUnit.test("Transforming abstract properties", function (assert) {
     var xmlstr = `<Property id="3842" name="reftotestrt" datatype="TestRT">
     <Version id="04ad505da057603a9177a1fcf6c9efd5f3690fe4" date="2020-11-23T10:38:02.936+0100" />
diff --git a/test/core/js/modules/ext_bookmarks.js.js b/test/core/js/modules/ext_bookmarks.js.js
index 24a4b98d7796da411890d9bdb2ca58b5927fda65..831df74231e479d5b4550524f6bc0da617c98fb3 100644
--- a/test/core/js/modules/ext_bookmarks.js.js
+++ b/test/core/js/modules/ext_bookmarks.js.js
@@ -62,20 +62,15 @@ QUnit.test("get_bookmarks, clear_bookmark_storage", function(assert) {
 });
 
 QUnit.test("get_export_table", async function (assert) {
-    connection.get = (id) => `<root><Response><File id="${id}" path="testpath_${id.split("/")[1]}"/></Response></root>`;
+    connection.get = (id) => `<root><Response><File id="${id}" path="testpath_${id.split("/")[1]}"><Version id="abcHead"/></File></Response></root>`;
     const TAB = "%09";
     const NEWL = "%0A";
     const context_root = connection.getBasePath() + "Entity/";
     var table = await ext_bookmarks.get_export_table(
       ["123@ver1", "456@ver2", "789@ver3", "101112", "@131415"]);
     assert.equal(table,
-      `data:text/csv;charset=utf-8,ID${TAB}Version${TAB}URI${TAB}Path${NEWL}123${TAB}ver1${TAB}${context_root}123@ver1${TAB}testpath_123@ver1${NEWL}456${TAB}ver2${TAB}${context_root}456@ver2${TAB}testpath_456@ver2${NEWL}789${TAB}ver3${TAB}${context_root}789@ver3${TAB}testpath_789@ver3${NEWL}101112${TAB}${TAB}${context_root}101112${TAB}testpath_101112${NEWL}${TAB}131415${TAB}${context_root}@131415${TAB}testpath_@131415`);
+      `data:text/csv;charset=utf-8,ID${TAB}Version${TAB}URI${TAB}Path${TAB}Name${TAB}RecordType${NEWL}123${TAB}ver1${TAB}${context_root}123@ver1${TAB}testpath_123@ver1${TAB}${TAB}${NEWL}456${TAB}ver2${TAB}${context_root}456@ver2${TAB}testpath_456@ver2${TAB}${TAB}${NEWL}789${TAB}ver3${TAB}${context_root}789@ver3${TAB}testpath_789@ver3${TAB}${TAB}${NEWL}101112${TAB}abcHead${TAB}${context_root}101112@abcHead${TAB}testpath_101112${TAB}${TAB}${NEWL}${TAB}131415${TAB}${context_root}@131415${TAB}testpath_@131415${TAB}${TAB}`);
 
-    connection.get = (id) => {throw new Error("path should be in cache");};
-    table = await ext_bookmarks.get_export_table(
-      ["123@ver1", "456@ver2", "789@ver3", "101112", "@131415"]);
-    assert.equal(table,
-      `data:text/csv;charset=utf-8,ID${TAB}Version${TAB}URI${TAB}Path${NEWL}123${TAB}ver1${TAB}${context_root}123@ver1${TAB}testpath_123@ver1${NEWL}456${TAB}ver2${TAB}${context_root}456@ver2${TAB}testpath_456@ver2${NEWL}789${TAB}ver3${TAB}${context_root}789@ver3${TAB}testpath_789@ver3${NEWL}101112${TAB}${TAB}${context_root}101112${TAB}testpath_101112${NEWL}${TAB}131415${TAB}${context_root}@131415${TAB}testpath_@131415`);
 });
 
 QUnit.test("update_clear_button", function (assert) {
diff --git a/test/core/js/modules/ext_map.js.js b/test/core/js/modules/ext_map.js.js
index 9d068ad1c688c7a8643c18846103ea33a10c8874..5afe434e375b40e3f141141b120ed5497939211f 100644
--- a/test/core/js/modules/ext_map.js.js
+++ b/test/core/js/modules/ext_map.js.js
@@ -23,10 +23,13 @@
 'use strict';
 
 QUnit.module("ext_map.js", {
-    before: function(assert) {
+    before: function (assert) {
         var lat = "latitude";
         var lng = "longitude";
-        this.datamodel = { lat: lat, lng: lng };
+        this.datamodel = {
+            lat: lat,
+            lng: lng
+        };
         this.test_map_entity = `
 <div class="caosdb-entity-panel caosdb-properties">
   <div class="caosdb-id">1234</div>
@@ -42,22 +45,22 @@ QUnit.module("ext_map.js", {
   </div>
 </div>`;
     },
-    beforeEach: function(assert) {
+    beforeEach: function (assert) {
         sessionStorage.removeItem("caosdb_map.view");
     }
 });
 
-QUnit.test("availability", function(assert) {
-    assert.equal(caosdb_map.version, "0.3", "test version");
+QUnit.test("availability", function (assert) {
+    assert.equal(caosdb_map.version, "0.4", "test version");
     assert.ok(caosdb_map.init, "init available");
 });
 
-QUnit.test("default config", function(assert) {
+QUnit.test("default config", function (assert) {
     assert.ok(caosdb_map._default_config);
     assert.equal(caosdb_map._default_config.version, caosdb_map.version, "version");
 });
 
-QUnit.test("load_config", async function(assert) {
+QUnit.test("load_config", async function (assert) {
     assert.ok(caosdb_map.load_config, "available");
     var config = await caosdb_map.load_config("non_existing.json");
     assert.ok(config, "returns something");
@@ -65,39 +68,43 @@ QUnit.test("load_config", async function(assert) {
     assert.equal(config.views[0].id, "UNCONFIGURED", "view has id 'UNCONFIGURED'.");
 });
 
-QUnit.test("check_config", function(assert) {
+QUnit.test("check_config", function (assert) {
     assert.ok(caosdb_map.check_config(caosdb_map._default_config), "default config ok");
-    assert.throws(()=>caosdb_map.check_config({"version": "wrong version",}), /The version of the configuration does not match the version of this implementation of the caosdb_map module. Should be '.*', was 'wrong version'./, "wrong version");
+    assert.throws(() => caosdb_map.check_config({
+        "version": "wrong version",
+    }), /The version of the configuration does not match the version of this implementation of the caosdb_map module. Should be '.*', was 'wrong version'./, "wrong version");
 });
 
-QUnit.test("check dependencies", function(assert) {
+QUnit.test("check dependencies", function (assert) {
     assert.ok(caosdb_map.check_dependencies, "available");
-    assert.propEqual(caosdb_map.dependencies, ["log", {"L": ["latlngGraticule", "Proj"]}, "navbar", "caosdb_utils"]);
+    assert.propEqual(caosdb_map.dependencies, ["log", {
+        "L": ["latlngGraticule", "Proj"]
+    }, "navbar", "caosdb_utils"]);
     assert.ok(caosdb_map.check_dependencies(), "deps available");
 });
 
-QUnit.test("create_toggle_map_button", function(assert) {
+QUnit.test("create_toggle_map_button", function (assert) {
     assert.ok(caosdb_map.create_toggle_map_button, "available");
     var button = caosdb_map.create_toggle_map_button();
     assert.equal(button.tagName, "BUTTON", "is button");
-    assert.ok($(button).hasClass("caosdb-f-toggle-map-button"),"has caosdb-f-toggle-map-button class");
+    assert.ok($(button).hasClass("caosdb-f-toggle-map-button"), "has caosdb-f-toggle-map-button class");
     assert.equal($(button).text(), "Map", "button says 'Map'");
 
     // set other content:
     button = caosdb_map.create_toggle_map_button("Karte");
     assert.equal(button.tagName, "BUTTON", "is button");
-    assert.ok($(button).hasClass("caosdb-f-toggle-map-button"),"has caosdb-f-toggle-map-button class");
+    assert.ok($(button).hasClass("caosdb-f-toggle-map-button"), "has caosdb-f-toggle-map-button class");
     assert.equal($(button).text(), "Karte", "button says 'Karte'");
 
 });
 
-QUnit.test("bind_toggle_map", function(assert) {
+QUnit.test("bind_toggle_map", function (assert) {
     let button = $("<button/>")[0];
     let done = assert.async();
 
     assert.ok(caosdb_map.bind_toggle_map, "available");
-    assert.throws(()=>caosdb_map.bind_toggle_map(button), /parameter 'toggle_cb'.* was undefined/, "no function throws");
-    assert.throws(()=>caosdb_map.bind_toggle_map("test", ()=>{}), /parameter 'button'.* was string/, "string button throws");
+    assert.throws(() => caosdb_map.bind_toggle_map(button), /parameter 'toggle_cb'.* was undefined/, "no function throws");
+    assert.throws(() => caosdb_map.bind_toggle_map("test", () => {}), /parameter 'button'.* was string/, "string button throws");
     assert.equal(caosdb_map.bind_toggle_map(button, done), button, "call returns button");
 
     // button click calls 'done'
@@ -105,12 +112,12 @@ QUnit.test("bind_toggle_map", function(assert) {
 });
 
 
-QUnit.test("create_map", function(assert) {
+QUnit.test("create_map", function (assert) {
     assert.equal(typeof caosdb_map.create_map_view, "function", "function available");
 
 });
 
-QUnit.test("create_map_panel", function(assert) {
+QUnit.test("create_map_panel", function (assert) {
     assert.ok(caosdb_map.create_map_panel, "available");
     let panel = caosdb_map.create_map_panel();
     assert.equal(panel.tagName, "DIV", "is div");
@@ -118,9 +125,11 @@ QUnit.test("create_map_panel", function(assert) {
     assert.ok($(panel).hasClass("container"), "has class container");
 });
 
-QUnit.test("create_map_view", function(assert) {
-    var view_config = $.extend(true, {}, caosdb_map._unconfigured_views[0],
-        {"select": true, "view_change": true});
+QUnit.test("create_map_view", function (assert) {
+    var view_config = $.extend(true, {}, caosdb_map._unconfigured_views[0], {
+        "select": true,
+        "view_change": true
+    });
     var map_panel = $("<div/>");
 
     var map = caosdb_map.create_map_view(map_panel[0], view_config);
@@ -144,7 +153,7 @@ QUnit.test("create_map_view", function(assert) {
     map.remove();
 
     // test with special crs:
-    view_config["crs"] =  {
+    view_config["crs"] = {
         "code": "EPSG:3995",
         "proj4def": "+proj=stere +lat_0=90 +lat_ts=71 +lon_0=0 +k=1 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs",
         "options": {
@@ -163,7 +172,7 @@ QUnit.test("create_map_view", function(assert) {
 
 });
 
-QUnit.test("get_map_entities", function(assert) {
+QUnit.test("get_map_entities", function (assert) {
     var datamodel = this.datamodel;
     var container = $('<div/>').append(this.test_map_entity);
     var map_objects = caosdb_map.get_map_entities(container[0], datamodel);
@@ -171,12 +180,12 @@ QUnit.test("get_map_entities", function(assert) {
 });
 
 
-QUnit.test("create_entitiy_markers", function(assert) {
+QUnit.test("create_entity_markers", function (assert) {
     var datamodel = this.datamodel;
     var entities = $(this.test_map_entity).toArray();
 
     // w/o popup
-    var markers = caosdb_map.create_entitiy_markers(entities, datamodel);
+    var markers = caosdb_map.create_entity_markers(entities, datamodel);
     assert.equal(markers.length, 1, "has one marker");
     assert.ok(markers[0] instanceof L.Marker, "is marker");
     var latlng = markers[0]._latlng;
@@ -185,30 +194,32 @@ QUnit.test("create_entitiy_markers", function(assert) {
     assert.notOk(markers[0].getPopup(), "no popup");
 
     // with popup
-    var markers = caosdb_map.create_entitiy_markers(entities, datamodel, ()=>"popup");
+    var markers = caosdb_map.create_entity_markers(entities, datamodel, () => "popup");
     assert.ok(markers[0].getPopup(), "has popup");
 });
 
 
-QUnit.test("_add_current_page_entities", function(assert) {
+QUnit.test("_add_current_page_entities", async function (assert) {
     var datamodel = this.datamodel;
     var layerGroup = L.layerGroup();
     var container = $('<div class="caosdb-f-main-entities"/>').append(this.test_map_entity);
     $("body").append(container);
 
     assert.equal(layerGroup.getLayers().length, 0, "no layer");
-    var cpe = caosdb_map._get_current_page_entities(datamodel, undefined, undefined, undefined, undefined);
+    var cpe = await caosdb_map._generic_get_current_page_entities(datamodel, undefined, undefined, undefined, undefined, undefined);
 
     assert.equal(cpe.length, 1, "has one entity");
     container.remove();
 });
 
 
-QUnit.test("make_layer_chooser_html", function(assert) {
-    var test_conf = { "id": "test_id",
+QUnit.test("make_layer_chooser_html", function (assert) {
+    var test_conf = {
+        "id": "test_id",
         "name": "test name",
         "description": "test description",
-        "icon": { "html": "<span>ICON</span>",
+        "icon": {
+            "html": "<span>ICON</span>",
         },
     };
 
@@ -217,19 +228,139 @@ QUnit.test("make_layer_chooser_html", function(assert) {
     assert.equal($(layer_chooser).attr("title"), "test description", "description set as title");
 });
 
-QUnit.test("init_entity_layer", function(assert) {
-    var done = assert.async();
-    var test_conf = { "id": "test_id",
+QUnit.test("_init_single_entity_layer", function (assert) {
+    var test_conf = {
+        "id": "test_id",
         "name": "test name",
         "description": "test description",
-        "get_entities": async function() {done(); return []},
-        "icon": { "html": "<span>ICON</span>",
+        "icon": {
+            "html": "<span>ICON</span>",
         },
     }
 
-    var entityLayer= caosdb_map.init_entity_layer(test_conf);
+    var entityLayer = caosdb_map._init_single_entity_layer(test_conf);
     assert.equal(entityLayer.id, test_conf.id, "id");
     assert.equal(entityLayer.active, true, "is active");
     assert.ok(entityLayer.chooser_html instanceof HTMLElement, "chooser_html is HTMLElement");
-    assert.equal(entityLayer.layer_group.getLayers().length, 0 , "empty layergroup");
+    assert.equal(entityLayer.layer_group.getLayers().length, 0, "empty layergroup");
+});
+
+QUnit.test("_get_with_POV ", function (assert) {
+    assert.equal(caosdb_map._get_with_POV(
+        []), "", "no POV");
+    assert.equal(caosdb_map._get_with_POV(
+        ["lol"]), " WITH lol ", "single POV");
+    assert.equal(caosdb_map._get_with_POV(
+        ["lol", "hi"]), " WITH lol  WITH hi ", "with two POV");
+});
+
+
+QUnit.test("_get_select_with_path  ", function (assert) {
+    assert.throws(() => caosdb_map._get_select_with_path(), /Supply the datamodel./, "missing datamodel");
+    assert.throws(() => caosdb_map._get_select_with_path(this.datamodel, []), /Supply at least a RecordType./, "missing value");
+    assert.equal(caosdb_map._get_select_with_path(
+        this.datamodel,
+        ["RealRT"]), "SELECT parent,latitude,longitude FROM ENTITY RealRT  WITH latitude AND longitude ", "RT only");
+    assert.equal(caosdb_map._get_select_with_path(
+        this.datamodel,
+        ["RealRT", "prop1"]), "SELECT parent,prop1.latitude,prop1.longitude FROM ENTITY RealRT  WITH prop1  WITH latitude AND longitude ", "RT with one prop");
+    assert.equal(caosdb_map._get_select_with_path(
+        this.datamodel,
+        ["RealRT", "prop1", "prop2"]), "SELECT parent,prop1.prop2.latitude,prop1.prop2.longitude FROM ENTITY RealRT  WITH prop1  WITH prop2  WITH latitude AND longitude ", "RT with two props");
+});
+
+
+QUnit.test("_get_leaf_prop", async function (assert) {
+    const test_response = str2xml(`
+<Response srid="851d063d-121b-4b67-98d4-6e5ad4d31e72" timestamp="1606214042274" baseuri="https://localhost:10443" count="8">
+  <Query string="select Campaign.responsible.firstname from icecore" results="8">
+    <ParseTree>(cq select (prop_sel (prop_subsel (selector_txt   C a m p a i g n) . (prop_subsel (selector_txt r e s p o n s i b l e) . (prop_subsel (selector_txt f i r s t n a m e  ))))) from (entity icecore) &lt;EOF&gt;)</ParseTree>
+    <Role/>
+    <Entity>icecore</Entity>
+    <Selection>
+      <Selector name="Campaign.responsible.firstname"/>
+    </Selection>
+  </Query>
+  <Record id="6525" name="Test_IceCore_1">
+    <Permissions/>
+    <Property datatype="Campaign" id="6430" name="Campaign">
+      <Record id="6516" name="Test-2020_Camp1">
+        <Permissions/>
+        <Property datatype="REFERENCE" id="168" name="responsible">
+          <Record id="6515" name="Test_Scientist">
+            <Permissions/>
+            <Property datatype="DOUBLE" id="151" name="latitude" importance="FIX">
+              1.34
+              <Permissions/>
+            </Property>
+            <Property datatype="DOUBLE" id="151" name="longitude" importance="FIX">
+              2
+              <Permissions/>
+            </Property>
+          </Record>
+          <Permissions/>
+        </Property>
+      </Record>
+      <Permissions/>
+    </Property>
+  </Record>
+  <Record id="6526" name="Test_IceCore_2">
+    <Permissions/>
+    <Property datatype="Campaign" id="6430" name="Campaign">
+      <Record id="6516" name="Test-2020_Camp1">
+        <Permissions/>
+        <Property datatype="REFERENCE" id="168" name="responsible">
+          <Record id="6515" name="Test_Scientist">
+            <Permissions/>
+            <Property datatype="DOUBLE" id="151" name="latitude" importance="FIX">
+              3
+              <Permissions/>
+            </Property>
+            <Property datatype="DOUBLE" id="151" name="longitude" importance="FIX">
+              4.8345
+              <Permissions/>
+            </Property>
+          </Record>
+          <Permissions/>
+        </Property>
+      </Record>
+      <Permissions/>
+    </Property>
+  </Record>
+</Response>`);
+    var leaves = caosdb_map._get_leaf_prop(test_response, 2, this.datamodel)
+
+    assert.equal(Object.keys(leaves).length, 2, "number of records");
+    assert.notEqual(typeof leaves["6525"], "undefined", "has entity id");
+    assert.deepEqual(leaves["6525"], ["1.34", "2"]);
+    assert.deepEqual(leaves["6526"], ["3", "4.8345"], "long/lat in second rec");
+
+    assert.equal(
+        caosdb_map._get_toplvl_rec_with_id(test_response, "6526")["id"],
+        "6526",
+        "number of records");
+
+    caosdb_map._set_subprops_at_top(
+        test_response, 2, this.datamodel, {
+            "6526": [1.234, 5.67]
+        })
+    assert.equal($(test_response).find(`[name='longitude']`).length,
+        4,
+        "number lng props");
+    assert.equal($(test_response).find(`[name='latitude']`).length,
+        4,
+        "number lat props");
+    // after transforming, the long/lat props should be accessible
+    var html_ents = await transformation.transformEntities(test_response);
+    assert.equal(
+        getProperty(html_ents[0], "longitude"),
+        "2",
+        "longitude of first rec");
+
+});
+
+QUnit.test("_get_id_POV", function (assert) {
+    assert.equal(caosdb_map._get_id_POV([]), "WITH ", "no POV");
+    assert.equal(caosdb_map._get_id_POV([5]), "WITH id=5", "one id");
+    assert.equal(caosdb_map._get_id_POV([5, 6]), "WITH id=5 or id=6", "two ids");
 });
diff --git a/test/core/js/modules/ext_xls_download.js.js b/test/core/js/modules/ext_xls_download.js.js
index 997a89ec21d4a22f49746cbda1c46bb56268f80d..5426580436933b942cec6750b521747ab62f2d00 100644
--- a/test/core/js/modules/ext_xls_download.js.js
+++ b/test/core/js/modules/ext_xls_download.js.js
@@ -115,17 +115,17 @@ QUnit.test("_get_property_value", function(assert) {
 QUnit.test("_get_tsv_string", function(assert) {
     const table = this.test_case_1;
     const entities = $(table).find("tbody tr").toArray();
-    assert.equal(entities.length, 2, "two example entities");
+    assert.equal(entities.length, 3, "three example entities");
 
-    var f = caosdb_table_export._create_tsv_string 
+    var f = caosdb_table_export._create_tsv_string
     var tsv_string = f(entities, ["Bag", "Number"], true);
     var prefix = "data:text/csv;charset=utf-8,"
     assert.equal(tsv_string, 
-        "ID\tBag\tNumber\n242\t6366, 6406, 6407, 6408, 6409, 6410, 6411, 6412, 6413\t02 8   4aaa a\n2112\t\t1101", "tsv generated");
+        "ID\tVersion\tBag\tNumber\n242\tabc123\t6366, 6406, 6407, 6408, 6409, 6410, 6411, 6412, 6413\t02 8   4aaa a\n2112\tabc124\t\t1101\n2112\tabc125\t\t1102", "tsv generated");
     tsv_string = caosdb_table_export._encode_tsv_string(tsv_string);
     assert.equal(tsv_string.slice(0,prefix.length), prefix);
-    assert.equal(decodeURIComponent(tsv_string.slice(prefix.length, tsv_string.length)), 
-        "ID\tBag\tNumber\n242\t6366, 6406, 6407, 6408, 6409, 6410, 6411, 6412, 6413\t02 8   4aaa a\n2112\t\t1101", "tsv generated");
+    assert.equal(decodeURIComponent(tsv_string.slice(prefix.length, tsv_string.length)),
+        "ID\tVersion\tBag\tNumber\n242\tabc123\t6366, 6406, 6407, 6408, 6409, 6410, 6411, 6412, 6413\t02 8   4aaa a\n2112\tabc124\t\t1101\n2112\tabc125\t\t1102", "tsv generated");
 });
 
 QUnit.test("_get_property_value", function (assert) {
diff --git a/test/core/js/modules/webcaosdb.js.js b/test/core/js/modules/webcaosdb.js.js
index 5b43d2c1dcd2be0a9cac710b5749d13c631c76ed..7b3f7abf404f261668c18690df337b73794dd8bd 100644
--- a/test/core/js/modules/webcaosdb.js.js
+++ b/test/core/js/modules/webcaosdb.js.js
@@ -957,10 +957,6 @@ QUnit.test("createCarouselNav", function(assert) {
     let original_get = connection.get;
     ref_property_elem.find('div').append(refLinks);
 
-    const sleep = function sleep(ms) {
-      return new Promise(resolve => setTimeout(resolve, ms));
-    }
-
     QUnit.test("initProperty", async function(assert) {
         var done = assert.async(2);
         assert.ok(preview.initProperty, "function available");
@@ -1939,3 +1935,80 @@ QUnit.test("toolbox example", function(assert) {
     assert.equal($('.caosdb-f-navbar-toolbox[data-toolbox-name="Tools"]').length, 1, "one 'Tools' toolbox");
     assert.equal($('.caosdb-f-navbar-toolbox[data-toolbox-name="Tools"] button').length, 3, "three 'Tools' buttons");
 });
+
+QUnit.module("webcaosdb.js - version_history", {
+    before: function(assert) {
+        connection._init();
+    },
+    after: function(assert) {
+        connection._init();
+    },
+});
+
+QUnit.test("available", function (assert) {
+    assert.equal(typeof version_history.init, "function");
+    assert.equal(typeof version_history.get_history_tsv, "function");
+    assert.equal(typeof version_history.init_export_history_buttons, "function");
+    assert.equal(typeof version_history.init_load_history_buttons, "function");
+    assert.equal(typeof version_history.retrieve_history, "function");
+})
+
+QUnit.test("init_load_history_buttons and init_load_history_buttons", async function (assert) {
+    var xml_str = `<Response username="user1" realm="Realm1" srid="bc2f8f6b-71d6-49ca-890c-eebea3e38e18" timestamp="1606253365632" baseuri="https://localhost:10443" count="1">
+  <UserInfo username="user1" realm="Realm1">
+    <Roles>
+      <Role>role1</Role>
+    </Roles>
+  </UserInfo>
+  <Record id="8610" name="TestRecord1-6thVersion" description="This is the 6th version.">
+    <Permissions>
+      <Permission name="RETRIEVE:HISTORY" />
+    </Permissions>
+    <Version id="efa5ac7126c722b3f43284e150d070d6deac0ba6">
+      <Predecessor id="f09114b227d88f23d4e23645ae471d688b1e82f7" />
+      <Successor id="5759d2bccec3662424db5bb005acea4456a299ef" />
+    </Version>
+    <Parent id="8609" name="TestRT" />
+  </Record>
+</Response>
+`;
+    var done = assert.async(2);
+    var xml = str2xml(xml_str);
+    version_history._get = async function (entity) {
+        assert.equal(entity, "Entity/8610@efa5ac7126c722b3f43284e150d070d6deac0ba6?H");
+        done();
+        $(xml).find("Version").attr("completeHistory", "true");
+        return xml;
+    }
+    var html = await transformation.transformEntities(xml);
+    var load_button = $(html).find(".caosdb-f-entity-version-load-history-btn");
+    $("body").append(html);
+
+    assert.notOk(load_button.is(":visible"), "load_button hidden");
+    load_button.click(); // nothing happens
+
+    version_history.init_load_history_buttons();
+    assert.ok(load_button.is(":visible"), "load_button is not hidden anymore");
+
+    // load_button triggers retrieval of history
+    load_button.click();
+    await sleep(200);
+
+    var gone_button = $(html).find(".caosdb-f-entity-version-load-history-btn");
+    assert.equal(gone_button.length, 0, "button is gone");
+
+    export_button = $(html).find(".caosdb-f-entity-version-export-history-btn");
+    assert.ok(export_button.is(":visible"), "export_button is visible");
+
+    version_history._download_tsv = function (tsv) {
+        assert.equal(tsv.indexOf("data:text/csv;charset=utf-8,Entity ID%09"), 0);
+        done();
+    }
+    export_button.click();
+
+    $(html).remove();
+});
+
+const sleep = function sleep(ms) {
+  return new Promise(resolve => setTimeout(resolve, ms));
+}
diff --git a/test/core/xml/table_export/test_case_select_table_1.xml b/test/core/xml/table_export/test_case_select_table_1.xml
index ae0a856f106557be3712f303b06a99f6220ef827..be07fd42df8bc1275ab16802c0e82c19ece417ad 100644
--- a/test/core/xml/table_export/test_case_select_table_1.xml
+++ b/test/core/xml/table_export/test_case_select_table_1.xml
@@ -9,6 +9,7 @@
     </Selection>
   </Query>
   <Record id="242">
+    <Version id="abc123" head="true"/>
     <Property id="117" name="Number" datatype="TEXT" importance="FIX">
       02&#x9;8&#xA;&#xA;&#xA;4aaa&#x9;a
     </Property>
@@ -35,10 +36,19 @@
     </Property>
   </Record>
   <Record id="2112">
+    <Version id="abc124" head="true"/>
     <Property id="117" name="Number" datatype="TEXT" importance="FIX">
       1101
     </Property>
     <Property id="104" name="Bag" datatype="LIST&lt;Bag&gt;" importance="FIX">
     </Property>
   </Record>
+  <Record id="2112">
+    <Version id="abc125" head="true"/>
+    <Property id="117" name="Number" datatype="TEXT" importance="FIX">
+      1102
+    </Property>
+    <Property id="104" name="Bag" datatype="LIST&lt;Bag&gt;" importance="FIX">
+    </Property>
+  </Record>
 </Response>
diff --git a/test/docker/Dockerfile b/test/docker/Dockerfile
index bccd94fe14f49a79d8a43b559b3b857ed1d7d07d..026887097ac3b7dc13e6e429bf73c363e3adcbf3 100644
--- a/test/docker/Dockerfile
+++ b/test/docker/Dockerfile
@@ -1,8 +1,22 @@
-FROM debian:latest
-RUN apt-get update && \
-    apt-get install firefox-esr gettext-base pylint3 python3-pip \
-    python3-httpbin git curl x11-apps xvfb unzip -y python3-pytest
+FROM debian:10
+ADD node_gpg.asc /etc/apt/
+RUN  apt-get update \
+    && apt-get install -y gnupg ca-certificates\
+    && apt-key add /etc/apt/node_gpg.asc \
+    && echo "deb http://deb.debian.org/debian buster-backports main" >> /etc/apt/sources.list \
+    && echo "deb https://deb.nodesource.com/node_14.x buster main" >> /etc/apt/sources.list \
+    && apt-get update \
+    && apt-get install -y \
+      firefox-esr gettext-base python3-pip \
+      python3-httpbin git curl x11-apps xvfb unzip \
+      nodejs # Don't install `npm` (Debian), it conflicts with the `nodejs` (Node) package \
+    && apt-get install -f
+
+RUN pip3 install pylint pytest
 RUN pip3 install caosdb
-RUN pip3 install pandas xlrd
+RUN pip3 install pandas xlrd==1.2.0
 RUN pip3 install git+https://gitlab.com/caosdb/caosdb-advanced-user-tools.git@dev
-
+# For automatic documentation
+#RUN npm install -g jsdoc
+#RUN npm install -g jsdoc-sphinx
+RUN pip3 install sphinx-js sphinx-autoapi recommonmark sphinx-rtd-theme
diff --git a/test/docker/node_gpg.asc b/test/docker/node_gpg.asc
new file mode 100644
index 0000000000000000000000000000000000000000..ed458a24e9132e0f22b6081d5e2b68926d00c725
--- /dev/null
+++ b/test/docker/node_gpg.asc
@@ -0,0 +1,54 @@
+Downloaded on 2020-12-18
+
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: GnuPG v1
+Comment: GPGTools - https://gpgtools.org
+
+mQINBFObJLYBEADkFW8HMjsoYRJQ4nCYC/6Eh0yLWHWfCh+/9ZSIj4w/pOe2V6V+
+W6DHY3kK3a+2bxrax9EqKe7uxkSKf95gfns+I9+R+RJfRpb1qvljURr54y35IZgs
+fMG22Np+TmM2RLgdFCZa18h0+RbH9i0b+ZrB9XPZmLb/h9ou7SowGqQ3wwOtT3Vy
+qmif0A2GCcjFTqWW6TXaY8eZJ9BCEqW3k/0Cjw7K/mSy/utxYiUIvZNKgaG/P8U7
+89QyvxeRxAf93YFAVzMXhoKxu12IuH4VnSwAfb8gQyxKRyiGOUwk0YoBPpqRnMmD
+Dl7SdmY3oQHEJzBelTMjTM8AjbB9mWoPBX5G8t4u47/FZ6PgdfmRg9hsKXhkLJc7
+C1btblOHNgDx19fzASWX+xOjZiKpP6MkEEzq1bilUFul6RDtxkTWsTa5TGixgCB/
+G2fK8I9JL/yQhDc6OGY9mjPOxMb5PgUlT8ox3v8wt25erWj9z30QoEBwfSg4tzLc
+Jq6N/iepQemNfo6Is+TG+JzI6vhXjlsBm/Xmz0ZiFPPObAH/vGCY5I6886vXQ7ft
+qWHYHT8jz/R4tigMGC+tvZ/kcmYBsLCCI5uSEP6JJRQQhHrCvOX0UaytItfsQfLm
+EYRd2F72o1yGh3yvWWfDIBXRmaBuIGXGpajC0JyBGSOWb9UxMNZY/2LJEwARAQAB
+tB9Ob2RlU291cmNlIDxncGdAbm9kZXNvdXJjZS5jb20+iQI4BBMBAgAiBQJTmyS2
+AhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRAWVaCraFdigHTmD/9OKhUy
+jJ+h8gMRg6ri5EQxOExccSRU0i7UHktecSs0DVC4lZG9AOzBe+Q36cym5Z1di6JQ
+kHl69q3zBdV3KTW+H1pdmnZlebYGz8paG9iQ/wS9gpnSeEyx0Enyi167Bzm0O4A1
+GK0prkLnz/yROHHEfHjsTgMvFwAnf9uaxwWgE1d1RitIWgJpAnp1DZ5O0uVlsPPm
+XAhuBJ32mU8S5BezPTuJJICwBlLYECGb1Y65Cil4OALU7T7sbUqfLCuaRKxuPtcU
+VnJ6/qiyPygvKZWhV6Od0Yxlyed1kftMJyYoL8kPHfeHJ+vIyt0s7cropfiwXoka
+1iJB5nKyt/eqMnPQ9aRpqkm9ABS/r7AauMA/9RALudQRHBdWIzfIg0Mlqb52yyTI
+IgQJHNGNX1T3z1XgZhI+Vi8SLFFSh8x9FeUZC6YJu0VXXj5iz+eZmk/nYjUt4Mtc
+pVsVYIB7oIDIbImODm8ggsgrIzqxOzQVP1zsCGek5U6QFc9GYrQ+Wv3/fG8hfkDn
+xXLww0OGaEQxfodm8cLFZ5b8JaG3+Yxfe7JkNclwvRimvlAjqIiW5OK0vvfHco+Y
+gANhQrlMnTx//IdZssaxvYytSHpPZTYw+qPEjbBJOLpoLrz8ZafN1uekpAqQjffI
+AOqW9SdIzq/kSHgl0bzWbPJPw86XzzftewjKNbkCDQRTmyS2ARAAxSSdQi+WpPQZ
+fOflkx9sYJa0cWzLl2w++FQnZ1Pn5F09D/kPMNh4qOsyvXWlekaV/SseDZtVziHJ
+Km6V8TBG3flmFlC3DWQfNNFwn5+pWSB8WHG4bTA5RyYEEYfpbekMtdoWW/Ro8Kmh
+41nuxZDSuBJhDeFIp0ccnN2Lp1o6XfIeDYPegyEPSSZqrudfqLrSZhStDlJgXjea
+JjW6UP6txPtYaaila9/Hn6vF87AQ5bR2dEWB/xRJzgNwRiax7KSU0xca6xAuf+TD
+xCjZ5pp2JwdCjquXLTmUnbIZ9LGV54UZ/MeiG8yVu6pxbiGnXo4Ekbk6xgi1ewLi
+vGmz4QRfVklV0dba3Zj0fRozfZ22qUHxCfDM7ad0eBXMFmHiN8hg3IUHTO+UdlX/
+aH3gADFAvSVDv0v8t6dGc6XE9Dr7mGEFnQMHO4zhM1HaS2Nh0TiL2tFLttLbfG5o
+QlxCfXX9/nasj3K9qnlEg9G3+4T7lpdPmZRRe1O8cHCI5imVg6cLIiBLPO16e0fK
+yHIgYswLdrJFfaHNYM/SWJxHpX795zn+iCwyvZSlLfH9mlegOeVmj9cyhN/VOmS3
+QRhlYXoA2z7WZTNoC6iAIlyIpMTcZr+ntaGVtFOLS6fwdBqDXjmSQu66mDKwU5Ek
+fNlbyrpzZMyFCDWEYo4AIR/18aGZBYUAEQEAAYkCHwQYAQIACQUCU5sktgIbDAAK
+CRAWVaCraFdigIPQEACcYh8rR19wMZZ/hgYv5so6Y1HcJNARuzmffQKozS/rxqec
+0xM3wceL1AIMuGhlXFeGd0wRv/RVzeZjnTGwhN1DnCDy1I66hUTgehONsfVanuP1
+PZKoL38EAxsMzdYgkYH6T9a4wJH/IPt+uuFTFFy3o8TKMvKaJk98+Jsp2X/QuNxh
+qpcIGaVbtQ1bn7m+k5Qe/fz+bFuUeXPivafLLlGc6KbdgMvSW9EVMO7yBy/2JE15
+ZJgl7lXKLQ31VQPAHT3an5IV2C/ie12eEqZWlnCiHV/wT+zhOkSpWdrheWfBT+ac
+hR4jDH80AS3F8jo3byQATJb3RoCYUCVc3u1ouhNZa5yLgYZ/iZkpk5gKjxHPudFb
+DdWjbGflN9k17VCf4Z9yAb9QMqHzHwIGXrb7ryFcuROMCLLVUp07PrTrRxnO9A/4
+xxECi0l/BzNxeU1gK88hEaNjIfviPR/h6Gq6KOcNKZ8rVFdwFpjbvwHMQBWhrqfu
+G3KaePvbnObKHXpfIKoAM7X2qfO+IFnLGTPyhFTcrl6vZBTMZTfZiC1XDQLuGUnd
+sckuXINIU3DFWzZGr0QrqkuE/jyr7FXeUJj9B7cLo+s/TXo+RaVfi3kOc9BoxIvy
+/qiNGs/TKy2/Ujqp/affmIMoMXSozKmga81JSwkADO1JMgUy6dApXz9kP4EE3g==
+=CLGF
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/test/server_side_scripting/ext_table_preview/__pycache__/pandas_table_preview.cpython-37.pyc b/test/server_side_scripting/ext_table_preview/__pycache__/pandas_table_preview.cpython-37.pyc
deleted file mode 100644
index 613d9dce64b94c3b4c66891f22cd02a6c337dff6..0000000000000000000000000000000000000000
Binary files a/test/server_side_scripting/ext_table_preview/__pycache__/pandas_table_preview.cpython-37.pyc and /dev/null differ
diff --git a/test/server_side_scripting/ext_table_preview/__pycache__/test_pandas_table_preview.cpython-37-pytest-6.0.2.pyc b/test/server_side_scripting/ext_table_preview/__pycache__/test_pandas_table_preview.cpython-37-pytest-6.0.2.pyc
deleted file mode 100644
index c3563a411e0c836d2613ab7189dc6833be735e00..0000000000000000000000000000000000000000
Binary files a/test/server_side_scripting/ext_table_preview/__pycache__/test_pandas_table_preview.cpython-37-pytest-6.0.2.pyc and /dev/null differ