diff --git a/.gitignore b/.gitignore
index cc9336b6256771f79eb104a8b1325a55352657e1..f69db87ad5a5226535559b6965e771d975ded103 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
+# -*- mode:conf; -*-
+
 # dot files
 .*
 !/.git*
@@ -8,13 +10,19 @@
 
 # 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 548f86457c3216f5eed7b75b4b81d7f048351248..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
@@ -58,6 +59,14 @@ test:
   script:
     - make run-qunit
 
+test-server-side-scripting:
+  timeout: 10 minutes
+  tags: [ docker ]
+  stage: test
+  script:
+    - whereis pytest pytest3 py.test pytest-3 py.test-3
+    - make test-sss
+
 # Trigger building of server image and integration tests
 trigger_build:
   timeout: 15 minutes
@@ -78,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 a700e0a8670aae69b375e48cbe55d007f5c5f530..985d1541eaeb597156b4ddb3de377b55d71e021c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,7 +15,51 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### 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
+- added preview for tif images
+* new function `form_elements.make_alert` which generates a proceed/cancel
+  dialog which intercepts a function call and asks the user for confirmation.
+* Deleting entities prompts for user confirmation after hitting the "Delete"
+  button (edit_mode).
+* Plotly preview has an additional parameter for a config object,
+  e.g., for disabling the plotly logo
+- 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)
+
+* 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) 
+
+### Removed (for now removed features)
+
+### 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
+- #125 special characters like "\t", \"n", "#" are replaced in table
+  download
 
 ### Security (in case of vulnerabilities)
 
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 82%
rename from makefile
rename to Makefile
index f0533f8c9f77db23fc69c5312f65493073b8cfb7..1f182e7864a2252e4c1263cde7fa4238d31a4269 100644
--- a/makefile
+++ b/Makefile
@@ -33,14 +33,17 @@ SQ=\'
 ROOT_DIR = $(abspath .)
 MISC_DIR = $(abspath misc)
 PUBLIC_DIR = $(abspath public)
+SSS_BIN_DIR = $(abspath sss_bin)
 CONF_CORE_DIR = $(abspath conf/core)
 CONF_EXT_DIR = $(abspath conf/ext)
 SRC_CORE_DIR = $(abspath src/core)
 SRC_EXT_DIR = $(abspath src/ext)
+SRC_SSS_DIR = $(abspath src/server_side_scripting)
 LIBS_DIR = $(abspath libs)
 TEST_CORE_DIR = $(abspath test/core/)
 TEST_EXT_DIR = $(abspath test/ext)
-LIBS = fonts css/bootstrap.css js/bootstrap.js js/state-machine.js js/jquery.js js/showdown.js js/dropzone.js css/dropzone.css js/loglevel.js js/leaflet.js css/leaflet.css css/images js/leaflet-latlng-graticule.js js/proj4.js js/proj4leaflet.js js/leaflet-coordinates.js css/leaflet-coordinates.css js/leaflet-graticule.js js/bootstrap-select.js css/bootstrap-select.css js/bootstrap-autocomplete.min.js js/plotly.js
+TEST_SSS_DIR =$(abspath test/server_side_scripting)
+LIBS = fonts css/bootstrap.css js/bootstrap.js js/state-machine.js js/jquery.js js/showdown.js js/dropzone.js css/dropzone.css js/loglevel.js js/leaflet.js css/leaflet.css css/images js/leaflet-latlng-graticule.js js/proj4.js js/proj4leaflet.js js/leaflet-coordinates.js css/leaflet-coordinates.css js/leaflet-graticule.js js/bootstrap-select.js css/bootstrap-select.css js/bootstrap-autocomplete.min.js js/plotly.js js/pako.js js/utif.js
 
 TEST_LIBS = $(LIBS) js/qunit.js css/qunit.css $(subst $(TEST_CORE_DIR)/,,$(shell find $(TEST_CORE_DIR)/))
 
@@ -49,9 +52,9 @@ LIBS_SUBDIRS = $(addprefix $(LIBS_DIR)/, js css fonts)
 
 ALL: install
 
-install: clean cp-src cp-ext cp-conf $(addprefix $(PUBLIC_DIR)/, $(LIBS)) build_properties merge_xsl
+install: clean install-sss cp-src cp-ext cp-conf $(addprefix $(PUBLIC_DIR)/, $(LIBS)) build_properties merge_xsl
 
-test: clean cp-src cp-ext cp-ext-test cp-conf $(addprefix $(PUBLIC_DIR)/, $(TEST_LIBS)) build_properties merge_xsl
+test: clean install-sss cp-src cp-ext cp-ext-test cp-conf $(addprefix $(PUBLIC_DIR)/, $(TEST_LIBS)) build_properties merge_xsl
 	@for f in $(shell find $(TEST_EXT_DIR) -type f -iname *.js) ; do \
 		sed -i "/EXTENSIONS/a \<script src=\"$${f#$(TEST_EXT_DIR)/}\" ></script>" $(PUBLIC_DIR)/index.html ; \
 		echo include $$f; \
@@ -129,18 +132,33 @@ run-qunit: test
 		exit 1; \
 	fi
 
+install-sss:
+	@set -a -e ; \
+	pushd build.properties.files ; \
+	for f in ../build.properties.d/* ; do source "$$f" ; done ; \
+	popd ; \
+	./install-sss.sh $(SRC_SSS_DIR) $(SSS_BIN_DIR)
+
+PYTEST ?= pytest
+PIP ?= pip3
+test-sss: install-sss
+	$(PIP) freeze
+	$(PYTEST) -vv $(TEST_SSS_DIR)
+
+
+CMD_COPY_EXT_FILES = cp -i -r -L
 cp-ext:
 	# TODO FIXME Base path for not-XSL-expanded files
 	mkdir -p $(PUBLIC_DIR)/html
 	for f in $(wildcard $(SRC_EXT_DIR)/html/*) ; do \
-		echo "y" | cp -i -r "$$(realpath "$$f")" $(PUBLIC_DIR)/html/ ; \
+		echo "y" | $(CMD_COPY_EXT_FILES) "$$(realpath "$$f")" $(PUBLIC_DIR)/html/ ; \
 	done
 	for f in $(wildcard $(SRC_EXT_DIR)/js/*) ; do \
-		echo "y" | cp -i -r "$$f" $(PUBLIC_DIR)/js/ ; \
+		echo "y" | $(CMD_COPY_EXT_FILES) "$$f" $(PUBLIC_DIR)/js/ ; \
 		sed -i "/JS_EXTENSIONS/a \<xsl:element name=\"script\"><xsl:attribute name=\"src\"><xsl:value-of select=\"concat\(\$$basepath, 'webinterface/${BUILD_NUMBER}$${f#$(SRC_EXT_DIR)}'\)\" /></xsl:attribute></xsl:element>" $(PUBLIC_DIR)/xsl/main.xsl ; \
 	done
 	for f in $(wildcard $(SRC_EXT_DIR)/css/*) ; do \
-		echo "y" | cp -i -r "$$f" $(PUBLIC_DIR)/css/ ; \
+		echo "y" | $(CMD_COPY_EXT_FILES) "$$f" $(PUBLIC_DIR)/css/ ; \
 		sed -i "/CSS_EXTENSIONS/a \<xsl:element name=\"link\"><xsl:attribute name=\"rel\">stylesheet</xsl:attribute><xsl:attribute name=\"href\"><xsl:value-of select=\"concat\(\$$basepath, 'webinterface/${BUILD_NUMBER}$${f#$(SRC_EXT_DIR)}'\)\" /></xsl:attribute></xsl:element>" $(PUBLIC_DIR)/xsl/main.xsl ; \
 		for html in $(PUBLIC_DIR)/html/* ; do \
 			echo "$$html"; \
@@ -148,26 +166,26 @@ cp-ext:
 		done \
 	done
 	for f in $(wildcard $(SRC_EXT_DIR)/pics/*) ; do \
-		echo "y" | cp -i -r "$$(realpath "$$f")" $(PUBLIC_DIR)/pics/ ; \
+		echo "y" | $(CMD_COPY_EXT_FILES) "$$(realpath "$$f")" $(PUBLIC_DIR)/pics/ ; \
 	done
 	for f in $(wildcard $(SRC_EXT_DIR)/xsl/*) ; do \
-		echo "y" | cp -i -r "$$(realpath "$$f")" $(PUBLIC_DIR)/xsl/ ; \
+		echo "y" | $(CMD_COPY_EXT_FILES) "$$(realpath "$$f")" $(PUBLIC_DIR)/xsl/ ; \
 	done
 
 cp-ext-test:
 	for f in $(wildcard $(TEST_EXT_DIR)/js/*) ; do \
-		echo "y" | cp -i -r "$$f" $(PUBLIC_DIR)/js/ ; \
+		echo "y" | $(CMD_COPY_EXT_FILES) "$$f" $(PUBLIC_DIR)/js/ ; \
 		sed -i "/JS_EXTENSIONS/a \<xsl:element name=\"script\"><xsl:attribute name=\"src\"><xsl:value-of select=\"concat\(\$$basepath, 'webinterface/${BUILD_NUMBER}$${f#$(SRC_EXT_DIR)}'\)\" /></xsl:attribute></xsl:element>" $(PUBLIC_DIR)/xsl/main.xsl ; \
 	done
 	mkdir -p $(PUBLIC_DIR)/html
 	for f in $(wildcard $(TEST_EXT_DIR)/html/*) ; do \
-		echo "y" | cp -i -r "$$(realpath "$$f")" $(PUBLIC_DIR)/html/ ; \
+		echo "y" | $(CMD_COPY_EXT_FILES) "$$(realpath "$$f")" $(PUBLIC_DIR)/html/ ; \
 	done
 	for f in $(wildcard $(TEST_EXT_DIR)/pics/*) ; do \
-		echo "y" | cp -i -r "$$(realpath "$$f")" $(PUBLIC_DIR)/pics/ ; \
+		echo "y" | $(CMD_COPY_EXT_FILES) "$$(realpath "$$f")" $(PUBLIC_DIR)/pics/ ; \
 	done
 	for f in $(wildcard $(TEST_EXT_DIR)/xsl/*) ; do \
-		echo "y" | cp -i -r "$$(realpath "$$f")" $(PUBLIC_DIR)/xsl/ ; \
+		echo "y" | $(CMD_COPY_EXT_FILES) "$$(realpath "$$f")" $(PUBLIC_DIR)/xsl/ ; \
 	done
 
 cp-conf: convert-yaml
@@ -193,13 +211,13 @@ $(PUBLIC_DIR)/%: $(TEST_EXT_DIR)/%
 	cp -r $< $@
 
 $(LIBS_DIR)/fonts: unzip
-	ln -s $(LIBS_DIR)/bootstrap-3.3.7-dist/fonts $@
+	ln -s $(LIBS_DIR)/bootstrap-3.4.1-dist/fonts $@
 
 $(LIBS_DIR)/js/bootstrap.js: unzip $(LIBS_DIR)/js
-	ln -s $(LIBS_DIR)/bootstrap-3.3.7-dist/js/bootstrap.min.js $@
+	ln -s $(LIBS_DIR)/bootstrap-3.4.1-dist/js/bootstrap.min.js $@
 
 $(LIBS_DIR)/css/bootstrap.css: unzip $(LIBS_DIR)/css
-	ln -s $(LIBS_DIR)/bootstrap-3.3.7-dist/css/bootstrap.min.css $@
+	ln -s $(LIBS_DIR)/bootstrap-3.4.1-dist/css/bootstrap.min.css $@
 
 $(LIBS_DIR)/js/bootstrap-select.js: unzip $(LIBS_DIR)/js
 	ln -s $(LIBS_DIR)/bootstrap-select-1.13.9/dist/js/bootstrap-select.min.js $@
@@ -208,7 +226,7 @@ $(LIBS_DIR)/css/bootstrap-select.css: unzip $(LIBS_DIR)/css
 	ln -s $(LIBS_DIR)/bootstrap-select-1.13.9/dist/css/bootstrap-select.min.css $@
 
 $(LIBS_DIR)/js/jquery.js: unzip $(LIBS_DIR)/js
-	ln -s $(LIBS_DIR)/jquery-3.3.1/jquery-3.3.1.min.js $@
+	ln -s $(LIBS_DIR)/jquery-3.5.1/jquery-3.5.1.min.js $@
 
 $(LIBS_DIR)/js/showdown.js: unzip $(LIBS_DIR)/js
 	ln -s $(LIBS_DIR)/showdown-1.8.6/dist/showdown.min.js $@
@@ -259,17 +277,24 @@ $(LIBS_DIR)/css/leaflet-coordinates.css: unzip $(LIBS_DIR)/css
 	ln -s $(LIBS_DIR)/Leaflet.Coordinates-0.1.5/dist/Leaflet.Coordinates-0.1.5.css $@
 
 $(LIBS_DIR)/js/bootstrap-autocomplete.min.js: unzip $(LIBS_DIR)/js
-	ln -s $(LIBS_DIR)/bootstrap-autocomplete-2.3.0/dist/latest/bootstrap-autocomplete.min.js $@
+	ln -s $(LIBS_DIR)/bootstrap-autocomplete-2.3.5/dist/latest/bootstrap-autocomplete.min.js $@
 
 $(LIBS_DIR)/js/plotly.js: unzip $(LIBS_DIR)/js
 	ln -s $(LIBS_DIR)/plotly.js-1.52.2/dist/plotly.min.js $@
 
+$(LIBS_DIR)/js/pako.js: unzip $(LIBS_DIR)/js
+	ln -s $(LIBS_DIR)/pako-dummy/pako.js $@
+
+$(LIBS_DIR)/js/utif.js: unzip $(LIBS_DIR)/js
+	ln -s $(LIBS_DIR)/UTIF-8205c1f/UTIF.js $@
+
 
 $(addprefix $(LIBS_DIR)/, js css):
 	mkdir $@ || true
 
 .PHONY: clean
 clean:
+	$(RM) -r $(SSS_BIN_DIR)
 	$(RM) -r $(PUBLIC_DIR)
 	for f in $(LIBS_SUBDIRS); do unlink $$f || $(RM) -r $$f || true; done
 	for f in $(patsubst %.zip,%/,$(LIBS_ZIP)); do $(RM) -r $$f; done
@@ -280,11 +305,13 @@ unzip:
 	for f in $(LIBS_ZIP); do unzip -q -o -d libs $$f; done
 
 
-PYLINT = pylint3 -d all -e E,F
+PYLINT ?= pylint
 PYTHON_FILES = $(subst $(ROOT_DIR)/,,$(shell find $(ROOT_DIR)/ -iname "*.py"))
 pylint: $(PYTHON_FILES)
-	for f in $(PYTHON_FILES); do $(PYLINT) $$f || exit 1; done
+	for f in $(PYTHON_FILES); do $(PYLINT) -d all -e E,F $$f || exit 1; done
+
 
-PYLINT_LOCAL = /usr/bin/pylint3 -d all -e E,F
-pylint-local: $(PYTHON_FILES)
-	for f in $(PYTHON_FILES); do $(PYLINT_LOCAL) $$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 4706efe28f798a0260b0bc46374491dca7c0d667..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,21 +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/build.properties.d/00_default.properties b/build.properties.d/00_default.properties
index 895820353eba3a8b680df238c2379e0c162f81f6..f6f800227321d557e000cb848d715ce37f9febaf 100644
--- a/build.properties.d/00_default.properties
+++ b/build.properties.d/00_default.properties
@@ -46,6 +46,11 @@ BUILD_MODULE_EXT_RESOLVE_REFERENCES=ENABLED
 BUILD_MODULE_EXT_SSS_MARKDOWN=DISABLED
 BUILD_MODULE_EXT_TRIGGER_CRAWLER_FORM=DISABLED
 BUILD_MODULE_EXT_ENTITY_STATE=ENABLED
+BUILD_MODULE_EXT_AUTOCOMPLETE=ENABLED
+BUILD_MODULE_EXT_BOTTOM_LINE=ENABLED
+BUILD_MODULE_EXT_BOTTOM_LINE_TABLE_PREVIEW=DISABLED
+BUILD_MODULE_EXT_BOTTOM_LINE_TIFF_PREVIEW=DISABLED
+BUILD_MODULE_EXT_BOOKMARKS=ENABLED
 
 ##############################################################################
 # Navbar properties
diff --git a/install-sss.sh b/install-sss.sh
new file mode 100755
index 0000000000000000000000000000000000000000..bb2db57649000ba1e701786f56dba575753110eb
--- /dev/null
+++ b/install-sss.sh
@@ -0,0 +1,17 @@
+SRC_DIR=$1
+INSTALL_DIR=$2
+
+mkdir -p $INSTALL_DIR
+
+# from here on do your module-wise installing
+
+# ext_table_preview
+if [ "${BUILD_MODULE_EXT_BOTTOM_LINE_TABLE_PREVIEW}" == "ENABLED" ]; then
+    mkdir -p $INSTALL_DIR/ext_table_preview
+    cp $SRC_DIR/ext_table_preview/*.py $INSTALL_DIR/ext_table_preview/
+    echo "installed all server-side scripts for ext_table_preview"
+fi
+# ext_file_download; should always be installed - No build variable
+mkdir -p $INSTALL_DIR/ext_file_download
+cp $SRC_DIR/ext_file_download/*.py $INSTALL_DIR/ext_file_download/
+echo "installed all server-side scripts for ext_file_download"
diff --git a/libs/UTIF-8205c1f.zip b/libs/UTIF-8205c1f.zip
new file mode 100644
index 0000000000000000000000000000000000000000..7069cd288d5d629a7fc3a03d8d92415e78aeb728
Binary files /dev/null and b/libs/UTIF-8205c1f.zip differ
diff --git a/libs/bootstrap-3.3.7-dist.zip b/libs/bootstrap-3.3.7-dist.zip
deleted file mode 100644
index 6fbb95ebaa3867ce196ef3b54951a732107d94d2..0000000000000000000000000000000000000000
Binary files a/libs/bootstrap-3.3.7-dist.zip and /dev/null differ
diff --git a/libs/bootstrap-3.4.1-dist.zip b/libs/bootstrap-3.4.1-dist.zip
new file mode 100644
index 0000000000000000000000000000000000000000..9002b8521706bc582546f41635da3437edf20c3c
Binary files /dev/null and b/libs/bootstrap-3.4.1-dist.zip differ
diff --git a/libs/bootstrap-autocomplete-2.3.0.zip b/libs/bootstrap-autocomplete-2.3.0.zip
deleted file mode 100644
index 206c00f49c87794e996c46da2f086d0a1d118071..0000000000000000000000000000000000000000
Binary files a/libs/bootstrap-autocomplete-2.3.0.zip and /dev/null differ
diff --git a/libs/bootstrap-autocomplete-2.3.5.zip b/libs/bootstrap-autocomplete-2.3.5.zip
new file mode 100644
index 0000000000000000000000000000000000000000..8cc2e03067955193bb89af2972a35ee9e0260b35
Binary files /dev/null and b/libs/bootstrap-autocomplete-2.3.5.zip differ
diff --git a/libs/jquery-3.3.1.zip b/libs/jquery-3.3.1.zip
deleted file mode 100644
index 404fac0639caf20bd4cf55419aa9a5f5bb768029..0000000000000000000000000000000000000000
Binary files a/libs/jquery-3.3.1.zip and /dev/null differ
diff --git a/libs/jquery-3.5.1.zip b/libs/jquery-3.5.1.zip
new file mode 100644
index 0000000000000000000000000000000000000000..c554bbf416786da9c18bef62061cba932646f996
Binary files /dev/null and b/libs/jquery-3.5.1.zip differ
diff --git a/libs/pako-dummy.zip b/libs/pako-dummy.zip
new file mode 100644
index 0000000000000000000000000000000000000000..e493ee9d673c81a523ad8e488c509c392765da52
Binary files /dev/null and b/libs/pako-dummy.zip 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 05e36429b7dfe6144915b522de2fa6c36375c484..69a700376423a44bcb28a9920f1f3d15ef9a3b90 100644
--- a/src/core/css/webcaosdb.css
+++ b/src/core/css/webcaosdb.css
@@ -27,6 +27,55 @@ 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;
+    display: inline-block;
+}
+
+.caosdb-v-bookmark-button,
+.caosdb-v-bookmark-button:focus,
+.caosdb-v-bookmark-button:hover {
+    color: #333;
+    position: relative;
+    top: 3px;
+}
+
+.caosdb-v-bookmark-button:active,
+.caosdb-v-bookmark-button.active {
+    color: red;
+}
+
+
 .caosdb-v-navbar-toolbox li a:hover,
 .caosdb-v-navbar-toolbox li input:hover,
 .caosdb-v-navbar-toolbox li button:hover {
diff --git a/src/core/js/autocomplete.js b/src/core/js/autocomplete.js
deleted file mode 100644
index 345d06b37bc6456645ef02947c70b3f2f5d6c44f..0000000000000000000000000000000000000000
--- a/src/core/js/autocomplete.js
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * ** 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
- */
-
-'use strict';
-
-var autocomplete = new function() {
-	this.version = "0.1";
-
-	/**
-	 * @type {Promise} promise for a list of entity names.
-	 */
-	this.candidates = undefined;
-	var logger = log.getLogger("autocomplete");
-	this.init = function() {
-		this.candidates = undefined;
-		this.toggle_completion();
-		logger.setLevel("trace");
-	};
-
-	this.retrieve_names = async function () {
-		var response = $(await connection.get(transaction.generateEntitiesUri(
-			["names"]))).find("Response [name]")
-
-		return response.toArray().map(x=> $(x).attr("name"));
-	};
-
-	/**
-	 * @return {Promise} promise for a list of entity names.
-	 */
-	this.get_candidates = function () {
-
-		if (typeof this.candidates === "undefined"){
-
-			this.candidates = this.retrieve_names();
-		};
-		return this.candidates;
-	};
-
-	this.filter = function (x, qry){
-		return x.toLowerCase().startsWith(qry.toLowerCase())
-	};
-
-	this.search = async function (qry, callback, origJQElement) { 
-		const names = await autocomplete.get_candidates();
-		callback(names.filter(x => autocomplete.filter(x, qry)));
-	};
-
-	this.typed = function (newValue, origJQElement) { 
-		var cursorpos = origJQElement[0].selectionEnd;
-		var beginning_of_word = newValue.slice(0, cursorpos).lastIndexOf(" ");
-		var word = newValue.slice(beginning_of_word+1, cursorpos);
-		return  word;
-	};
-
-	this.searchPost = function (resultsFromServer, origJQElement) { 
-		var cursorpos = origJQElement[0].selectionEnd;
-		var newValue = origJQElement[0].value
-		var beginning_of_word = newValue.slice(0, cursorpos).lastIndexOf(
-			" ");
-		var start = newValue.slice(0, beginning_of_word+1);
-		var end = origJQElement[0].value.slice(cursorpos);
-		var result = resultsFromServer.map( x => {
-			return {
-				text: start + x + end, 
-				html: x
-			}});
-		return result;
-	};
-
-    this.toggle_completion = function () {
-		var field  = $("#caosdb-query-textarea");
-		field.toggleClass("basicAutoComplete", true);
-		field.autoComplete({
-				events: {
-					search: this.search,
-		 			typed: this.typed,
-		 			searchPost: this.searchPost,
-				}
-			}
-		);
-		return field;
-	};
-};
-
-// this will be replaced by require.js in the future.
-$(document).ready(function() {
-    if ("${BUILD_MODULE_EXT_AUTOCOMPLETE}" === "ENABLED") {
-        caosdb_modules.register(autocomplete);
-    }
-});
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 0c52f403d5d9b2f0b2feb60a9dc21403c12dc0b2..9df7505f1e81fd0234aaa4461444535846921885 100644
--- a/src/core/js/edit_mode.js
+++ b/src/core/js/edit_mode.js
@@ -42,6 +42,11 @@ var edit_mode = new function() {
      */
     this.start_edit = new Event("caosdb.edit_mode.start_edit")
 
+    /**
+     * Fired by the entity element after editing (canceled or saved).
+     */
+    this.end_edit = new Event("caosdb.edit_mode.end_edit")
+
     /**
      * Fired by a list-property when an (input) element is added to the list during editing.
      */
@@ -407,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),
@@ -417,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
         );
     }
 
@@ -625,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();
@@ -1188,7 +1208,7 @@ var edit_mode = new function() {
      */
     this.delete_action = async function(entity) {
         var app = edit_mode.app;
-        // this is the callback of the delete button
+
         // show waiting notification
         edit_mode.smooth_replace(entity, app.waiting);
         // send delete request
@@ -1533,6 +1553,7 @@ var edit_mode = new function() {
             }
             hintMessages.hintMessages(app.entity);
             edit_mode.unfreeze();
+            app.entity.dispatchEvent(edit_mode.end_edit);
             resolve_references.init();
             preview.init();
         }
@@ -1780,7 +1801,24 @@ var edit_mode = new function() {
         $(entity).find(".caosdb-f-edit-mode-entity-actions-panel").append(button);
 
         $(button).click(() => {
-            edit_mode.delete_action(entity);
+            // hide other elements
+            const _alert = form_elements.make_alert({
+              title: "Warning",
+              message: "You are going to delete this entity permanently. This cannot be undone.",
+              proceed_callback: () => {
+                  edit_mode.delete_action(entity)
+              },
+              cancel_callback: () => {
+                  $(_alert).remove();
+                  $(entity).find(".caosdb-f-edit-mode-entity-actions-panel").show();
+              },
+              proceed_text: "Yes, delete!",
+              remember_my_decision_id : "delete_entity",
+            });
+            $(_alert).addClass("text-right");
+
+            $(entity).find(".caosdb-f-edit-mode-entity-actions-panel").after(_alert).hide();
+
         });
     }
 
diff --git a/src/core/js/ext_autocomplete.js b/src/core/js/ext_autocomplete.js
new file mode 100644
index 0000000000000000000000000000000000000000..9d99241485245ca6232a7626c04f7998579174e8
--- /dev/null
+++ b/src/core/js/ext_autocomplete.js
@@ -0,0 +1,161 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2019 IndiScale GmbH, Henrik tom Wörden
+ *
+ * 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 module uses and extends bootstrap-autocomplete to provide
+ * autocompletion to the query field.
+ * Currently, completion exists for names that are stored in CaosDB
+ */
+'use strict';
+
+var ext_autocomplete = new function () {
+    this.CQL_WORDS = [
+        "FIND",
+        "FILE",
+        "ENTITY",
+        "SELECT",
+        "COUNT",
+        "RECORD",
+        "PROPERTY",
+        "RECORDTYPE",
+        "REFERENCES",
+        "REFERENCED BY",
+        "WHICH",
+        "WITH",
+        "CREATED BY",
+        "CREATED ON",
+        "SOMEONE",
+        "STORED AT",
+        "HAS A PROPERTY",
+        "HAS BEEN",
+        "ANY VERSION OF",
+    ];
+    this.version = "0.1";
+
+
+    /**
+     * configure logging
+     */
+    var logger = log.getLogger("ext_autocomplete");
+
+    /**
+     * Initialize this module.
+     *
+     * Fetch candidates and prepare bootstrap-autocomplete.
+     */
+    this.init = async function () {
+        this.candidates = await this.retrieve_names();
+        this.switch_on_completion();
+    };
+
+
+    /**
+     * creates a list of names from the server resource Entity/names
+     */
+    this.retrieve_names = async function () {
+        var response = $(await connection.get(transaction.generateEntitiesUri(
+            ["names"]))).find("Property[name],RecordType[name],Record[name]")
+
+        response = response.toArray().map(x => $(x).attr("name"));
+        response = response.concat(ext_autocomplete.CQL_WORDS);
+
+        return response
+    };
+
+    /**
+     * case insensitive filter that returns elements that start with qry
+     */
+    this.starts_with_filter = function (x, qry) {
+        return x.toLowerCase().startsWith(qry.toLowerCase())
+    };
+
+    /**
+     * Overwrites the default search function of bootstrap-autocomplete
+     * uses candidates and filters the list using filter
+     */
+    this.search = async function (qry, callback, origJQElement) {
+        callback(ext_autocomplete.candidates.filter(
+            x => ext_autocomplete.starts_with_filter(x, qry)));
+    };
+
+    /**
+     * Overwrites the default typed function of bootstrap-autocomplete
+     * This assures that only the word before the cursor is used for completion
+     */
+    this.typed = function (newValue, origJQElement) {
+        var cursorpos = origJQElement[0].selectionEnd;
+        var beginning_of_word = newValue.slice(0, cursorpos).lastIndexOf(" ");
+        var word = newValue.slice(beginning_of_word + 1, cursorpos);
+        return word;
+    };
+
+    /**
+     * Overwrites the default searchPost function of bootstrap-autocomplete
+     * This assures that only the word before the cursor is replaced.
+     * It is achieved by returning not a simple list, but an object where each
+     * element has "text" and "html" fields, where "html" is what is visible
+     * in the dropdown and "text" will be inserted.
+     */
+    this.searchPost = function (resultsFromServer, origJQElement) {
+        var cursorpos = origJQElement[0].selectionEnd;
+        var newValue = origJQElement[0].value
+        var beginning_of_word = newValue.slice(0, cursorpos).lastIndexOf(
+            " ");
+        var start = newValue.slice(0, beginning_of_word + 1);
+        var end = origJQElement[0].value.slice(cursorpos);
+        var result = resultsFromServer.map(x => {
+            return {
+                text: start + x + end,
+                html: x
+            }
+        });
+        return result;
+    };
+
+    /**
+     * enables autocompletion in the query field
+     */
+    this.switch_on_completion = function () {
+        var field = $("#caosdb-query-textarea");
+        field.attr("autocomplete", "off");
+        field.toggleClass("basicAutoComplete", true);
+        field.autoComplete({
+            events: {
+                search: this.search,
+                typed: this.typed,
+                searchPost: this.searchPost,
+            },
+            noResultsText: 'No autocompletion suggestions',
+            bootstrapVersion: "3",
+
+        });
+
+        return field;
+    };
+};
+
+// this will be replaced by require.js in the future.
+$(document).ready(function () {
+    if ("${BUILD_MODULE_EXT_AUTOCOMPLETE}" == "ENABLED") {
+        caosdb_modules.register(ext_autocomplete);
+    }
+});
diff --git a/src/core/js/ext_bookmarks.js b/src/core/js/ext_bookmarks.js
new file mode 100644
index 0000000000000000000000000000000000000000..d99ff362d8a26ee4818a76a624c80f55f757c419
--- /dev/null
+++ b/src/core/js/ext_bookmarks.js
@@ -0,0 +1,730 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 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';
+
+
+/**
+ * Keep track of bookmarked entities and provide functions for export, viewing
+ * all entities and resetting the bookmarks.
+ *
+ * @module ext_bookmarks
+ * @version 0.2
+ *
+ * @param jQuery - well-known library.
+ * @param log - singleton from loglevel library or javascript console.
+ * @param {ext_bookmarks_config} config
+ *
+ * @type ext_bookmarks_config
+ * @property {function} [get_context_root] - param-less function which returns the
+ *     uri of the collection resource which returns the bookmarked entities.
+ * @property {string} [data_attribute=data-bmval] - name of the data-attribute
+ *     which contains the bookmark key.
+ * @property {Storage} [bookmark_storage=localStorage] - the Storage
+ *     implementation which is used for storing bookmarks.
+ * @property {function} [set_collection_link] - a function which sets the
+ *     collection link.
+ * @property {function} [set_counter] - a function which sets the counter
+ * @property {function} [getPaging] - a function which returns the paging string
+ * @property {function} [set_export_button_click] - a function which sets the click
+ *     event handler of the export button.
+ * @property {function} [set_clear_button_click] - a function which sets the click
+ *     event handler of the clear button.
+ * @property {string[]} [tsv_columns=["URI"]]
+ * @property {function} [get_export_table_row] - function which receives the
+ *     bookmarked id and must return an array of columns.
+ * @property {object} [data_getters={}] - a dictionary of functions which
+ *     retrieve bookmark data for a given bookmark id and a data_key.
+ * @property {string[]} [data_no_cache] - an array of data_keys which are not
+ *     to be cached.
+ */
+var ext_bookmarks = function ($, logger, config) {
+
+    config = config || {};
+
+    /**
+     * This counter is used as a cache for the number of current bookmarks. It
+     * is used to quickly change the counter in the bookmarks menu and lazily
+     * updated.
+     */
+    var counter = 0;
+
+    /**
+     * Currently this is mainly usefull for testing, but in the future it might
+     * be desirable to have multiple bookmark collection at the same time. It
+     * would be easy to extent this class for this because the collection_id is
+     * used in the generated links and as part of the storage key in the
+     * bookmark_storage.
+     */
+    var collection_id = config["collection_id"] || 0;
+
+    const data_getters = config["data_getters"] || {
+        "URI": (id) => get_context_root() + id
+    };
+    const data_no_cache = config["data_no_cache"] || ["URI"];
+
+    const data_attribute = config["data_attribute"] || "data-bmval";
+
+    /**
+     * Return all bookmark buttons on this page or which are children of scope.
+     *
+     * @param {HTMLElement|string} [scope='body']
+     * @return {HTMLElement[]} array of bookmark buttons.
+     */
+    const get_bookmark_buttons = function (scope) {
+        return $(scope || "body").find(`[${data_attribute}]`).toArray();
+    }
+
+    /**
+     * Sets the click event handler of the clear button.
+     *
+     * @param {function} cb - event handler.
+     */
+    const set_clear_button_click = config["set_clear_button_click"] || function (cb) {
+        $("#caosdb-f-bookmarks-clear")
+            .toggleClass("disabled", !cb)
+            .on("click", cb);
+    }
+
+    /**
+     * Sets the click event handler of the export button.
+     *
+     * @param {function} cb - event handler.
+     */
+    const set_export_button_click = config["set_export_button_click"] || function (cb) {
+        $("#caosdb-f-bookmarks-export-link")
+            .toggleClass("disabled", !cb)
+            .on("click", cb);
+    }
+
+    const getPaging = config["getPaging"] || (() => "?P=0L10");
+
+    /**
+     * The storage backend for the bookmarks.
+     */
+    const bookmark_storage = config["bookmark_storage"] || window.localStorage;
+
+    /**
+     * Set the href attribute of the bookmark collection link.
+     *
+     * @param {string} uri
+     */
+    const set_collection_link = config["set_collection_link"] || function (uri) {
+        const link = $("#caosdb-f-bookmarks-collection-link")
+            .toggleClass("disabled", !uri)
+            .find("a");
+        if (uri) {
+            link.attr("href", uri);
+        } else {
+            link.removeAttr("href");
+        }
+    }
+
+    /**
+     * Set the counter badge in the bookmark menu.
+     */
+    const update_counter = config["set_counter"] || function (counter) {
+        $("#caosdb-f-bookmarks-collection-counter").text(counter);
+    }
+
+    const get_context_root = config["get_context_root"] || (() => "");
+
+    /**
+     * This is used as a prefix of the key in the bookmark_storage.
+     */
+    const key_prefix = "_bm_";
+
+    /**
+     * This marker is used to identify uris which specify a bookmark collection
+     * which should be reloaded.
+     */
+    const uri_marker = "_bm_";
+
+    /**
+     * Extract the bookmark id from the bookmark button.
+     *
+     * @param {HTMLElement} button - the bookmark button.
+     * @return {string} the bookmark id.
+     */
+    const get_value = function (button) {
+        const result = $(button).attr(data_attribute);
+        return result;
+    }
+
+    /**
+     * Construct the prefix of the key for the bookmark_storage.
+     *
+     * This can be used to construct the item key and the data key
+     * to delete all storage keys which belong to the current bookmark
+     * collection.
+     *
+     * @param {string}
+     */
+    const get_collection_prefix = function () {
+        return key_prefix + collection_id;
+    }
+
+    /**
+     * Generate the key for the bookmark_storage.
+     *
+     * @param {string} val - the value which is used to generate the key.
+     */
+    const get_key = function (val) {
+        return get_collection_prefix() + '_it_' + val;
+    }
+
+
+    /**
+     * These will be the columns in the TSV file. For each column there should
+     * exist a data_getter.
+     */
+    const tsv_columns = config["tsv_columns"] || ["URI"];
+
+    /**
+     * Generate a single TSV row
+     *
+     * @return {string[]} array of row columns
+     */
+    const get_export_table_row = async function (id) {
+        const row = [];
+        for (var col of tsv_columns) {
+            row.push(await get_bookmark_data(id, col));
+        }
+        return row;
+    }
+
+    /**
+     * 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.
+     * @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";
+        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));
+        }
+        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.
+     *
+     * This is the call-back for the export button.
+     */
+    const export_bookmarks = async function () {
+        const ids = get_bookmarks();
+        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");
+    }
+
+    /**
+     * Return all current bookmarks.
+     *
+     * @return {string[]} array of bookmarked ids.
+     */
+    const get_bookmarks = function () {
+        const result = [];
+
+        const storage_key_prefix = get_key("");
+        for (let i = 0; i < bookmark_storage.length; i++) {
+            const key = bookmark_storage.key(i);
+            if (key.indexOf(storage_key_prefix) > -1) {
+                result.push(bookmark_storage[key]);
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Update the clear button (i.e. add a click handler which resets the bookmarks.)
+     *
+     * @param {string[]} bookmarks - array of ids.
+     */
+    const update_clear_button = function (bookmarks) {
+        if (bookmarks.length > 0) {
+            set_clear_button_click(clear_bookmark_storage);
+        } else {
+            set_clear_button_click(false);
+        }
+    }
+
+    /**
+     * Update the export button (i.e. add a click handler which generates the
+     * tsv file.)
+     *
+     * @param {string[]} bookmarks - array of ids.
+     */
+    const update_export_link = function (bookmarks) {
+        if (bookmarks.length > 0) {
+            set_export_button_click(export_bookmarks);
+        } else {
+            set_export_button_click(false);
+        }
+    }
+
+    /**
+     * Generate the uri for the collection of all bookmarked entities.
+     *
+     * @param {string[]} bookmarks - array of ids.
+     * @return {string} uri
+     */
+    const get_collection_link = function (bookmarks) {
+        const uri_segment = bookmarks.join("&");
+        return get_context_root() + uri_segment + getPaging() +
+            "#" + uri_marker + collection_id;
+    }
+
+    /**
+     * Update the link of the collection of bookmarks in the bookmark drop down
+     * menu.
+     *
+     * @param {string[]} bookmarks - array of ids.
+     */
+    const update_collection_link = function (bookmarks) {
+        if (bookmarks.length > 0) {
+            const link = get_collection_link(bookmarks);
+            set_collection_link(link);
+        } else {
+            set_collection_link(false);
+        }
+    }
+
+    /**
+     * Syncronize the bookmark_storage and currently visible bookmark button and
+     * update all buttons and other visible elements and the bookmarks drop
+     * down menu.
+     *
+     * @param {string[]} [bookmarks] - array of ids. If omitted, the
+     * get_bookmarks function is being called.
+     */
+    const update_collection = function (bookmarks) {
+        bookmarks = bookmarks || get_bookmarks();
+        update_counter(bookmarks.length);
+        update_collection_link(bookmarks);
+        update_export_link(bookmarks);
+        update_clear_button(bookmarks);
+    }
+
+    /**
+     * Toggle the active class of the button and change the title of the button
+     * accordingly.
+     *
+     * @param {HTMLElement} button - bookmark button
+     * @param {boolean} is_active - whether the new state is active or not.
+     */
+    const set_button_state = function (button, is_active) {
+        $(button).toggleClass("active", is_active);
+        if (is_active) {
+            $(button).attr("title", "Remove bookmark");
+        } else {
+            $(button).attr("title", "Add bookmark");
+        }
+    }
+
+    /**
+     * Event handler for the click event of the bookmark buttons.
+     *
+     * Toggles the buttons state and adds or removes the bookmark.
+     *
+     * @param {Event} e - the click event;
+     */
+    const toggle_bookmark_active = function (e) {
+        const button = $(this);
+
+        const new_is_active = !button.is(".active");
+        set_button_state(button, new_is_active);
+
+        const value = get_value(button[0]);
+        const key = get_key(value);
+        if (new_is_active) {
+            bookmark_storage.setItem(key, value);
+            update_counter(++counter);
+
+            // fill the cache immediately. This is a good idea, because many
+            // data_getters can work on the DOM tree when the bookmark is being
+            // selected. Later, when the user has left the current page, the
+            // getters might need to request the database. We want to prevent
+            // that.
+            collect_bookmark_data(value);
+        } else {
+            bookmark_storage.removeItem(key);
+            update_counter(--counter);
+            remove_bookmark_data(value);
+        }
+        update_collection();
+    }
+
+    /**
+     * Fill the cache with data for the export for a bookmark.
+     *
+     * @param {string} id - bookmark id.
+     */
+    const collect_bookmark_data = function (id) {
+        for (let data_key in data_getters) {
+            if (data_no_cache.indexOf(data_key) == -1) {
+                // do nothing, only trigger the fetching
+                get_bookmark_data(id, data_key)
+            }
+        }
+    }
+
+    /**
+     * Remove all data item which belong to a bookmark.
+     *
+     * @param {string} id - bookmark id.
+     */
+    const remove_bookmark_data = function (id) {
+        const data_key_prefix = get_data_key(id, "");
+        remove_from_storage_by_prefix(data_key_prefix);
+    }
+
+    /**
+     * Initialize a single bookmark button.
+     *
+     * Fetch the state from the bookmark_storage and set the bookmark button to
+     * active or inactive. Also add the onclick handler which toggles the
+     * bookmark state.
+     *
+     * @param {HTMLElement} button - The bookmark button
+     */
+    const init_button = function (button) {
+        // load state
+        const key = get_key(get_value(button));
+        const is_bookmarked = !!key && !!bookmark_storage[key];
+        set_button_state(button, is_bookmarked);
+
+        // onlick handler
+        button.onclick = toggle_bookmark_active;
+    }
+
+    /**
+     * Remove all items in the bookmark_storage by a prefix.
+     *
+     * Useful for resetting the whole bookmark_storage or just deleting a
+     * single item along with its data items.
+     *
+     * @param {string} prefix
+     */
+    const remove_from_storage_by_prefix = function (prefix) {
+        const keys = [];
+        for (let i = 0; i < bookmark_storage.length; i++) {
+            const key = bookmark_storage.key(i);
+            if (key.indexOf(prefix) > -1) {
+                keys.push(key);
+            }
+        }
+        for (let i = 0; i < keys.length; i++) {
+            bookmark_storage.removeItem(keys[i]);
+        }
+    }
+
+    /**
+     * Remove all bookmarks, clear the counter and reset the buttons
+     */
+    const clear_bookmark_storage = function () {
+        counter = 0;
+        update_collection([]);
+
+        // reset all buttons
+        get_bookmark_buttons().forEach((x) => {
+            set_button_state(x, false);
+        });
+
+        const storage_key_prefix = get_collection_prefix()
+        remove_from_storage_by_prefix(storage_key_prefix);
+    }
+
+    /**
+     * Add all bookmarks to storage.
+     *
+     * @param {string[]} ids - an array of ids.
+     */
+    const add_all_bookmarks_to_storage = function (ids) {
+        counter = counter + ids.length;
+        for (let i = 0; i < ids.length; i++) {
+            const id = ids[i];
+            if (id) {
+                bookmark_storage[get_key(id)] = id
+            }
+        }
+        update_collection();
+    }
+
+    /**
+     * Parse a list of bookmarked entities and the collection_id from and URI.
+     *
+     * @param {string} uri
+     * @return {object} dict with two keys:
+     *    {string[]} bookmarks
+     *    {string} _collection_id
+     */
+    const parse_uri = function (uri) {
+        var cut_index = uri.indexOf("#" + uri_marker);
+        if (cut_index > -1) {
+            // get collection id
+            const _collection_id = uri
+                .substring(cut_index + uri_marker.length + 1)
+            // remove query
+            const query_marker = uri.indexOf("?");
+            if (query_marker > -1) {
+                cut_index = query_marker;
+            }
+            const uri_segments = uri.substring(0, cut_index).split("/")
+            const ids = uri_segments[uri_segments.length - 1].split("&");
+            logger.debug("found ids in uri", ids);
+            return {
+                bookmarks: ids,
+                collection_id: _collection_id
+            };
+        }
+        return undefined;
+    }
+
+
+    /**
+     * Initialize all bookmark buttons which are children of scope.
+     *
+     * @param {HTMLElement|string} [scope="body"] - element or jquery selector.
+     */
+    const init_bookmark_buttons = function (scope) {
+        logger.trace("enter init_bookmark_buttons", scope);
+        $(get_bookmark_buttons(scope)).each((idx, button) => {
+            init_button(button);
+        });
+    }
+
+    /**
+     * Setter for the collection_id.
+     *
+     * @param {string} id
+     */
+    const set_collection_id = function (id) {
+        collection_id = id;
+    }
+
+    /**
+     * Initialize this module.
+     */
+    const init = async function (scope) {
+        logger.info("init ext_bookmarks");
+        //add_bookmark_buttons();
+        counter = 0;
+        const parsed_uri = parse_uri(window.location.href);
+        if (typeof parsed_uri != "undefined") {
+            // this hack removes the "#_bm" marker from the uri without
+            // reloading the page.
+            window.location.href = "#";
+
+            clear_bookmark_storage();
+            collection_id = parsed_uri["collection_id"];
+            add_all_bookmarks_to_storage(parsed_uri["bookmarks"]);
+        }
+
+        init_bookmark_buttons(scope);
+        update_collection();
+        if (edit_mode) {
+            window.document.body.addEventListener(edit_mode.end_edit.type, (e) => {
+                init_bookmark_buttons(e.target);
+            }, true);
+        }
+    }
+
+    /**
+     * Construct the key for data items in the bookmark_storage.
+     */
+    const get_data_key = function (id, data_key) {
+        return get_collection_prefix() + '_da_' + id + "_" + data_key;
+    }
+
+    /**
+     * Get a specific data item which belongs to a bookmark.
+     *
+     * This is currently prominently used by the tsv-export.
+     *
+     * @param {string} id - the bookmarked id
+     * @param {string} data_key - an identifier for the data item to be
+     * retrieved.
+     * @returns {string} the `data_key` of bookmark `id`.
+     */
+    const get_bookmark_data = async function (id, data_key) {
+        // get full key (needed for the cache)
+        const full_data_key = get_data_key(id, data_key);
+
+        // retrieve from cache
+        const cached = bookmark_storage[full_data_key];
+        if (typeof cached != "undefined") {
+            return cached;
+        }
+
+        // not in cache, try the data_getters
+        var uncached = undefined
+        if (data_getters[data_key]) {
+            uncached = (await data_getters[data_key](id))
+        }
+
+        // don't cache if getting the information is trivial or there are other
+        // reasons why this is in the data_no_cache array.
+        if (data_no_cache.indexOf(data_key) == -1) {
+            bookmark_storage[full_data_key] = uncached || "";
+        }
+        return uncached;
+    }
+
+    return {
+        init: init,
+        parse_uri: parse_uri,
+        get_bookmarks: get_bookmarks,
+        get_key: get_key,
+        get_value: get_value,
+        set_collection_id: set_collection_id,
+        bookmark_storage: bookmark_storage,
+        get_export_table: get_export_table,
+        clear_bookmark_storage,
+        clear_bookmark_storage,
+        update_clear_button: update_clear_button,
+        update_export_link: update_export_link,
+        update_collection_link: update_collection_link,
+        get_collection_link: get_collection_link,
+        get_bookmark_buttons: get_bookmark_buttons,
+        init_button: init_button,
+        get_bookmark_data: get_bookmark_data,
+    }
+};
+
+
+
+$(document).ready(function () {
+    if ("${BUILD_MODULE_EXT_BOOKMARKS}" == "ENABLED") {
+        // The following is the configuration for the CaosDB WebUI.
+        const get_context_root = (() => connection.getBasePath() + "Entity/");
+
+        // This getter retrieves a file's path from the page or, if necessary,
+        // from the server.
+        const get_path = async function (id) {
+            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", "Name", "RecordType"];
+        // functions for collecting the export data for a particular bookmarked id.
+        const data_getters = {
+            "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": async (id) => get_context_root() + id + (id.indexOf("@") > -1 ? "" : ("@" + await get_version(id))),
+            "Name": get_name,
+            "RecordType": get_rt,
+        };
+
+        // 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,
+            tsv_columns: tsv_columns,
+            data_getters: data_getters,
+            data_no_cache: data_no_cache,
+        };
+
+        ext_bookmarks = ext_bookmarks($, log.getLogger("ext_bookmarks"), config);
+        caosdb_modules.register(ext_bookmarks);
+    }
+});
diff --git a/src/core/js/ext_bottom_line.js b/src/core/js/ext_bottom_line.js
index 8c657698cfb9d076b1252f99cab8d57e01239f50..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,
@@ -42,8 +64,10 @@
  * @requires load_config (function from caosdb.js)
  * @requires getEntityPath (function from caosdb.js)
  * @requires connection (module from webcaosdb.js)
+ * @requires UTIF (from utif.js library)
+ * @requires ext_table_preview (module from ext_table_preview.js)
  */
-var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntityPath, connection, ext_applicable) {
+var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntityPath, connection, UTIF, ext_table_preview, ext_applicable) {
 
     const contentShownEvent = new Event("ext_bottom_line.content.shown");
     const contentReadyEvent = new Event("ext_bottom_line.content.ready");
@@ -57,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.
-     */
 
 
     /**
@@ -89,7 +92,91 @@ var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntit
      */
     const _create_video_preview = function(entity) {
         var path = connection.getFileSystemPath() + getEntityPath(entity);
-        return $(`<div class="caosdb-v-bottom-line-video-preview"><video controls="controls"><source src="${path}"/></video></div>`)[0];
+        return $(`<div class="caosdb-v-bottom-line-video-preview">
+          <video controls="controls"><source src="${path}"/></video></div>`)[0];
+    }
+
+    /**
+     * Error class which has the special to_html method.
+     *
+     * The to_html method creates a html representation of the error which is
+     * intended for displaying in the bottom_line container.
+     */
+    const BottomLineError = function(arg) {
+        this._is_bottom_line_error = true;
+
+        if (arg.message) {
+            // arg is an Error object
+            this.message = arg.message;
+            this.stack = arg.stack;
+        } else {
+            this.message = arg;
+        }
+
+        this.to_html = function() {
+            return $(`<div><p>An error occured while loading this preview.<p>${
+              this.message}<div>`)[0];
+        }
+    }
+
+    const BottomLineWarning = function (arg) {
+        this._is_bottom_line_error = true;
+
+        if (arg.message) {
+            // arg is an Error object
+            this.message = arg.message;
+            this.stack = arg.stack;
+        } else {
+            this.message = arg;
+        }
+
+        this.to_html = function() {
+            return $(`<div>${this.message}<div>`)[0];
+        }
+    }
+
+    /**
+     * Create a preview for tiff files.
+     *
+     * Tiff files are decompressed if necessary and converted into png by UTIF library.
+     *
+     * @param {HTMLElement} entity
+     * @return {Promise | HTMLElement} Promise for an IMG element.
+     */
+    const _create_tiff_preview = function(entity) {
+        const path = connection.getFileSystemPath() + getEntityPath(entity);
+        const result = $(`<div class="caosdb-v-bottom-line-image-preview"></div>`);
+        const img = $(`<img src="${path}"/>`)[0];
+        result.append(img);
+
+        /**
+         * Promise which retrieves the tiff file and calls the UTIF library for
+         * decompression and conversion into png.
+         */
+        return new Promise((resolve, reject) => {
+            const xhr = new XMLHttpRequest();
+            UTIF._xhrs.push(xhr);
+            UTIF._imgs.push(img);
+            xhr.open("GET", path);
+            xhr.responseType = "arraybuffer";
+            xhr.onload = (e) => {
+                try {
+                  // decompress and convert tiff file
+                  UTIF._imgLoaded(e);
+
+                  // return the result if no error occured.
+                  resolve(result);
+                } catch(err) {
+                  // throw errors from UTIF to the awaiting caller.
+                  reject(new BottomLineError(err));
+                }
+            }
+            // throw http errors to the awaiting caller.
+            xhr.onerror = reject;
+
+            // this finally triggers the retrieval
+            xhr.send();
+        });
     }
 
     /**
@@ -105,6 +192,8 @@ var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntit
 
     var fallback_preview = undefined;
 
+    const _tiff_preview_enabled = "${BUILD_MODULE_EXT_BOTTOM_LINE_TIFF_PREVIEW}" == "ENABLED";
+
     /**
      * Default creators.
      *
@@ -114,12 +203,21 @@ var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntit
             id: "_default_creators.pictures",
             is_applicable: (entity) => ext_applicable.helpers.path_has_file_extension(
                 entity, ["jpg", "png", "gif", "svg"]),
-            create: _create_picture_preview
+            create: _create_picture_preview,
+        }, {
+            id: "_default_creators.tiff_images",
+            is_applicable: (entity) => _tiff_preview_enabled && _path_has_file_extension(
+                entity, ["tif", "tiff","dng","cr2","nef"]),
+            create: _create_tiff_preview,
         }, { // videos
             id: "_default_creators.videos",
             is_applicable: (entity) => ext_applicable.helpers.path_has_file_extension(
                 entity, ["mp4", "mov", "webm"]),
             create: _create_video_preview,
+        }, { // tables
+            id: "_default_creators.table_preview",
+            is_applicable: (e) => ext_table_preview.is_table(e),
+            create: (e) => ext_table_preview.get_preview(e),
         },
 
     ];
@@ -137,6 +235,71 @@ var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntit
             _css_class_preview_container}`)[0];
     }
 
+    /**
+     * Append a preview to the entity and removes any pre-existing preview.
+     *
+     * If the preview is Promise for a preview a waiting notification is added
+     * to the entity instead and the actual preview is added after the Promise
+     * is resolved. If the Promise is rejected, a correspondig error is shown
+     * instead.
+     *
+     * @see root_preview_handler
+     *
+     * @async
+     * @param {HTMLElement} entity
+     * @param {string|HTMLElement|Promise} preview - A preview for an entity or
+     *     a Promise for a preview (which resolves as a string or an HTMLElement as well).
+     */
+    /*
+    var set_preview = async function(entity, preview) {
+        try {
+            const wait = "Please wait...";
+            set_preview_container(entity, wait);
+            const result = await preview;
+            set_preview_container(entity, result);
+            if (result) {
+                entity.dispatchEvent(previewReadyEvent);
+            }
+        } catch (err) {
+            logger.error(err);
+            if (!err._is_bottom_line_error) {
+              err = new BottomLineError(err);
+            }
+            set_preview_container(entity, err.to_html());
+        }
+    }*/
+
+    /**
+     * Create and return a preview for a given entity.
+     *
+     * This root_preview_creator iterates over all the registered creators and
+     * uses the first match, i.e. the first creator object which return true
+     * for the `is_applicable(entity)` method of the creator object.
+     *
+     * If a creator throws an error during checking whether it `is_applicable`
+     * or during the `create` the error is logged and the creator is treated as
+     * if it were not applicable.
+     *
+     * @async
+     * @param {HTMLElement} entity - the entity for which the preview is to be
+     *     created.
+     * @returns {String|HTMLElement|Promise} A preview which can be added to
+     *     the entity DOM representation or a Promise for such a preview.
+     */
+    /*
+    var root_preview_creator = async function(entity) {
+        for (let c of _creators) {
+            try {
+                if (await c.is_applicable(entity)) {
+                    return c.create(entity);
+                }
+            } catch (err) {
+                logger.error(err);
+            }
+        }
+        return undefined;
+    };*/
+
     /**
      * Add a preview container to the entity.
      *
@@ -265,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,
@@ -274,7 +440,7 @@ var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntit
         get_container: get_container,
         _css_class_preview_container: _css_class_preview_container,
     }
-}($, log.getLogger("ext_bottom_line"), resolve_references.is_in_viewport_vertically, load_config, getEntityPath, connection, ext_applicable);
+}($, log.getLogger("ext_bottom_line"), resolve_references.is_in_viewport_vertically, load_config, getEntityPath, connection, UTIF, ext_table_preview, ext_applicable);
 
 
 /**
@@ -297,20 +463,23 @@ var plotly_preview = function(logger, ext_bottom_line, plotly) {
      *     to be plotted.
      * @param {object[]} layout - dictionary of settings defining the layout of
      *     the plot.
+     * @param {object[]} settings - object containing additional
+     *     settings for the plot.
      * @returns {HTMLElement} the element which contains the plot.
      */
     const create_plot = function(data,
-        layout = {
-            margin: {
-                t: 0
-            },
-            height: 400,
-            widht: 400
-        }) {
+				 layout = {
+				     margin: {
+					 t: 0
+				     },
+				     height: 400,
+				     widht: 400
+				 },
+				 settings = {
+				     responsive: true
+				 }) {
         var div = $('<div/>')[0];
-        plotly.newPlot(div, data, layout, {
-            responsive: true
-        });
+        plotly.newPlot(div, data, layout, settings);
         return div;
     }
 
@@ -338,7 +507,7 @@ var plotly_preview = function(logger, ext_bottom_line, plotly) {
 
 // this will be replaced by require.js in the future.
 $(document).ready(function() {
-    if ("${BUILD_MODULE_EXT_BOTTOM_LINE}" === "ENABLED") {
+    if ("${BUILD_MODULE_EXT_BOTTOM_LINE}" == "ENABLED") {
         caosdb_modules.register(plotly_preview);
         caosdb_modules.register(ext_bottom_line);
     }
diff --git a/src/core/js/ext_file_download.js b/src/core/js/ext_file_download.js
new file mode 100644
index 0000000000000000000000000000000000000000..477349b421e03d74aa28fddf6b4748f64ffa9f7b
--- /dev/null
+++ b/src/core/js/ext_file_download.js
@@ -0,0 +1,186 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2020 Henrik tom Wörden <h.tomwoerden@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';
+
+/**
+ * The ext_file_download module provides a very basic preview for table files.
+ *
+ * The preview is generated using a server side script.
+ *
+ * @module ext_file_download 
+ * @version 0.1
+ *
+ * @requires jQuery
+ * @requires log
+ */
+var ext_file_download  = function ($, logger) {
+
+    /**
+     * collect the file ids that will be passed to the zip script
+     *
+     * @return {string[]} array of entity ids
+     */
+    const collect_ids = function(){
+        var properties = $(".caosdb-f-property-value").find(
+            ".caosdb-id.caosdb-f-property-single-raw-value");
+        var id_list = properties.toArray().map(x=>x.textContent);
+        var entities = $("tr[data-entity-id]")
+        id_list = id_list.concat(entities.toArray().map(
+            x=>x.attributes['data-entity-id'].value)); 
+        return id_list
+    };
+
+    /**
+     * chunk list to smaller pieces.
+     *
+     * @param {string[]} list - array of (usually) ids
+     * @param {Number} size - chunk size (use integer)
+     * @return {string[][]} array of array of string from the original list.
+     */
+    const chunk_list = function(list, size){
+        size = size || 20;
+        var pieces = [];
+        var index = 0;
+        while (index < list.length){
+            pieces.push(list.slice(index,index+size));
+            index+=size;
+        }
+
+        return pieces
+    };
+
+
+    /**
+     * create select statement to find files.
+     *
+     * @param {string[]} id_list - array of ids
+     * @return {string} a query string
+     */
+    const query_files_str = function(id_list){
+        var povs = id_list.map(x=>` id=${x} `);
+        const query_str="SELECT ID FROM FILE WITH " + povs.join("or");
+        return query_str;
+    };
+
+    /**
+     * Reduce id list to files. Throw away all ids which do not belong to a
+     * file entity.
+     *
+     * @param {string[]} id_list - array of ids
+     * @return {string[]} array of file ids.
+     */
+    const reduce_ids = async function(id_list){
+        var file_ids = []
+        for (var part of chunk_list(id_list)) {
+            // query for files
+            var result = await query(query_files_str(part));
+            file_ids=file_ids.concat(result.map(x => getEntityID(x)));
+        }
+        return file_ids;
+    };
+
+
+    /**
+     * Callback function for the download files link.
+     *
+     * Collects all the file entities and sends them to a server-side script
+     * which then puts the files into a zip.
+     *
+     * @param {HTMLElement} zip_link - the link element which triggered this
+     *     call and which will be disabled during the execution of this
+     *     function in order to prevent the user from triggering the process
+     *     twice.
+     */
+    const download_files = async function (zip_link) {
+        const onClickValue = zip_link.getAttribute("onClick");
+        try {
+            // remove click event handler which called this function in the first place
+            zip_link.removeAttribute("onClick");
+
+            // add loading info. TODO make an animated one
+            $("#downloadModal").find(".caosdb-f-modal-footer-left").append(
+                createWaitingNotification("Collecting files...")
+            );
+
+            var ids = collect_ids();
+            ids = await reduce_ids(ids);
+            if (ids.length == 0){
+                alert ("There are no file entities in this table.");
+                return;
+            }
+
+            var table = await ext_bookmarks.get_export_table(ids, "", "\t", "\n");
+
+            const result = await connection.runScript(
+                "ext_file_download/zip_files.py",
+                {
+                  "-p0": ids,
+                  "-p1": table,
+                }
+            );
+            const code = result.getElementsByTagName("script")[0].getAttribute("code");
+            if (parseInt(code) > 0) {
+                throw ("An error occurred during execution of the server-side script:\n"
+                    + result.getElementsByTagName("script")[0].outerHTML);
+            }
+            const filename = result.getElementsByTagName("stdout")[0].textContent;
+            if (filename.length == 0) {
+                throw("Server-side script produced no file or did not return the file name: \n"
+                    + result.getElementsByTagName("script")[0].outerHTML);
+            }
+
+
+            // trigger download of generated file
+            caosdb_table_export.go_to_script_results(filename);
+
+            //close modal
+            $("#downloadModal").find(".modal-footer").find(".btn")[0].click();
+        } catch (e) {
+            globalError(e);
+        } finally {
+            removeAllWaitingNotifications($("#downloadModal")[0]);
+            // restore the old click handler - hence a new file is generated with each click.
+            zip_link.setAttribute("onClick", onClickValue);
+        }
+
+    };
+
+    const init = function () {
+        // only enable when init is being called
+        logger.info("init ext_file_download");
+        if (userIsAnonymous()) {
+            $("#caosdb-f-query-select-files").parent().hide();
+        }
+    };
+
+    return {
+        init: init,
+        download_files: download_files,
+        collect_ids: collect_ids,
+        chunk_list: chunk_list,
+    };
+
+}($, log.getLogger("ext_file_download"));
+
+// This module is registered by caosdb_table_export.
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_references.js b/src/core/js/ext_references.js
index 7aecb16a9c8158683f84e364dc1362103f296c45..06bed856ff83f39b4f27ae54a4520c5fe4e6608b 100644
--- a/src/core/js/ext_references.js
+++ b/src/core/js/ext_references.js
@@ -460,9 +460,11 @@ var resolve_references = new function () {
 
         // Loop over all property values in the container element. Note: each property_value can be a single reference or a list of references.
         for (const property_value of property_values) {
-            const lists = $(property_value).find(
-                ".caosdb-value-list").has(
-                `.${_unresolved_class_name}`);
+            var lists = findElementByConditions(
+                property_value, 
+                x => x.classList.contains("caosdb-value-list"), 
+                x => x.classList.contains("caosdb-preview-container"))
+            lists = $(lists).has(`.${_unresolved_class_name}`);
 
             if (lists.length > 0) {
                 logger.debug("processing list of references", lists);
@@ -535,8 +537,10 @@ var resolve_references = new function () {
 
             // Load all remaining references. These are single reference values
             // and those references from lists which are left for lazy loading.
-            const rs = $(property_value).find(
-                `.${_unresolved_class_name}`);
+            const rs = findElementByConditions(
+                property_value, 
+                x => x.classList.contains(`${_unresolved_class_name}`), 
+                x => x.classList.contains("caosdb-preview-container"));
             for (var i = 0; i < rs.length; i++) {
                 if (resolve_references.is_in_viewport_vertically(
                         rs[i]) &&
diff --git a/src/core/js/ext_table_preview.js b/src/core/js/ext_table_preview.js
new file mode 100644
index 0000000000000000000000000000000000000000..1d9da6fa9334a52de5eb39a66b585c7eb67cd120
--- /dev/null
+++ b/src/core/js/ext_table_preview.js
@@ -0,0 +1,97 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2020 Henrik tom Wörden <h.tomwoerden@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';
+
+/**
+ * The ext_table_preview module provides a very basic preview for table files.
+ *
+ * The preview is generated using a server side script.
+ *
+ * @module ext_table_preview 
+ * @version 0.1
+ *
+ * @requires jQuery
+ * @requires log
+ * @requires getEntityPath
+ * @requires getEntityID
+ * @requires markdown
+ */
+var ext_table_preview  = function ($, logger, connection, getEntityPath, getEntityID, markdown) {
+
+    const get_preview = async function (entity) {
+        try {
+            const script_result = await connection.runScript("ext_table_preview/pandas_table_preview.py",
+                {"-p0": getEntityID(entity)}
+            );
+
+            const code = script_result.getElementsByTagName("script")[0].getAttribute("code");
+            if (parseInt(code) > 1) {
+                return script_result.getElementsByTagName("stderr")[0]
+            } else if (parseInt(code) != 0) {
+                throw ("An error occurred during execution of the server-side "
+                    + "script:\n"
+                    + script_result.getElementsByTagName("stderr")[0]);
+            } else {
+                const tablecontent = script_result.getElementsByTagName("stdout")[0];
+                const unformatted = markdown.textToHtml(tablecontent.textContent)
+                const formatted = $('<div class="table-responsive"/>').append(unformatted);
+                formatted.find("table").addClass("table table-bordered table-condensed").removeAttr("border");
+                return formatted[0];
+            }
+        } catch (err) {
+            if (err.message && err.message.indexOf && err.message.indexOf("HTTP status 403") > -1) {
+                throw new ext_bottom_line.BottomLineWarning("You are not allowed to generate the table preview. Please log in.");
+            } else {
+                throw err;
+            }
+        }
+    };
+
+    const is_table = function (entity) {
+        const path = getEntityPath(entity);
+        return path && (path.toLowerCase().endsWith('.xls') 
+            || path.toLowerCase().endsWith('.xlsx') 
+            || path.toLowerCase().endsWith('.csv') 
+            || path.toLowerCase().endsWith('.tsv'));
+    };
+
+    const init = function () {
+        // only enable when init is being called
+        ext_table_preview.is_table = is_table;
+    };
+
+    return {
+        init: init,
+        get_preview: get_preview,
+        is_table: () => false,
+    };
+
+}($, log.getLogger("ext_table_preview"), connection, getEntityPath, getEntityID, markdown);
+
+// this will be replaced by require.js in the future.
+$(document).ready(function () {
+    if ("${BUILD_MODULE_EXT_BOTTOM_LINE_TABLE_PREVIEW}" == "ENABLED") {
+        caosdb_modules.register(ext_table_preview);
+    }
+});
diff --git a/src/core/js/ext_xls_download.js b/src/core/js/ext_xls_download.js
index 0f607a459b8e502e1d4c451b1636d99ceb01657c..d80bf4efa539f483811ca1d3b7f85b817489a572 100644
--- a/src/core/js/ext_xls_download.js
+++ b/src/core/js/ext_xls_download.js
@@ -24,7 +24,7 @@
 
 /**
  * @module caosdb_table_export
- * @version 0.1
+ * @version 0.2
  *
  * Convert Entities for TSV and XLS export.
  *
@@ -35,8 +35,6 @@
 var caosdb_table_export = new function () {
 
     var logger = log.getLogger("caosdb_table_export");
-    var TAB = "%09";
-    var NEWLINE = "%0A";
 
     /**
      * Hide "Download XLS File" link if the user is not authenticted (i.e.
@@ -44,11 +42,7 @@ var caosdb_table_export = new function () {
      */
     this.init = function() {
         logger.info("init caosdb_table_export");
-        // TODO with AMD, use userIsAnonymous()
-        if (Array.from(
-            document.getElementsByClassName("caosdb-user-role")).map(
-                el => el.innerText
-            ).filter(el => el == "anonymous").length > 0) {
+        if (userIsAnonymous()) {
             $(".caosdb-v-query-select-data-xsl").parent().hide();
         }
     }
@@ -62,19 +56,64 @@ var caosdb_table_export = new function () {
      * @return {string}
      */
     this.get_tsv_string = function(raw) {
+        const columns = this._get_column_header();
+        const entities = this._get_table_content();
+        return this._create_tsv_string(entities, columns, raw);
+    }
+
+    /**
+     * In order to create a valid tsv table, characters that have a special 
+     * meaning (line break and tab) need to be removed.
+     *
+     * Both characters will be replaced by a single white space
+     *
+     * @param {string} raw - the cell content.
+     * @return {string} cleaned up content
+     */
+    this._clean_cell = function(raw) {
+        return raw.replaceAll("\t"," ").replaceAll("\n"," ").replaceAll("\r"," ").replaceAll("\x1E"," ").replaceAll("\x15"," ")
+    }
+
+    /**
+     * Finds the table in the DOM tree and returns the column names as array
+     *
+     * @return {string[]}
+     */
+    this._get_column_header = 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 && e.toLowerCase() != "id" && e.toLowerCase() != "version");
+    }
+
+    /**
+     * Finds the table in the DOM tree and returns the table content as array
+     *
+     * @return {HTMLElement[]}
+     */
+    this._get_table_content = function() {
         const table = $('.caosdb-select-table');
-        const columns = table.find("th").toArray()
-            .map(e => e.textContent)
-            .filter(e => e.length > 0);
         // TODO use entity-panel class in table as well (change in query.xsl
         // and then here)
-        const entities = table.find("tbody tr").toArray();
-        const csv_string = this._get_tsv_string(entities, columns, raw);
-        return csv_string;
+        return table.find("tbody tr").toArray();
+    }
+
+    /**
+     * Encode tsv string
+     *
+     * @param {string} main - the tsv table string.
+     * @return {string} the same with prefix and URL encoded
+     */
+    this._encode_tsv_string = function(main) {
+        const preamble = "data:text/csv;charset=utf-8,";
+        main = encodeURIComponent(main);
+        return `${preamble}${main}`;
     }
 
     /**
-     * Convert all entities to a tsv string with the given columns.
+     * 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.
@@ -83,36 +122,56 @@ var caosdb_table_export = new function () {
      *     cells. Otherwise, the displayed data is used instead.
      * @return {string}
      */
-    this._get_tsv_string = function (entities, columns, raw) {
-        logger.trace("enter get_tsv_string", entities, columns);
-        var preamble = "data:text/csv;charset=utf-8,";
-        var header = "ID" + TAB + columns.join(TAB) + NEWLINE
-        var rows = caosdb_table_export._get_tsv_rows(entities, columns, raw).join(NEWLINE);
-
-        const ret = `${preamble}${header}${rows}`;
-        logger.trace("leave get_tsv_string", ret);
-        return ret;
+    this._create_tsv_string = function (entities, columns, raw) {
+        logger.trace("enter _create_tsv_string ", entities, columns);
+        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"));
+        }
+        var rows = rows.join("\n");
+        logger.trace("leave _create_tsv_string ", rows);
+        return `${header}${rows}`;
     }
 
     /**
-     * Return an array of rows with the given columns of the tsv table, one per
-     * entity.
+     * Return an array of cells, one per column, which contain a string
+     * representation of the value of the properties with the same name (as the
+     * column).
      *
-     * @param {HTMLElement[]} entities - entities which are converted to rows
-     *     of the tsv string.
+     * @param {HTMLElement} entity - entity from which the cells are extracted.
      * @param {string[]} columns - array of property names.
      * @param {boolean} raw - if true, the raw entity ids are put into the
      *     cells. Otherwise, the displayed data is used instead.
      * @return {string[]}
      */
-    this._get_tsv_rows = function (entities, columns, raw) {
-        var rows = [];
-        for (const entity of entities) {
-            rows.push(this._get_entity_row(entity, columns, raw));
+    this._get_entity_row = function (entity, columns, raw) {
+        var cells = [getEntityID(entity), getEntityVersion(entity)];
+        var properties = getProperties(entity);
+
+        for (const column of columns) {
+            var cell = "";
+            for (const property of properties) {
+                if(property.name.toLowerCase() === column.toLowerCase()) {
+                    var value = caosdb_table_export
+                        ._get_property_value(property.html);
+                    if (raw) {
+                        cell = value.raw;
+                    } else if (value.summary) {
+                        cell = value.summary;
+                    } else if (value.pretty) {
+                        cell = value.pretty;
+                    } else {
+                        cell = value.raw;
+                    }
+                }
+            }
+            cells.push(caosdb_table_export._clean_cell(cell));
         }
-        return rows;
-    }
 
+        logger.trace("leave _get_entity_row", cells);
+        return cells;
+    }
 
     /**
      * Return different string representations of the property's value.
@@ -187,73 +246,15 @@ var caosdb_table_export = new function () {
         }
     }
 
-
-    /**
-     * Return an array of cells, one per column, which contain a string
-     * representation of the value of the properties with the same name (as the
-     * column).
-     *
-     * @param {HTMLElement} entity - entity from which the cells are extracted.
-     * @param {string[]} columns - array of property names.
-     * @param {boolean} raw - if true, the raw entity ids are put into the
-     *     cells. Otherwise, the displayed data is used instead.
-     * @return {string[]}
-     */
-    this._get_entity_row = function (entity, columns, raw) {
-        var cells = [getEntityID(entity)];
-        var properties = getProperties(entity);
-
-        for (const column of columns) {
-            var cell = "";
-            for (const property of properties) {
-                if(property.name.toLowerCase() === column.toLowerCase()) {
-                    var value = caosdb_table_export
-                        ._get_property_value(property.html);
-                    if (raw) {
-                        cell = value.raw;
-                    } else if (value.summary) {
-                        cell = value.summary;
-                    } else if (value.pretty) {
-                        cell = value.pretty;
-                    } else {
-                        cell = value.raw;
-                    }
-                }
-            }
-            cells.push(cell);
-        }
-
-        logger.trace("leave _get_entity_row", cells);
-        return cells.join(TAB);
-    }
-
-
     /**
      * Open the resulting xls file by setting href to the location of the resulting
      * file in the server's `Shared` resource and imitate a click.
      */
-    this._go_to_script_results = function (xls_link, filename) {
-        xls_link.setAttribute(
-            "href",
-            location.protocol + "//" +location.host + "/Shared/" + filename);
-        xls_link.click();
-    }
-
-
-    this._get_csv_string = function (){
-        const raw = $("input#caosdb-table-export-raw-flag-xls").is(":checked");
-        const csv_string = caosdb_table_export.get_tsv_string(raw);
-        //const csv_string = document.getElementById("caosdb-f-query-select-data-tsv").getAttribute(
-            //"href");
-        if (!csv_string) {
-            return undefined;
-        }
-
-        return decodeURIComponent(csv_string.replace(/^data.*utf-8,/, ""));
+    this.go_to_script_results = function (filename) {
+        window.location.href = connection.getBasePath() + "Shared/" + filename;
     }
 }
 
-
 /**
  * This function is called on click by the link button which says "Download TSV
  * File".
@@ -264,12 +265,12 @@ var caosdb_table_export = new function () {
  */
 function downloadTSV(tsv_link) {
     const raw = $("input#caosdb-table-export-raw-flag-tsv").is(":checked");
-    const tsv_string = caosdb_table_export.get_tsv_string(raw);
+    var tsv_string = caosdb_table_export.get_tsv_string(raw);
+    tsv_string = caosdb_table_export._encode_tsv_string(tsv_string);
 
     $(tsv_link).attr("href", tsv_string);
     return true;
 }
-
 /**
  * This function is called on click by the link button which says "Download XLS
  * File".
@@ -279,15 +280,24 @@ function downloadTSV(tsv_link) {
  * resulting file.
  */
 async function downloadXLS(xls_link) {
-    const csv_string = caosdb_table_export._get_csv_string();
-
-    // remove click event handler which called this function in the first place
     const onClickValue = xls_link.getAttribute("onClick");
-    xls_link.removeAttribute("onClick");
-
     try {
+        // remove click event handler which called this function in the first place
+        xls_link.removeAttribute("onClick");
+
+        // add loading info. TODO make an animated one
+        $("#downloadModal").find(".caosdb-f-modal-footer-left").append(
+            createWaitingNotification("Exporting table...")
+        );
+
+        const raw = $("input#caosdb-table-export-raw-flag-xls").is(":checked");
+        const tsv_string = caosdb_table_export.get_tsv_string(raw);
+        // TODO This existed previously. I do not know why.
+        if (!tsv_string) {
+            tsv_string = undefined;
+        }
         const xls_result = await connection.runScript("xls_from_csv.py",
-            {"-p0": {"filename": "selected.tsv", "blob": new Blob([csv_string], {type: "text/tab-separated-values;charset=utf-8"})}});
+            {"-p0": {"filename": "selected.tsv", "blob": new Blob([tsv_string], {type: "text/tab-separated-values;charset=utf-8"})}});
         const code = xls_result.getElementsByTagName("script")[0].getAttribute("code");
         if (parseInt(code) > 0) {
             throw ("An error occurred during execution of the server-side script:\n"
@@ -299,11 +309,14 @@ async function downloadXLS(xls_link) {
                   + xls_result.getElementsByTagName("script")[0].outerHTML);
         }
 
-        // set the href in order to download the file and simulate a click.
-        caosdb_table_export._go_to_script_results(xls_link, filename);
+        // trigger download of generated file
+        caosdb_table_export.go_to_script_results(filename);
+
+
     } catch (e) {
         globalError(e);
     } finally {
+        removeAllWaitingNotifications($("#downloadModal")[0]);
         // restore the old click handler - hence a new file is generated with each click.
         xls_link.setAttribute("onClick", onClickValue);
     }
@@ -314,4 +327,5 @@ async function downloadXLS(xls_link) {
 
 $(document).ready(function () {
     caosdb_modules.register(caosdb_table_export);
+    caosdb_modules.register(ext_file_download);
 });
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 0014f4db452a55839edaa860d7fa49f98529434e..d4cd4234a28953140fcc1f62104e2c43a3460cdb 100644
--- a/src/core/js/form_elements.js
+++ b/src/core/js/form_elements.js
@@ -9,8 +9,7 @@
  * 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
+ * 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.
  *
@@ -26,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
@@ -49,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"];
@@ -164,37 +190,119 @@ var form_elements = new function () {
     }
 
 
+    this._get_alert_decision = function (key) {
+        return localStorage["form_elements.alert_decision." + key];
+    }
+
+    this._set_alert_decision = function (key, val) {
+        localStorage["form_elements.alert_decision." + key] = val;
+    }
+
     /**
-     * (Re-)set this module's functions to standard implementation.
+     * Make an alert, that is a dialog which can intercept a function call and
+     * asks the user to proceed or cancel.
+     *
+     * @param {AlertConfig} config
+     * @return {HTMLElement}
      */
-    this._init_functions = function () {
+    this.make_alert = function (config) {
+        caosdb_utils.assert_string(config.message, "config param `message`");
+        caosdb_utils.assert_type(config.proceed_callback, "function",
+          "config param `proceed_callback`");
+
+        // define some defaults.
+        const title = config.title ? `<h4>${config.title}</h4>` : "";
+        const proceed_text = config.proceed_text || "Proceed";
+        const cancel_text = config.cancel_text || "Cancel";
+        const severity = config.severity || "danger";
+        const remember = !!config.remember_my_decision_id || false;
+
+        // check if alert should be created at all
+        if (remember) {
+          var result = this._get_alert_decision(config.remember_my_decision_id);
+          if (result == "proceed") {
+            // call callback asyncronously and return
+            (async function(){ config.proceed_callback(); })();
+            return undefined;
+          }
+        }
 
-        this.init = function () {
-            this.logger.trace("enter init");
+        // create the alert
+        const _alert = $(`<div class="alert alert-${severity}
+              alert-dismissible fade in caosdb-f-form-elements-alert" role="alert">${title}
+            <p>${config.message}</p>
+        </div>`);
+
+        // create the "Don't ask me again" checkbox
+        var checkbox = undefined;
+        if (remember) {
+            const remember_my_decision_text = config.remember_my_decision_text
+                || "Don't ask me again.";
+            checkbox = $(`<p class="checkbox"><label>
+              <input type="checkbox"/> ${remember_my_decision_text}</label></p>`);
+            _alert.append(checkbox);
         }
 
-        /**
-         * 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;
+
+        // create buttons ...
+        const cancel_button = config.cancel_button || $(`<button type="button" class="btn btn-default caosdb-f-btn-alert-cancel">${cancel_text}</button>`);
+        const proceed_button = config.proceed_button || $(`<button type="button" class="btn btn-${severity} caosdb-f-btn-alert-proceed">${proceed_text}</button>`);
+        _alert.append($("<p/>").append([proceed_button, cancel_button]));
+
+
+        // ... and bind callbacks to the buttons.
+        cancel_button.click(() => {
+            $(_alert).alert('close');
+            if (typeof config.cancel_callback == "function") {
+                config.cancel_callback();
             }
-            var opt_str = '<option value="' + entity_id + '">' + desc +
-                "</option>";
-            return $(opt_str)[0];
+        });
+        proceed_button.click(() => {
+            if (remember && checkbox.find("input[type='checkbox']").is(":checked")) {
+                // store this decision.
+                form_elements._set_alert_decision(config.remember_my_decision_id,
+                    "proceed");
+            }
+            $(_alert).alert('close');
+            config.proceed_callback();
+        });
+
+        return _alert[0];
+    }
+
+
+    this.init = function () {
+        this.logger.trace("enter init");
+    }
+
+    /**
+     * 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.
@@ -244,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.
@@ -263,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
@@ -277,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);
@@ -421,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);
+
+            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;
+            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 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[subform] = [data[subform], subform_obj]
+                            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 {
-                                data[name] = [data[name], value]
-                            }
-                        } else {
-                            // TODO checkbox
-                        }
-                    } 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);
             }
+        }
 
-            // add fields
-            for (let field of config.fields) {
-                this.logger.trace("add field", field);
-                if (field instanceof HTMLElement) {
-                    form.append(field);
+        // 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)
+
+                }
+                // 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);
+            this.logger.debug("submit form", e);
 
-            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);
+            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 df93703d83fafde15908efb40c3ae578824980d9..55337a36a97d6a7ad42aadfe673e429d6d942a0a 100644
--- a/src/core/js/webcaosdb.js
+++ b/src/core/js/webcaosdb.js
@@ -146,6 +146,64 @@ this.navbar = new function () {
             .on("hidden.bs.collapse", function (e) {
                 logger.trace("navbar shrinks", e);
             });
+        this.init_login_show_button();
+    }
+
+
+    /**
+     * Initialize the hiding/showing of the input form.
+     *
+     * If the viewport is xs (width <= 768px) the login form is hidden in the
+     * a menu anyways.
+     *
+     * If the viewport is greater then the folloging applies:
+     *
+     * When loading the page, the login form is hidden behind a "Login" button.
+     * When the user clicks the Login button the form appears and the other
+     * button disappears. After a timeout of 10 seconds of inactivity the form
+     * hides again. When the user starts typing, the timeout is canceled.
+     *
+     * If the user leaves the input fields ("blur" event) the timeout is
+     * reestablished and the form hides after 10 seconds.
+     */
+    this.init_login_show_button = function () {
+        const form = $("#caosdb-f-login-form");
+        const show_button = $("#caosdb-f-login-show-button");
+        var timeout = undefined;
+
+        // show form and hide the show_button
+        const _in = () => {
+          // xs means viewport <= 768px
+          form.removeClass("visible-xs-inline-block");
+          show_button.addClass("hidden");
+        }
+        // hide form and show the show_button
+        const _out = () => {
+          // xs means viewport <= 768px
+          form.addClass("visible-xs-inline-block");
+          show_button.removeClass("hidden");
+        }
+        show_button.on("click", () => {
+            // show form...
+            _in();
+
+            // and hide it after ten seconds if nothing happens
+            timeout = setTimeout(_out,10000)
+        });
+        form.find("input,button").on("blur", () => {
+            if (timeout) {
+                // cancel existing timeout (e.g. from showing)
+                clearTimeout(timeout);
+            }
+            // hide after 10 seconds if nothing happens
+            timeout = setTimeout(_out,10000)
+        });
+        form.find("input,button").on("change", () => {
+            // something happens!
+            if (timeout) {
+                clearTimeout(timeout);
+            }
+        });
     }
 
 
@@ -269,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)}`;
+    }
 }
 
 /**
@@ -592,7 +674,8 @@ this.transaction = new function () {
      * @return {Element[]} array of xml elements.
      */
     this.retrieveEntitiesById = async function _rEBIs(entityIds) {
-        return $(await connection.get(this.generateEntitiesUri(entityIds))).find('Response [id]').toArray();
+        const response = await connection.get(this.generateEntitiesUri(entityIds));
+        return $(response).find('Response > [id]').toArray();
     }
 
     /** Sends a PUT request with an xml representation of entities and
@@ -905,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 () {
 
@@ -979,6 +1173,9 @@ var paging = new function () {
             return null;
         }
 
+        // remove fragment
+        uri_old = uri_old.split("#")[0];
+
         var pattern = /(\?(.*&)?)P=([^&]*)/;
         if (pattern.test(uri_old)) {
             // replace existing P=...
@@ -1003,7 +1200,7 @@ var paging = new function () {
         if (uri == null) {
             throw new Error("uri was null.");
         }
-        var pattern = /\?(.*&)?P=([^&]*)/;
+        var pattern = /\?(.*&)?P=([^&#]*)/;
         var ret = pattern.exec(uri);
         return (ret != null ? ret[2] : null);
     }
@@ -1121,7 +1318,6 @@ var queryForm = new function () {
           See https://developer.mozilla.org/en-US/docs/Web/Events/submit why this is necessary.
           */
         var submithandler = function () {
-
             // store current query
             var queryField = form.query;
             var value = queryField.value.toUpperCase();
@@ -1141,6 +1337,13 @@ var queryForm = new function () {
             queryForm.redirect(queryField.value, paging);
         };
 
+        $("#caosdb-query-textarea").on("keydown", (e) => {
+            // prevent submit on enter
+            if(e.originalEvent.which == 13) {
+                e.originalEvent.preventDefault();
+            }
+        })
+
 
         // handler for the form
         form.onsubmit = function (e) {
@@ -1503,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();
@@ -1534,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 299b1dac37a1029e5bc7c48008efa3735d61037b..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">
@@ -156,6 +157,16 @@
               <span class="label caosdb-id caosdb-id-button hidden">
                 <xsl:value-of select="@id"/>
               </span>
+              <button class="btn btn-link caosdb-v-bookmark-button">
+                <xsl:attribute name="data-bmval">
+                  <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>
               <xsl:apply-templates mode="entity-heading-attributes-version" select="Version">
                 <xsl:with-param name="entityId" select="@id"/>
               </xsl:apply-templates>
@@ -392,8 +403,8 @@
       <xsl:otherwise>
         <xsl:choose>
           <!-- the referenced entities have been returned. -->
-          <xsl:when test="*[@id]">
-            <xsl:for-each select="*[@id]">
+          <xsl:when test="Record|RecordType|Property|File">
+            <xsl:for-each select="Record|RecordType|Property|File">
               <xsl:call-template name="single-value">
                 <xsl:with-param name="reference">
                   <xsl:value-of select="'true'"/>
@@ -518,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"/>
@@ -598,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.
@@ -613,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/main.xsl b/src/core/xsl/main.xsl
index 10a4887aed065a5c8ffc74e463fc7660f81d743b..f81d8792c94d8da38d058f96d026b757af402b6c 100644
--- a/src/core/xsl/main.xsl
+++ b/src/core/xsl/main.xsl
@@ -142,6 +142,16 @@
         <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/webcaosdb.js')"/>
       </xsl:attribute>
     </xsl:element>
+    <xsl:element name="script">
+      <xsl:attribute name="src">
+        <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/pako.js')"/>
+      </xsl:attribute>
+    </xsl:element>
+    <xsl:element name="script">
+      <xsl:attribute name="src">
+        <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/utif.js')"/>
+      </xsl:attribute>
+    </xsl:element>
     <script>
       $(document).ready(() => paging.initPaging(window.location.href, <xsl:value-of select="/Response/@count"/> ));
     </script>
@@ -157,7 +167,7 @@
     </xsl:element>
     <xsl:element name="script">
       <xsl:attribute name="src">
-        <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/autocomplete.js')"/>
+        <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_autocomplete.js')"/>
       </xsl:attribute>
     </xsl:element>
     <xsl:element name="script">
@@ -170,6 +180,11 @@
         <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_references.js')"/>
       </xsl:attribute>
     </xsl:element>
+    <xsl:element name="script">
+      <xsl:attribute name="src">
+        <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_table_preview.js')"/>
+      </xsl:attribute>
+    </xsl:element>
     <xsl:element name="script">
       <xsl:attribute name="src">
         <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_xls_download.js')"/>
@@ -195,6 +210,11 @@
         <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/edit_mode.js')"/>
       </xsl:attribute>
     </xsl:element>
+    <xsl:element name="script">
+      <xsl:attribute name="src">
+        <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_file_download.js')"/>
+      </xsl:attribute>
+    </xsl:element>
     <xsl:element name="script">
       <xsl:attribute name="src">
         <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/leaflet.js')"/>
@@ -265,6 +285,11 @@
         <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_entity_state.js')"/>
       </xsl:attribute>
     </xsl:element>
+    <xsl:element name="script">
+      <xsl:attribute name="src">
+        <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_bookmarks.js')"/>
+      </xsl:attribute>
+    </xsl:element>
     <!--JS_EXTENSIONS-->
   </xsl:template>
   <xsl:template name="caosdb-data-container">
diff --git a/src/core/xsl/navbar.xsl b/src/core/xsl/navbar.xsl
index 907f27e512250cf5fde0ff2fe93fe85f800428a6..6ce69e638efda10dbb95a066e8b59508854a4435 100644
--- a/src/core/xsl/navbar.xsl
+++ b/src/core/xsl/navbar.xsl
@@ -132,6 +132,23 @@
             </ul>
           </xsl:if>
           <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>
+                  Bookmarks
+                <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. 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.">
+                  <a>Clear</a></li>
+              </ul>
+            </li>
             <xsl:call-template name="caosdb-user-menu"/>
           </ul>
         </div>
@@ -191,17 +208,15 @@
       </xsl:when>
       <xsl:otherwise>
         <li id="user-menu">
-          <form class="navbar-form" method="POST">
+          <form id="caosdb-f-login-form" class="navbar-form visible-xs-inline-block" method="POST">
             <xsl:attribute name="action">
               <xsl:value-of select="concat($basepath, 'login')"/>
             </xsl:attribute>
             <input class="form-control" id="username" name="username" placeholder="username" type="text"/>
             <input class="form-control" id="password" name="password" placeholder="password" type="password"/>
-            <button class="btn btn-default" type="submit">
-              <span class="glyphicon glyphicon-log-in"></span>
-                            Login
-                        </button>
+            <button class="btn btn-primary" type="submit">Login</button>
           </form>
+          <button style="margin-right: 15px" class="btn btn-default navbar-btn hidden-xs" id="caosdb-f-login-show-button" type="button">Login</button>
         </li>
       </xsl:otherwise>
     </xsl:choose>
diff --git a/src/core/xsl/query.xsl b/src/core/xsl/query.xsl
index be49ed7d889e920cfee87e723c4c5c3b8efb27b2..44d1c1bd6cff8f4b22138c1287af15713069ed79 100644
--- a/src/core/xsl/query.xsl
+++ b/src/core/xsl/query.xsl
@@ -89,7 +89,7 @@
           </div>
           <div class="col-xs-6 text-right">
             <!-- Trigger the modal with a button -->
-            <button class="btn btn-info btn-sm" data-target="#downloadModal" data-toggle="modal" type="button">Download this table</button>
+            <button class="btn btn-info btn-sm caosdb-v-btn-select" data-target="#downloadModal" data-toggle="modal" type="button">Export</button>
             <!-- Modal -->
             <div class="modal fade text-left" id="downloadModal" role="dialog">
               <div class="modal-dialog">
@@ -102,16 +102,21 @@
                   <div class="modal-body">
                     <p>
                       <a id="caosdb-f-query-select-data-tsv" onclick="downloadTSV(this)" href="#selected_data.tsv" download="selected_data.tsv">
-                        Download TSV File
+                        Download table as TSV File
                       </a>
                       <span class="checkbox" style="margin-top: 0; display: inline; position: absolute; right: 10px"><label><input type="checkbox" name="raw" id="caosdb-table-export-raw-flag-tsv" title="Export raw entity ids instead of the visible page content."/>raw</label></span>
                     </p>
                     <p>
                       <a class="caosdb-v-query-select-data-xsl" onclick="downloadXLS(this)" href="#selected_data.xsl" download="">
-                        Download XLS File
+                        Download table as XLS File
                       </a>
                       <span class="checkbox" style="margin-top: 0; display: inline; position: absolute; right: 10px"><label><input type="checkbox" name="raw" id="caosdb-table-export-raw-flag-xls" title="Export raw entity ids instead of the visible page content."/>raw</label></span>
                     </p>
+                    <p>
+                      <a id="caosdb-f-query-select-files" onclick="ext_file_download.download_files(this)" href="#selected_data.tsv" download="files.zip" title="Collects file entities listed in the table in a zip file. If the entity belonging to a row is a file entity, it will be included.">
+                          Download files referenced in the table
+                      </a>
+                    </p>
                     <hr/>
                     <p>
                       <small>Download this dataset in Python with:</small>
@@ -123,7 +128,13 @@
                     </p>
                   </div>
                   <div class="modal-footer">
-                    <button class="btn btn-default" data-dismiss="modal" type="button">Close</button>
+                    <div class="row" style="margin:0px">
+                      <div class="col-xs-6 caosdb-f-modal-footer-left">
+                      </div>
+                      <div class="col-xs-6">
+                        <button class="btn btn-default" data-dismiss="modal" type="button">Close</button>
+                      </div>
+                    </div>
                   </div>
                 </div>
               </div>
@@ -138,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>
@@ -149,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>
@@ -159,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">
@@ -172,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>
@@ -192,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>
@@ -212,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="."/>
@@ -263,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/src/server_side_scripting/ext_file_download/zip_files.py b/src/server_side_scripting/ext_file_download/zip_files.py
new file mode 100755
index 0000000000000000000000000000000000000000..65f27c9d901a6790456b8addb636bd55b7fe0c7e
--- /dev/null
+++ b/src/server_side_scripting/ext_file_download/zip_files.py
@@ -0,0 +1,112 @@
+#!/usr/bin/env python3
+# ** 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
+
+"""Creates a zip file from multiple file entities.  """
+
+import argparse
+import datetime
+import io
+import logging
+import os
+import sys
+from tempfile import NamedTemporaryFile
+from zipfile import ZipFile
+
+import caosdb as db
+import pandas as pd
+from caosadvancedtools.serverside import helper
+from caosdb import CaosDBException, ConsistencyError, EntityDoesNotExistError
+
+
+def _parse_arguments():
+    """Parses the command line arguments.
+
+    Takes into account defaults from the environment (where known).
+    """
+    parser = argparse.ArgumentParser(description='__doc__')
+    parser.add_argument('-a', '--auth-token', required=False,
+                        help=("An authentication token. If not provided caosdb"
+                              " pylib will search for other methods of "
+                              "authentication if necessary."))
+    parser.add_argument('ids', help="list of entity ids.")
+    parser.add_argument('table', help="tsv table to be saved (as string).")
+
+    return parser.parse_args()
+
+
+def collect_files_in_zip(ids, table):
+    # File output
+    now = datetime.datetime.now()
+    zip_name = "files.{time}.zip".format(
+        time=now.strftime("%Y-%m-%dT%H_%M_%S"))
+    zip_display_path, zip_internal_path = helper.get_shared_filename(zip_name)
+    with ZipFile(zip_internal_path, 'w') as zf:
+        nc = helper.NameCollector()
+
+        # add the table which has been genereated by the webui table exporter
+        with NamedTemporaryFile(delete=False) as table_file:
+            # the file has been transmitted as string and has to be written to
+            # a file first.
+            table_file.write(table.encode())
+        zf.write(table_file.name, "selected_table.tsv")
+
+        # download and add all files
+        for file_id in ids:
+            try:
+                tmp = db.execute_query("FIND {a:} WITH ID={a:}".format(
+                    a=file_id),
+                    unique=True)
+            except EntityDoesNotExistError as e:
+                # TODO
+                # Current behavior: script terminates with error if just one
+                # file cannot be retrieved.
+                # Desired behavior: The script should go on with the other
+                # ids, but the user should be informed about the missing files.
+                # How should we do this?
+                logger = logging.getLogger("caosadvancedtools")
+                logger.error("Did not find Entity with ID={}.".format(
+                    file_id))
+
+                raise e
+            savename = nc.get_unique_savename(os.path.basename(tmp.path))
+            val_file = helper.get_file_via_download(
+                tmp, logger=logging.getLogger("caosadvancedtools"))
+
+            zf.write(val_file, savename)
+
+    return zip_display_path
+
+
+def main():
+    args = _parse_arguments()
+
+    if hasattr(args, "auth_token") and args.auth_token:
+        db.configure_connection(auth_token=args.auth_token)
+
+    id_list = [int(el) for el in args.ids.split(",")]
+
+    zip_file = collect_files_in_zip(id_list, args.table)
+
+    print(zip_file)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/src/server_side_scripting/ext_table_preview/pandas_table_preview.py b/src/server_side_scripting/ext_table_preview/pandas_table_preview.py
new file mode 100755
index 0000000000000000000000000000000000000000..c0659d9b1839c43e0629a878d792c414577ea344
--- /dev/null
+++ b/src/server_side_scripting/ext_table_preview/pandas_table_preview.py
@@ -0,0 +1,145 @@
+#!/usr/bin/env python3
+# encoding: utf-8
+#
+# ** header v3.0
+# This file is a part of the CaosDB Project.
+#
+# Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+# Copyright (C) 2020 Henrik tom Wörden <h.tomwoerden@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 script tries to read typical table data files (.csv etc.) with pandas and
+creates a html (partial) representation of the table.
+"""
+
+import logging
+import os
+import sys
+from datetime import datetime
+
+import caosdb as db
+import pandas as pd
+from caosadvancedtools.serverside.helper import get_argument_parser
+from caosadvancedtools.serverside.logging import configure_server_side_logging
+
+MAXIMUMFILESIZE = 1e8
+VALID_ENDINGS = [".csv", ".tsv", ".xls", ".xlsx"]
+
+
+def get_file(eid):
+    """ retrieves the file entity from caosdb """
+    try:
+        fi = db.File(id=eid)
+        fi.retrieve()
+    except db.exceptions.EntityDoesNotExistError:
+        print("Cannot create preview for Entity with ID={}, because it seems"
+              "not to exist.".format(eid), file=sys.stderr)
+        sys.exit(1)
+
+    return fi
+
+
+def size_is_ok(fi):
+    """ show previews only for files that are not too large """
+
+    return fi.size <= MAXIMUMFILESIZE
+
+
+def get_ending(fipath):
+    """ return which of the valid endings (tsv etc.) is the one present"""
+
+    for end in VALID_ENDINGS:
+        if fipath.lower().endswith(end):
+            return end
+
+    return None
+
+
+def ending_is_valid(fipath):
+    """ return whether the ending indicates a file type that can be treated"""
+
+    return get_ending(fipath) is not None
+
+
+def read_file(fipath, ftype):
+    """ tries to read the provided file """
+
+    try:
+        if ftype in [".xls", ".xlsx"]:
+            df = pd.read_excel(fipath)
+        elif ftype == ".tsv":
+            df = pd.read_csv(fipath, sep="\t", comment="#")
+        elif ftype == ".csv":
+            df = pd.read_csv(fipath, comment="#")
+        else:
+            print("File type unknown: {}".format(ftype))
+            raise RuntimeError("")
+    except Exception:
+        raise ValueError()
+
+    return df
+
+
+def create_table_preview(fi):
+    if not ending_is_valid(fi.path):
+        print("Cannot create preview for Entity with ID={}, because download"
+              "failed.".format(entity_id), file=sys.stderr)
+        sys.exit(5)
+
+    ending = get_ending(fi.path)
+
+    if not size_is_ok(fi):
+        print("Skipped creating a preview for Entity with ID={}, because the"
+              "file is large!".format(entity_id), file=sys.stderr)
+        sys.exit(2)
+
+    try:
+        tmpfile = fi.download()
+    except Exception:
+        print("Cannot create preview for Entity with ID={}, because download"
+              "failed.".format(entity_id), file=sys.stderr)
+
+        sys.exit(3)
+
+    try:
+        df = read_file(tmpfile, ending)
+    except ValueError:
+        print("Cannot read File Entity with ID={}.".format(entity_id),
+              file=sys.stderr)
+        sys.exit(4)
+
+    print(df.to_html(max_cols=10, max_rows=10))
+
+
+if __name__ == "__main__":
+    conlogger = logging.getLogger("connection")
+    conlogger.setLevel(level=logging.ERROR)
+
+    parser = get_argument_parser()
+    args = parser.parse_args()
+
+    debug_file = configure_server_side_logging()
+    logger = logging.getLogger("caosadvancedtools")
+
+    db.configure_connection(auth_token=args.auth_token)
+    entity_id = args.filename
+
+    fi = get_file(entity_id)
+
+    create_table_preview(fi)
diff --git a/test/core/index.html b/test/core/index.html
index e4d6d81634aa73c2d71385545fb917392ba5df1f..5786583c3df7bf281bc3a151f67b378dd22ca5ff 100644
--- a/test/core/index.html
+++ b/test/core/index.html
@@ -37,6 +37,8 @@
   <script src="js/bootstrap.js"></script>
   <script src="js/bootstrap-select.js"></script>
   <script src="js/bootstrap-autocomplete.min.js"></script>
+  <script src="js/utif.js"></script>
+  <script src="js/pako.js"></script>
   <script src="js/webcaosdb.js"></script>
   <script src="js/plotly.js"></script>
   <script>
@@ -54,6 +56,7 @@
   <script src="js/edit_mode.js"></script>
   <script src="js/query_shortcuts.js"></script>
   <script src="js/ext_references.js"></script>
+  <script src="js/ext_file_download.js"></script>
   <script src="js/ext_xls_download.js"></script>
   <script src="js/form_elements.js"></script>
   <script src="js/tour.js"></script>
@@ -65,11 +68,13 @@
   <script src="js/proj4leaflet.js"></script>
   <script src="js/ext_map.js"></script>
   <script src="js/ext_applicable.js"></script>
+  <script src="js/ext_table_preview.js"></script>
   <script src="js/ext_bottom_line.js"></script>
   <script src="js/ext_revisions.js"></script>
-  <script src="js/autocomplete.js"></script>
+  <script src="js/ext_autocomplete.js"></script>
   <script src="js/ext_sss_markdown.js"></script>
   <script src="js/ext_trigger_crawler_form.js"></script>
+  <script src="js/ext_bookmarks.js"></script>
   <!--EXTENSIONS-->
   <script src="js/modules/webcaosdb.js.js"></script>
   <script src="js/modules/caosdb.js.js"></script>
@@ -81,6 +86,7 @@
   <script src="js/modules/navbar.xsl.js"></script>
   <script src="js/modules/edit_mode.js.js"></script>
   <script src="js/modules/ext_xls_download.js.js"></script>
+  <script src="js/modules/ext_file_download.js.js"></script>
   <script src="js/modules/query_shortcuts.js.js"></script>
   <script src="js/modules/form_elements.js.js"></script>
   <script src="js/modules/ext_references.js.js"></script>
@@ -88,8 +94,9 @@
   <script src="js/modules/ext_applicable.js.js"></script>
   <script src="js/modules/ext_bottom_line.js.js"></script>
   <script src="js/modules/ext_revisions.js.js"></script>
-  <script src="js/modules/autocomplete.js.js"></script>
+  <script src="js/modules/ext_autocomplete.js.js"></script>
   <script src="js/modules/ext_sss_markdown.js.js"></script>
   <script src="js/modules/ext_trigger_crawler_form.js.js"></script>
+  <script src="js/modules/ext_bookmarks.js.js"></script>
 </body>
 </html>
diff --git a/test/core/js/modules/autocomplete.js.js b/test/core/js/modules/autocomplete.js.js
deleted file mode 100644
index b75acdcad8b08e548423513ccdf78d7ca0ea7359..0000000000000000000000000000000000000000
--- a/test/core/js/modules/autocomplete.js.js
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * ** 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
- */
-
-'use strict';
-
-QUnit.module("autocomplete.js", {
-	before: function (assert){
-		autocomplete.retrieve_names = async function () {
-			return ['IceCore', 'Bag', 'IceSample', 'IceCream', 'Palette'];
-		}
-	}
-});
-
-QUnit.test("availability", function(assert) {
-	//assert.ok(bootstrap..init, "init available");
-    assert.equal(autocomplete.version, "0.1", "test version");
-    assert.ok(autocomplete.init, "init available");
-});
-
-
-
-QUnit.test("filter", function(assert) {
-    assert.equal(autocomplete.filter('IceCore','Ice'), true, 'test filter')
-    assert.equal(autocomplete.filter('IceCore','iCe'), true, 'test filter')
-    assert.equal(autocomplete.filter('IceCore','Core'), false, 'test filter')
-    assert.equal(autocomplete.filter('Bag','Ice'), false, 'test filter')
-});
-
-QUnit.test("search", async function(assert) {
-
-	var done = assert.async(2);
-	var gcallback = function(expresults){
-		return function (results) {
-			assert.propEqual(
-				results, 
-				expresults, 
-				"test list filter");
-			done();
-		};
-	};
-
-	await autocomplete.search("Ice", gcallback(
-			['IceCore', 'IceSample', 'IceCream']
-	));
-
-	await autocomplete.search("Core", gcallback([]));
-});
-
-QUnit.test("class", function(assert) {
-    assert.ok(autocomplete.toggle_completion , "toggle available");
-    assert.ok(autocomplete.toggle_completion() , "toggle runs");
-});
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 c607ee28bf7888f94e3086abf8e033e1532d0d09..59a2f8a6f9a03cf4990fe11bfd751d437a3ffc3e 100644
--- a/test/core/js/modules/entity.xsl.js
+++ b/test/core/js/modules/entity.xsl.js
@@ -264,6 +264,67 @@ 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" />
+  </Property>`;
+    var xml = str2xml(xmlstr);
+    var html = applyTemplates(xml, this.entityXSL, "entity-body", "Property");
+    var prop = getPropertyFromElement(html.firstElementChild);
+    assert.propEqual(prop, {
+        "datatype": "TestRT",
+        "html": {},
+        "id": "3842",
+        "list": false,
+        "name": "reftotestrt",
+        "reference": true,
+        "unit": undefined,
+        "value": ""}
+    );
+});
+
 /* MISC FUNCTIONS */
 function applyTemplates(xml, xsl, mode, select = "*") {
     let entryRule = '<xsl:template priority="9" match="/"><xsl:apply-templates select="' + select + '" mode="' + mode + '"/></xsl:template>';
diff --git a/test/core/js/modules/ext_autocomplete.js.js b/test/core/js/modules/ext_autocomplete.js.js
new file mode 100644
index 0000000000000000000000000000000000000000..e8776f945b7bb46a0d431eb2d0ac0f7fe21419fc
--- /dev/null
+++ b/test/core/js/modules/ext_autocomplete.js.js
@@ -0,0 +1,72 @@
+/*
+ * ** 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
+ */
+
+'use strict';
+
+QUnit.module("ext_autocomplete.js", {
+    before: function (assert){
+        ext_autocomplete.retrieve_names = async function () {
+            return ['IceCore', 'Bag', 'IceSample', 'IceCream', 'Palette'];
+        }
+        ext_autocomplete.init();
+
+    }
+});
+
+QUnit.test("availability", function(assert) {
+    //assert.ok(bootstrap..init, "init available");
+    assert.equal(ext_autocomplete.version, "0.1", "test version");
+    assert.ok(ext_autocomplete.init, "init available");
+});
+
+
+
+QUnit.test("starts_with_filter", function(assert) {
+    assert.equal(ext_autocomplete.starts_with_filter('IceCore','Ice'), true, 'test filter')
+    assert.equal(ext_autocomplete.starts_with_filter('IceCore','iCe'), true, 'test filter')
+    assert.equal(ext_autocomplete.starts_with_filter('IceCore','Core'), false, 'test filter')
+    assert.equal(ext_autocomplete.starts_with_filter('Bag','Ice'), false, 'test filter')
+});
+
+QUnit.test("search", async function(assert) {
+
+    var done = assert.async(2);
+    var gcallback = function(expresults){
+        return function (results) {
+            assert.propEqual(
+                results, 
+                expresults, 
+                "test list filter");
+            done();
+        };
+    };
+    await ext_autocomplete.search("Ice", 
+        gcallback( ['IceCore', 'IceSample', 'IceCream'])
+    );
+
+    await ext_autocomplete.search("Core", gcallback([]));
+});
+
+QUnit.test("class", function(assert) {
+    assert.ok(ext_autocomplete.switch_on_completion , "toggle available");
+    assert.ok(ext_autocomplete.switch_on_completion() , "toggle runs");
+});
diff --git a/test/core/js/modules/ext_bookmarks.js.js b/test/core/js/modules/ext_bookmarks.js.js
new file mode 100644
index 0000000000000000000000000000000000000000..831df74231e479d5b4550524f6bc0da617c98fb3
--- /dev/null
+++ b/test/core/js/modules/ext_bookmarks.js.js
@@ -0,0 +1,181 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 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';
+
+QUnit.module("ext_bookmarks.js", {
+    before: function (assert) {
+        // setup before module
+        ext_bookmarks.set_collection_id("test");
+        // otherwise tests would collide with actual bookmarks
+    },
+    beforeEach: function (assert) {
+        // setup before each test
+    },
+    afterEach: function (assert) {
+        // teardown after each test
+        ext_bookmarks.clear_bookmark_storage();
+        connection._init();
+    },
+    after: function (assert) {
+        // teardown after module
+    }
+});
+
+QUnit.test("parse_uri", function(assert) {
+    assert.equal(typeof ext_bookmarks.parse_uri(""), "undefined");
+    assert.equal(typeof ext_bookmarks.parse_uri("asdf"), "undefined");
+    assert.equal(typeof ext_bookmarks.parse_uri("https://localhost:1234/Entity/sada?sadfasd#sdfgdsf"), "undefined");
+
+    assert.propEqual(ext_bookmarks.parse_uri("safgsa/123&456&789?sadfasdf#_bm_1"),
+        {bookmarks: ["123", "456", "789"], collection_id: "1"});
+});
+
+QUnit.test("get_bookmarks, clear_bookmark_storage", function(assert) {
+    assert.propEqual(ext_bookmarks.get_bookmarks(), []);
+
+    ext_bookmarks.bookmark_storage[ext_bookmarks.get_key("sdfg")] = "3456"
+    assert.propEqual(ext_bookmarks.get_bookmarks(), ["3456"]);
+
+    ext_bookmarks.clear_bookmark_storage();
+    assert.propEqual(ext_bookmarks.get_bookmarks(), []);
+});
+
+QUnit.test("get_export_table", async function (assert) {
+    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${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}`);
+
+});
+
+QUnit.test("update_clear_button", function (assert) {
+    const clear_button = $(`<div id="caosdb-f-bookmarks-clear"/>`);
+    $("body").append(clear_button);
+
+    assert.notOk(clear_button.is(".disabled"));
+    ext_bookmarks.update_clear_button([]);
+    assert.ok(clear_button.is(".disabled"));
+
+    ext_bookmarks.update_clear_button(["asdf"]);
+    assert.notOk(clear_button.is(".disabled"));
+
+    ext_bookmarks.update_clear_button(["asdf"]);
+    assert.notOk(clear_button.is(".disabled"));
+
+    ext_bookmarks.update_clear_button([]);
+    assert.ok(clear_button.is(".disabled"));
+
+    clear_button.remove();
+});
+
+QUnit.test("update_export_link", function (assert) {
+    const export_link = $(`<div id="caosdb-f-bookmarks-export-link"/>`);
+    $("body").append(export_link);
+
+    assert.notOk(export_link.is(".disabled"));
+    ext_bookmarks.update_export_link([]);
+    assert.ok(export_link.is(".disabled"));
+
+    ext_bookmarks.update_export_link(["asdf"]);
+    assert.notOk(export_link.is(".disabled"));
+
+    ext_bookmarks.update_export_link(["asdf"]);
+    assert.notOk(export_link.is(".disabled"));
+
+    ext_bookmarks.update_export_link([]);
+    assert.ok(export_link.is(".disabled"));
+
+    export_link.remove();
+});
+
+QUnit.test("update_collection_link", function (assert) {
+    const collection_link = $(
+      `<div id="caosdb-f-bookmarks-collection-link"><a/></div>`);
+    const a = collection_link.find("a")[0];
+    $("body").append(collection_link);
+
+    assert.notOk(collection_link.is(".disabled"));
+    assert.notOk(a.href);
+
+    ext_bookmarks.update_collection_link([]);
+    assert.ok(collection_link.is(".disabled"));
+    assert.notOk(a.href);
+
+    ext_bookmarks.update_collection_link(["asdf"]);
+    assert.notOk(collection_link.is(".disabled"));
+    assert.equal(a.href, ext_bookmarks.get_collection_link(["asdf"]));
+
+    ext_bookmarks.update_collection_link(["asdf", "sdfg"]);
+    assert.notOk(collection_link.is(".disabled"));
+    assert.equal(a.href, ext_bookmarks.get_collection_link(["asdf", "sdfg"]));
+
+    ext_bookmarks.update_collection_link([]);
+    assert.ok(collection_link.is(".disabled"));
+    assert.notOk(a.href);
+
+    collection_link.remove();
+});
+
+QUnit.test("bookmark buttons", function (assert) {
+    const inactive_button = $(`<div data-bmval="id1"/>`);
+    const active_button = $(`<div class="active" data-bmval="id2"/>`);
+    const broken_button = $(`<div data-bmval=""/>`);
+    const non_button = $(`<div data-bla="sadf"/>)`);
+    const outside_button = $(`<div data-bmval="id3"/>`);
+    const inside_buttons = $("<div/>").append([inactive_button, active_button,
+      broken_button, non_button]);
+
+    // get_bookmark_buttons
+    assert.equal(ext_bookmarks.get_bookmark_buttons("body").length, 0);
+
+    $("body").append([outside_button, inside_buttons]);
+
+    assert.equal(ext_bookmarks.get_bookmark_buttons("body").length, 4, "all but no_button");
+    assert.equal(ext_bookmarks.get_bookmark_buttons(inside_buttons).length, 3, "all but non_button and outside_button");
+
+    // get_value
+    assert.equal(ext_bookmarks.get_value(inactive_button), "id1");
+    assert.notOk(ext_bookmarks.get_value(non_button));
+    assert.notOk(ext_bookmarks.get_value(broken_button));
+
+    // init_button
+    assert.ok(active_button.is(".active"));
+    ext_bookmarks.init_button(active_button);
+    assert.notOk(active_button.is(".active"));
+
+    ext_bookmarks.bookmark_storage[ext_bookmarks.get_key("id1")] = "id1"
+    assert.notOk(inactive_button.is(".active"));
+    ext_bookmarks.init_button(inactive_button);
+    assert.ok(inactive_button.is(".active"));
+
+    ext_bookmarks.clear_bookmark_storage();
+    assert.notOk(inactive_button.is(".active"), "clear_bookmark_storage removes active class");
+
+    inside_buttons.remove();
+    outside_button.remove();
+});
diff --git a/test/core/js/modules/ext_bottom_line.js.js b/test/core/js/modules/ext_bottom_line.js.js
index 067aade473ba9918f9c1d44eee2b9bb60e8cc863..d4add1a8997b86dbf9f39566aa5f14b0b6721df1 100644
--- a/test/core/js/modules/ext_bottom_line.js.js
+++ b/test/core/js/modules/ext_bottom_line.js.js
@@ -45,7 +45,7 @@ var ext_bottom_line_test_suite = function ($, ext_bottom_line, QUnit) {
         },
         { "id": "test.success-2",
           "is_applicable": "(entity) => getParents(entity).map(par => par.name).includes('TestPreviewRecordType') && getEntityName(entity) != 'TestPreviewRecord-fall-back'",
-          "create": "(entity) => { return plotly_preview.create_plot([{x: [1,2,3,4,5], y: [1,2,4,8,16]}], { 'xaxis': {'title': 'time [samples]'}}); }"
+          "create": "(entity) => { return plotly_preview.create_plot([{x: [1,2,3,4,5], y: [1,2,4,8,16]}], { 'xaxis': {'title': 'time [samples]'}}, {displaylogo: false}); }"
         }
       ]
     };
@@ -66,8 +66,8 @@ var ext_bottom_line_test_suite = function ($, ext_bottom_line, QUnit) {
         }
     });
 
-    QUnit.test("app.creators", function (assert) {
-        assert.equal(ext_bottom_line.app.creators.length, 7, "seven creators");
+    QUnit.test("_creators", function (assert) {
+        assert.equal(ext_bottom_line._creators.length, 9, "nine creators, 5 default ones, 4 from these tests.");
     });
 
     QUnit.test("get_container - creation", function(assert) {
@@ -120,4 +120,12 @@ var ext_bottom_line_test_suite = function ($, ext_bottom_line, QUnit) {
 
     });
 
+    QUnit.test("tiff converter", async function(assert) {
+        let entity_xml = `<Response><File path="../pics/saturn.tif"/></Response>`;
+        const entity = (await transformation.transformEntities(str2xml(entity_xml)))[0];
+        const tiff_preview = await ext_bottom_line._creators.filter((c) => c.id == "_default_creators.tiff_images")[0].create(entity);
+
+        assert.equal($(tiff_preview).find("img").attr("src").slice(0,21), "data:image/png;base64", "decoded tiff to png");
+    });
+
 }($, ext_bottom_line, QUnit);
diff --git a/test/core/js/modules/ext_file_download.js.js b/test/core/js/modules/ext_file_download.js.js
new file mode 100644
index 0000000000000000000000000000000000000000..1103f61304c56259cc1a54a94c0c6b6b201d2aa5
--- /dev/null
+++ b/test/core/js/modules/ext_file_download.js.js
@@ -0,0 +1,65 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2020 Henrik tom Wörden <h.tomwoerden@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';
+
+QUnit.module("ext_file_download.js", {
+    before: function (assert) {
+        // setup before module
+    },
+    beforeEach: function (assert) {
+        // setup before each test
+    },
+    afterEach: function (assert) {
+        // teardown after each test
+    },
+    after: function (assert) {
+        // teardown after module
+    }
+});
+
+QUnit.test("chunk_list ", function(assert) {
+    const li = [1,2,3,4,5,6,7];
+    const res = ext_file_download.chunk_list(li, 3);
+    assert.equal(res.length, 3, "number of parts");
+    assert.propEqual(res[2], [7], "number of parts");
+});
+
+QUnit.test("collect_ids ", function (assert) {
+    const line = id => $(`<tr data-entity-id="${id}"/>`);
+    const prop_val = x => $(`<div class="caosdb-f-property-value"/>`);
+    const single_val =x =>  $(`<div class="caosdb-f-property-single-raw-value caosdb-id">${x}</div>`);
+
+
+    const line1 = line("34");
+    line1.append([prop_val().append(single_val("5")),prop_val().append(single_val("6"))])
+    $("body").append([line1]);
+
+    const res = ext_file_download.collect_ids()
+    assert.ok(res.indexOf("5") > -1, "missing id");
+    assert.ok(res.indexOf("6") > -1, "missing id");
+    assert.ok(res.indexOf("34") > -1, "missing id");
+
+    line1.remove();
+
+});
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 4f9dc6c59b156f1f2265acb4b315887536667194..5426580436933b942cec6750b521747ab62f2d00 100644
--- a/test/core/js/modules/ext_xls_download.js.js
+++ b/test/core/js/modules/ext_xls_download.js.js
@@ -79,16 +79,14 @@ QUnit.module("ext_xls_download", {
         return str2xml('<response><script code="0" /><stdout>bla</stdout></response>');
     }
 
-    caosdb_table_export._go_to_script_results = function(xls_link, filename) {
-        xls_link.setAttribute(
-            "href",
-            location.protocol + "//" +location.host + "/Shared/" + filename);
+    caosdb_table_export.go_to_script_results = function(filename) {
         assert.equal(filename, "bla", "filename correct");
         done();
     }
 
     var tsv_data = $('<a id="caosdb-f-query-select-data-tsv" />');
-    $(document.body).append(tsv_data);
+    var modal = $('<div id="downloadModal"><div>');
+    $(document.body).append([tsv_data, modal]);
 
 
     var xsl_link = $("<a/>");
@@ -96,12 +94,15 @@ QUnit.module("ext_xls_download", {
 
     await sleep(500);
 
-    assert.ok(xsl_link.attr("href").endsWith("Shared/bla"), xsl_link.attr("href") + " ends with Shared/bla");
-
     tsv_data.remove();
+    modal.remove();
   });
 }
 
+QUnit.test("_clean_cell", function(assert) {
+    assert.equal(caosdb_table_export._clean_cell("\n\t\n\t"), "    ", "No valid content");
+});
+
 QUnit.test("_get_property_value", function(assert) {
     var f = caosdb_table_export._get_property_value;
 
@@ -114,11 +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._get_tsv_string
+    var f = caosdb_table_export._create_tsv_string
     var tsv_string = f(entities, ["Bag", "Number"], true);
-    assert.equal(tsv_string, "data:text/csv;charset=utf-8,ID%09Bag%09Number%0A242%096366, 6406, 6407, 6408, 6409, 6410, 6411, 6412, 6413%090284%0A2112%09%091101", "tsv generated");
+    var prefix = "data:text/csv;charset=utf-8,"
+    assert.equal(tsv_string, 
+        "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\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/form_elements.js.js b/test/core/js/modules/form_elements.js.js
index 7f5930cd4253f408edf785045db10290697cd6c3..f8bf1ed1ac1495a8a3688aeb7b0cce387c8b69fb 100644
--- a/test/core/js/modules/form_elements.js.js
+++ b/test/core/js/modules/form_elements.js.js
@@ -541,3 +541,91 @@ QUnit.test("field_ready", function(assert) {
     });
 });
 
+{
+const sleep = (ms) => {
+  return new Promise(res => setTimeout(res, ms))
+}
+
+QUnit.test("make_alert - cancel", async function(assert) {
+    var cancel_callback = assert.async()
+    var _alert = form_elements.make_alert({
+        message: "message",
+        proceed_callback: () => {assert.ok(false, "this should not be called");},
+        cancel_callback: cancel_callback,
+    });
+    $("body").append(_alert);
+
+    assert.equal($("body").find(".caosdb-f-form-elements-alert").length, 1, "alert is there");
+    $(_alert).find("button.caosdb-f-btn-alert-cancel")[0].click();
+    await sleep(500);
+
+    assert.equal($("body").find(".caosdb-f-form-elements-alert").length, 0, "has been removed");
+
+
+});
+
+QUnit.test("make_alert - proceed", async function(assert) {
+    var proceed_callback = assert.async();
+    var _alert = form_elements.make_alert({
+        message: "message",
+        proceed_callback: proceed_callback,
+    });
+    assert.equal($(_alert).find("[type='checkbox']").length, 0, "no remember checkbox");
+
+    $("body").append(_alert);
+    assert.equal($("body").find(".caosdb-f-form-elements-alert").length, 1, "alert is there");
+    $(_alert).find("button.caosdb-f-btn-alert-proceed")[0].click();
+    await sleep(500);
+
+    assert.equal($("body").find(".caosdb-f-form-elements-alert").length, 0, "has been removed");
+
+});
+
+QUnit.test("make_alert - remember", async function(assert) {
+    form_elements._set_alert_decision("unittests", "");
+
+    var proceed_callback = assert.async(3);
+    var _alert = form_elements.make_alert({
+        message: "message",
+        proceed_callback: proceed_callback,
+        remember_my_decision_id: "unittests",
+    });
+    assert.equal($(_alert).find("[type='checkbox']").length, 1, "has remember checkbox");
+
+    // append for the first time
+    $("body").append(_alert);
+    assert.equal($("body").find(".caosdb-f-form-elements-alert").length, 1, "alert is there");
+
+    // click proceed without "don't ask me again".
+    $(_alert).find("button.caosdb-f-btn-alert-proceed")[0].click();
+    await sleep(500);
+
+    form_elements._set_alert_decision("unittests", "");
+
+    assert.equal($("body").find(".caosdb-f-form-elements-alert").length, 0, "has been removed");
+
+    // append 2nd time
+    _alert = form_elements.make_alert({
+        message: "message",
+        proceed_callback: proceed_callback,
+        remember_my_decision_id: "unittests",
+    });
+    assert.equal($(_alert).find("[type='checkbox']").length, 1, "has remember checkbox");
+    $("body").append(_alert);
+    assert.equal($("body").find(".caosdb-f-form-elements-alert").length, 1, "alert is there");
+    $(_alert).find("[type='checkbox']").prop("checked", true);
+
+    $(_alert).find("button.caosdb-f-btn-alert-proceed")[0].click();
+    await sleep(500);
+    form_elements._set_alert_decision("unittests", "proceed");
+
+    // try 3rd time
+    _alert = form_elements.make_alert({
+        message: "message",
+        proceed_callback: proceed_callback,
+        remember_my_decision_id: "unittests",
+    });
+    assert.equal(typeof _alert, "undefined", "alert was not created, proceed callback was called third time");
+});
+
+}
diff --git a/test/core/js/modules/webcaosdb.js.js b/test/core/js/modules/webcaosdb.js.js
index 239758161b8419a8a5e15799c67c909977f1fbf0..7b3f7abf404f261668c18690df337b73794dd8bd 100644
--- a/test/core/js/modules/webcaosdb.js.js
+++ b/test/core/js/modules/webcaosdb.js.js
@@ -324,7 +324,7 @@ QUnit.test("createUpdateForm", function(assert) {
 QUnit.test("createUpdateEntityHeading", function(assert) {
     let cueh = transaction.update.createUpdateEntityHeading;
     assert.ok(cueh, "function available");
-    let eh = $('<div class="panel-heading"><div class="1strow"/><div class="2ndrow"/></div>')[0];
+    let eh = $('<div class="panel-heading"><div class="1strow"></div><div class="2ndrow"></div></div>')[0];
     assert.equal($(eh).children('.1strow').length, 1, "eh has 1st row");
     assert.equal($(eh).children('.2ndrow').length, 1, "eh has 2nd row");
     let uh = cueh(eh);
@@ -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/pics/saturn.tif b/test/core/pics/saturn.tif
new file mode 100644
index 0000000000000000000000000000000000000000..f226ebab7fca3c9caa306f78d80eaa7b4068f994
Binary files /dev/null and b/test/core/pics/saturn.tif differ
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 12f26180b89bdf3f2ba3280d4546783f54bfa8ea..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,8 +9,9 @@
     </Selection>
   </Query>
   <Record id="242">
+    <Version id="abc123" head="true"/>
     <Property id="117" name="Number" datatype="TEXT" importance="FIX">
-      0284
+      02&#x9;8&#xA;&#xA;&#xA;4aaa&#x9;a
     </Property>
     <Property id="104" name="Bag" datatype="Bag" importance="FIX">
       6366
@@ -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 a19843fb3de11bbedf1c01f71619470bcc99f75c..026887097ac3b7dc13e6e429bf73c363e3adcbf3 100644
--- a/test/docker/Dockerfile
+++ b/test/docker/Dockerfile
@@ -1,7 +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
-RUN git clone -b dev https://gitlab.com/caosdb/caosdb-pylib.git && \
-    cd caosdb-pylib && pip3 install .
+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==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/data/bad.csv b/test/server_side_scripting/ext_table_preview/data/bad.csv
new file mode 100644
index 0000000000000000000000000000000000000000..d29a9312a387186beb8bf4f77a8ec0e4b0ab80fa
Binary files /dev/null and b/test/server_side_scripting/ext_table_preview/data/bad.csv differ
diff --git a/test/server_side_scripting/ext_table_preview/data/bad.tsv b/test/server_side_scripting/ext_table_preview/data/bad.tsv
new file mode 100644
index 0000000000000000000000000000000000000000..d29a9312a387186beb8bf4f77a8ec0e4b0ab80fa
Binary files /dev/null and b/test/server_side_scripting/ext_table_preview/data/bad.tsv differ
diff --git a/test/server_side_scripting/ext_table_preview/data/bad.xls b/test/server_side_scripting/ext_table_preview/data/bad.xls
new file mode 100644
index 0000000000000000000000000000000000000000..1f31bf2754258e3d07f88fd1e6bdee4d7b11bee1
Binary files /dev/null and b/test/server_side_scripting/ext_table_preview/data/bad.xls differ
diff --git a/test/server_side_scripting/ext_table_preview/data/bad.xlsx b/test/server_side_scripting/ext_table_preview/data/bad.xlsx
new file mode 100644
index 0000000000000000000000000000000000000000..1f31bf2754258e3d07f88fd1e6bdee4d7b11bee1
Binary files /dev/null and b/test/server_side_scripting/ext_table_preview/data/bad.xlsx differ
diff --git a/test/server_side_scripting/ext_table_preview/data/server_error.csv b/test/server_side_scripting/ext_table_preview/data/server_error.csv
new file mode 100644
index 0000000000000000000000000000000000000000..3e770df012f65d73ce4721a5f65d7e3f39959519
--- /dev/null
+++ b/test/server_side_scripting/ext_table_preview/data/server_error.csv
@@ -0,0 +1 @@
+Hi,  this line contains a unicode backspace. This causes a server error, when pandas_table_preview.py's output is serialized into XML.
\ No newline at end of file
diff --git a/test/server_side_scripting/ext_table_preview/data/test.csv b/test/server_side_scripting/ext_table_preview/data/test.csv
new file mode 100644
index 0000000000000000000000000000000000000000..7c9bfd1354393439f551021cfe340577433ce2aa
--- /dev/null
+++ b/test/server_side_scripting/ext_table_preview/data/test.csv
@@ -0,0 +1,12 @@
+# test header
+# two lines
+A1,B1,C1,D1,E1,F1,G1,H1,I1,J1,K1,L1,M1,N1,O1,P1,Q1,R1,S1,T1
+A2,B2,C2,D2,E2,F2,G2,H2,I2,J2,K2,L2,M2,N2,O2,P2,Q2,R2,S2,T2
+A3,B3,csvfile,D3,E3,F3,G3,H3,I3,J3,K3,L3,M3,N3,O3,P3,Q3,R3,S3,T3
+A5,B5,C5,D5,E5,F5,G5,H5,I5,J5,K5,L5,M5,N5,O5,P5,Q5,R5,S5,T5
+A6,B6,C6,D6,E6,F6,G6,H6,I6,J6,K6,L6,M6,N6,O6,P6,Q6,R6,S6,T6
+A7,B7,csvfile,D7,E7,F7,G7,H7,I7,J7,K7,L7,M7,N7,O7,P7,Q7,R7,S7,T7
+A8,B8,C8,D8,E8,F8,G8,H8,I8,J8,K8,L8,M8,N8,O8,P8,Q8,R8,S8,T8
+A9,B9,C9,D9,E9,F9,G9,H9,I9,J9,K9,L9,M9,N9,O9,P9,Q9,R9,S9,T9
+A10,B10,C10,D10,E10,F10,G10,H10,I10,J10,K10,L10,M10,N10,O10,P10,Q10,R10,S10,T10
+A11,B11,C11,D11,E11,F11,G11,H11,I11,J11,K11,L11,M11,N11,O11,P11,Q11,R11,S11,T11
diff --git a/test/server_side_scripting/ext_table_preview/data/test.tsv b/test/server_side_scripting/ext_table_preview/data/test.tsv
new file mode 100644
index 0000000000000000000000000000000000000000..863f692bf64e7dcabf74703587a93e37adf27e67
--- /dev/null
+++ b/test/server_side_scripting/ext_table_preview/data/test.tsv
@@ -0,0 +1,12 @@
+# test header
+# two lines
+A1	B1	C1	D1	E1	F1	G1	H1	I1	J1	K1	L1	M1	N1	O1	P1	Q1	R1	S1	T1
+A2	B2	C2	D2	E2	F2	G2	H2	I2	J2	K2	L2	M2	N2	O2	P2	Q2	R2	S2	T2
+A3	B3	csvfile	D3	E3	F3	G3	H3	I3	J3	K3	L3	M3	N3	O3	P3	Q3	R3	S3	T3
+A5	B5	C5	D5	E5	F5	G5	H5	I5	J5	K5	L5	M5	N5	O5	P5	Q5	R5	S5	T5
+A6	B6	C6	D6	E6	F6	G6	H6	I6	J6	K6	L6	M6	N6	O6	P6	Q6	R6	S6	T6
+A7	B7	tsvfile	D7	E7	F7	G7	H7	I7	J7	K7	L7	M7	N7	O7	P7	Q7	R7	S7	T7
+A8	B8	C8	D8	E8	F8	G8	H8	I8	J8	K8	L8	M8	N8	O8	P8	Q8	R8	S8	T8
+A9	B9	C9	D9	E9	F9	G9	H9	I9	J9	K9	L9	M9	N9	O9	P9	Q9	R9	S9	T9
+A10	B10	C10	D10	E10	F10	G10	H10	I10	J10	K10	L10	M10	N10	O10	P10	Q10	R10	S10	T10
+A11	B11	C11	D11	E11	F11	G11	H11	I11	J11	K11	L11	M11	N11	O11	P11	Q11	R11	S11	T11
diff --git a/test/server_side_scripting/ext_table_preview/data/test.xls b/test/server_side_scripting/ext_table_preview/data/test.xls
new file mode 100644
index 0000000000000000000000000000000000000000..a355756b9ab72f9035246c5303800a2076d9bfc0
Binary files /dev/null and b/test/server_side_scripting/ext_table_preview/data/test.xls differ
diff --git a/test/server_side_scripting/ext_table_preview/data/test.xlsx b/test/server_side_scripting/ext_table_preview/data/test.xlsx
new file mode 100644
index 0000000000000000000000000000000000000000..bc291f1aa86cd6d550320f07a7ce69cf813b8116
Binary files /dev/null and b/test/server_side_scripting/ext_table_preview/data/test.xlsx differ
diff --git a/test/server_side_scripting/ext_table_preview/data/xss_attack.csv b/test/server_side_scripting/ext_table_preview/data/xss_attack.csv
new file mode 100644
index 0000000000000000000000000000000000000000..e7d43505aef42c397f1859805bc87aab8b6da1a2
--- /dev/null
+++ b/test/server_side_scripting/ext_table_preview/data/xss_attack.csv
@@ -0,0 +1,8 @@
+# as it seems all these characters are escaped correctly.
+"![Alt text](url/to/image)","%3C","&lt","&lt;","&LT","&LT;","&#60","&#060","&#0060","&#00060"
+"&#000060","&#0000060","&#60;","&#060;","&#0060;","&#00060;","&#000060;","&#0000060;","&#x3c","&#x03c"
+"&#x003c","&#x0003c","&#x00003c","&#x000003c","&#x3c;","&#x03c;","&#x003c;","&#x0003c;","&#x00003c;","&#x000003c;"
+"&#X3c","&#X03c","&#X003c","&#X0003c","&#X00003c","&#X000003c","&#X3c;","&#X03c;","&#X003c;","&#X0003c;"
+"&#X00003c;","&#X000003c;","&#x3C","&#x03C","&#x003C","&#x0003C","&#x00003C","&#x000003C","&#x3C;","&#x03C;"
+"&#x003C;","&#x0003C;","&#x00003C;","&#x000003C;","&#X3C","&#X03C","&#X003C","&#X0003C","&#X00003C","&#X000003C"
+"&#X3C;","&#X03C;","&#X003C;","&#X0003C;","&#X00003C;","&#X000003C;","\x3c","\x3C","\u003c","\u003C"
diff --git a/test/server_side_scripting/ext_table_preview/pandas_table_preview.py b/test/server_side_scripting/ext_table_preview/pandas_table_preview.py
new file mode 120000
index 0000000000000000000000000000000000000000..f24b3901ce8610fd02fc28468b747b7171834307
--- /dev/null
+++ b/test/server_side_scripting/ext_table_preview/pandas_table_preview.py
@@ -0,0 +1 @@
+../../../src/server_side_scripting/ext_table_preview/pandas_table_preview.py
\ No newline at end of file
diff --git a/test/server_side_scripting/ext_table_preview/requirements.txt b/test/server_side_scripting/ext_table_preview/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..4628529ba9dce50a08d574e21d3b4a71b50af2b1
--- /dev/null
+++ b/test/server_side_scripting/ext_table_preview/requirements.txt
@@ -0,0 +1,3 @@
+caosdb
+caosadvancedtools
+pandas
diff --git a/test/server_side_scripting/ext_table_preview/test_pandas_table_preview.py b/test/server_side_scripting/ext_table_preview/test_pandas_table_preview.py
new file mode 100644
index 0000000000000000000000000000000000000000..00d1c7f38746abe437abc76cd51b29600adcd049
--- /dev/null
+++ b/test/server_side_scripting/ext_table_preview/test_pandas_table_preview.py
@@ -0,0 +1,74 @@
+#!/usr/bin/env python3
+# encoding: utf-8
+#
+# ** header v3.0
+# This file is a part of the CaosDB Project.
+#
+# Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+# Copyright (C) 2020 Henrik tom Wörden <h.tomwoerden@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
+#
+
+import os
+import unittest
+
+import caosdb as db
+from caosdb.common.models import _parse_single_xml_element
+from lxml import etree
+from pandas_table_preview import (MAXIMUMFILESIZE, create_table_preview,
+                                  ending_is_valid, read_file, size_is_ok)
+
+
+class PreviewTest(unittest.TestCase):
+    def test_file_ending(self):
+        self.assertFalse(ending_is_valid("/this/is/no/xls.lol"))
+        self.assertFalse(ending_is_valid("xls.lol"))
+        self.assertFalse(ending_is_valid("ag.xls.lol"))
+        assert ending_is_valid("/this/is/a/lol.xls")
+        assert ending_is_valid("/this/is/a/lol.csv")
+        assert ending_is_valid("/this/is/a/lol.cSv")
+        assert ending_is_valid("/this/is/a/lol.CSV")
+        assert ending_is_valid("lol.CSV")
+
+    def test_file_size(self):
+        entity_xml = ('<File id="1234" name="SomeFile" '
+                      'path="/this/path.tsv" size="{size}"></File>')
+        small = _parse_single_xml_element(
+            etree.fromstring(entity_xml.format(size="20000")))
+
+        assert size_is_ok(small)
+        large = _parse_single_xml_element(
+            etree.fromstring(entity_xml.format(
+                size=str(int(MAXIMUMFILESIZE+1)))))
+        assert not size_is_ok(large)
+
+    def test_output(self):
+        files = [os.path.join(os.path.dirname(__file__), "data", f)
+                 for f in ["test.csv", "test.tsv", "test.xls", "test.xlsx"]]
+
+        for fi in files:
+            table = read_file(fi, ftype="."+fi.split(".")[-1])
+            searchkey = fi.split(".")[-1]+"file"
+            print(table)
+            assert (table == searchkey).any(axis=None)
+
+        badfiles = [os.path.join(os.path.dirname(__file__), "data", f)
+                    for f in ["bad.csv", "bad.tsv", "bad.xls", "bad.xlsx"]]
+
+        for bfi in badfiles:
+            self.assertRaises(ValueError, read_file,
+                              bfi, "."+bfi.split(".")[-1])