diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index bcbcfcda9df7fa823ecd1454990bb8d6ff28ff79..eeced833b2e991e36937c8d6968b445832d57d37 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -23,7 +23,7 @@
 
 variables:
   DEPLOY_REF: dev
-  CI_REGISTRY_IMAGE: $CI_REGISTRY/caosdb/caosdb-webui/testenv
+  CI_REGISTRY_IMAGE: $CI_REGISTRY/caosdb/src/caosdb-webui/testenv
   # When using dind, it's wise to use the overlayfs driver for
   # improved performance.
 
@@ -73,9 +73,8 @@ trigger_build:
   tags: [ docker ]
   stage: deploy
   script:
-    - echo $TOKEN
     - /usr/bin/curl -X POST
-       -F token=$DEPLOY_TRIGGER_TOKEN
+       -F token=$CI_JOB_TOKEN
        -F "variables[F_BRANCH]=$CI_COMMIT_REF_NAME"
        -F "variables[WEBUI]=$CI_COMMIT_REF_NAME"
        -F "variables[TriggerdBy]=WEBUI"
@@ -88,6 +87,9 @@ build-testenv:
   image: docker:19.03
   stage: setup
   timeout: 3 h
+  only:
+    - web
+    - shedules
   script: 
     - cd test/docker
     - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
@@ -106,7 +108,12 @@ pages:
   tags: [ docker ]
   stage: deploy
   only:
-    - dev
+    refs:
+      - /^release-.*$/i
+      - master
+    variables:
+      # run pages only on gitlab.com
+      - $CI_SERVER_HOST == "gitlab.com"
   script:
       # TODO is this a good location here?
     - npm install jsdoc
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 985d1541eaeb597156b4ddb3de377b55d71e021c..60242d138a183f92ba46b1be036839b3100ff354 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,14 +8,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Added (for new features, dependecies etc.)
 
+* Visually highlighted drop zones for properties and parents in the edit_mode.
+* two new field types for the form_elements module, `file` and `select`. See
+  the module documentation for more information.
+
 ### Changed (for changes in existing functionality)
 
 ### Deprecated (for soon-to-be removed features) 
 
 ### Removed (for now removed features)
 
+* `ext_revisions` module. This module was only a work-around which had been
+  used for versioning functionality before the native versioning was
+  implemented. Also, the `BUILD_MODULE_EXT_REVISIONS` is no longer used and can
+  be removed from the config files in `build.properties.d/`
+
 ### Fixed
 
+* #156 - Edit mode for Safari 11
+* #160 - Entity preview for Safari 11
+* Several minor cosmetic flaws
+* Fixed edit mode for Safari 11.
+
 ### Security (in case of vulnerabilities)
 
 ## [0.3.0] - 2021-02-10
@@ -60,6 +74,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - summaries when opening preview
 - #125 special characters like "\t", \"n", "#" are replaced in table
   download
+- #158 show preview if the entity is too large for the viewport if
+  bottom line is in view.
 
 ### Security (in case of vulnerabilities)
 
diff --git a/README.md b/README.md
index 3f144a30731dab4f425f3f8978248f730a4c392c..5abbfafe8f45ad430e91d66ddd50f055cad61a25 100644
--- a/README.md
+++ b/README.md
@@ -1,27 +1,47 @@
-<!--THIS FILE HAS BEEN GENERATED BY A SCRIPT. PLEASE DON'T CHANGE IT MANUALLY.-->
 
-Project migrated to https://gitlab.com/caosdb
+# README
 
-# Welcome
+## Welcome
 
-This is the **CaosDB WebUI** repository and a part of the CaosDB project.
+This is the **CaosDB Web User Interface** repository and a part of the
+CaosDB project.
 
-# Setup
+## Setup
 
 Please read the [README_SETUP.md](README_SETUP.md) for instructions on how to
 setup this code.
 
 
-# Further Reading
+## Further Reading
 
-Please refer to the [official gitlab repository of the CaosDB
-project](https://gitlab.com/caosdb/caosdb) for more information.
+Please refer to the [official documentation](https://docs.indiscale.com/caosdb-webui/) for more information.
 
-# License
+## Contributing
 
-Copyright (C) 2018 Research Group Biomedical Physics, Max Planck Institute for
-Dynamics and Self-Organization Göttingen.
+Thank you very much to all contributers—[past, present](https://gitlab.com/caosdb/caosdb/-/blob/dev/HUMANS.md), and prospective ones.
+
+### Code of Conduct
+
+By participating, you are expected to uphold our [Code of Conduct](https://gitlab.com/caosdb/caosdb/-/blob/dev/CODE_OF_CONDUCT.md).
+
+### How to Contribute
+
+* You found a bug, have a question, or want to request a feature? Please
+[create an issue](https://gitlab.com/caosdb/caosdb-webui/-/issues).
+* You want to contribute code? Please fork the repository and create a merge
+request in GitLab and choose this repository as target. Make sure to select
+"Allow commits from members who can merge the target branch" under Contribution
+when creating the merge request. This allows our team to work with you on your request.
+- If you have a suggestion for the [documentation](https://docs.indiscale.com/caosdb-webui/),
+the preferred way is also a merge request as describe above (the documentation resides in `src/doc`).
+However, you can also create an issue for it.
+- You can also contact us at **info (AT) caosdb.de**.
+
+## License
+
+* Copyright (C) 2018 Research Group Biomedical Physics, Max Planck Institute
+  for Dynamics and Self-Organization Göttingen.
+* Copyright (C) 2020-2021 Indiscale GmbH <info@indiscale.com>
 
 All files in this repository are licensed under a [GNU Affero General Public
 License](LICENCE.md) (version 3 or later).
-
diff --git a/misc/revision_test_data.py b/misc/revision_test_data.py
deleted file mode 100755
index 0f41fa9c1b748be0cbc6c5b917dc6739d3c21d89..0000000000000000000000000000000000000000
--- a/misc/revision_test_data.py
+++ /dev/null
@@ -1,46 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-"""
-Copyright 2020 IndiScale GmbH <info@indiscale.com>
-Copyright 2020 Timm Fitschen <t.fitschen@indiscale.com>
-"""
-
-import caosdb
-import random
-import os
-
-# data model
-c = caosdb.execute_query("FIND Test*")
-if len(c) > 0:
-    print(c)
-    delete = input("Delete these entities?\nType `yes`:")
-    if delete == "yes":
-        c.delete();
-    else:
-        print("You typed `{}`".format(delete))
-        print("[Canceled]")
-        exit(0)
-
-
-print("inserting test data")
-
-upload_file = open("test.dat", "w")
-upload_file.write("hello world\n")
-upload_file.close()
-
-testdata = caosdb.Container()
-testdata.extend([
-    caosdb.File("TestFile",
-        path="test.dat",
-        file="test.dat"),
-    caosdb.Property("TestRevisionOf", datatype="TestObsolete"),
-    caosdb.RecordType("TestObsolete"),
-    caosdb.RecordType("TestRecordType"),
-    caosdb.Property("TestProperty", datatype=caosdb.TEXT),
-    caosdb.Record("TestRecord"
-                 ).add_parent("TestRecordType"
-                 ).add_property("TestProperty", "this is a test"),
-])
-
-testdata.insert()
-os.remove("test.dat")
diff --git a/misc/yaml_to_json.py b/misc/yaml_to_json.py
index a7d5bd62a7a1ccc50766b797ef6710466e9bee11..e77e5efc56b2cea39b0a7b6f90236fb5b39da24e 100755
--- a/misc/yaml_to_json.py
+++ b/misc/yaml_to_json.py
@@ -6,4 +6,4 @@ import json
 import yaml
 
 with open(sys.argv[1], 'r') as infile:
-    print(json.dumps(yaml.load(infile)))
+    print(json.dumps(yaml.safe_load(infile)))
diff --git a/src/core/css/webcaosdb.css b/src/core/css/webcaosdb.css
index 69a700376423a44bcb28a9920f1f3d15ef9a3b90..c4329eb20c788acb933b6b9f8d4476019ece6df4 100644
--- a/src/core/css/webcaosdb.css
+++ b/src/core/css/webcaosdb.css
@@ -27,6 +27,14 @@ body {
     flex-direction: column;
 }
 
+html {
+    min-height: 100vh%;
+    background-color: lightgray;
+}
+
+.caosdb-v-server-message strong {
+  margin-right: 8px;
+}
 
 div.export-data {
     display: none;
@@ -169,6 +177,7 @@ button.caosdb-v-entity-version-button {
 .caosdb-f-main {
     display: flex;
     width: unset;
+    min-height: 65vh;
 }
 .caosdb-f-main-entities {
     width: calc(100% - 5px);
@@ -618,6 +627,74 @@ input[type="file"] {
     min-height: 22px;
 }
 
+.caosdb-v-property-value-inputs > textarea {
+    width: 100%;
+}
+
+.caosdb-v-property-value-inputs li > textarea {
+    width: calc(100% - 40px);
+}
+
+.caosdb-v-edit-mode-property-dropzone {
+    list-style: none;
+    text-align: center;
+    color: #69c2df;
+    border: 2px dashed #69c2df;
+    padding: 25px 0px;
+    margin: 25px 0px;
+}
+
+.caosdb-v-edit-mode-property-dropzone:hover {
+    filter: brightness(80%);
+}
+
+.caosdb-v-edit-mode-parent-dropzone {
+    position: relative;
+    display: block;
+    color: #69c2df;
+    border: 2px dashed #69c2df;
+    padding-top: 15px;
+    padding-bottom: 15px;
+    padding-left: 5px;
+    padding-right: 15px;
+    margin: 0;
+}
+
+.caosdb-v-edit-mode-parent-dropzone:hover {
+    filter: brightness(80%);
+}
+
+.caosdb-v-edit-mode-highlight {
+    color: #333;
+    background-color: #d3fdd3;
+    border: 2px solid #d3fdd3;
+}
+
+.caosdb-v-edit-mode-parent-dropzone div:first-child {
+    font-size: 80%;
+    position: absolute;
+    top: 0px;
+    right: 0px;
+    margin: 0px;
+}
+
+.caosdb-v-property-value-inputs .caosdb-v-edit-value-list-buttons > button {
+    padding: 1px;
+}
+
+.caosdb-v-property-other-inputs * + * {
+    margin-left: 6px;
+}
+
+.caosdb-v-property-other-inputs label * {
+    margin-left: 6px;
+}
+
+.caosdb-v-property-other-inputs {
+    margin-top: 10px;
+    margin-bottom: 10px;
+}
+
 footer {
     background-color: lightgrey;
     padding: 0.5em;
diff --git a/src/core/js/caosdb.js b/src/core/js/caosdb.js
index 0c63fbe038908903dcf426d1c711d4fad6d3bdd2..cb10201556af4eca6fd3105397eda1a3f5552c0b 100644
--- a/src/core/js/caosdb.js
+++ b/src/core/js/caosdb.js
@@ -964,23 +964,15 @@ function appendProperty(doc, element, property, append_datatype = false) {
 
 
 /**
- * Return a new Document or DocumentFragment, depending on the availability of the latter.
+ * Return a new Document.
  *
  * Helper function.
  *
  * @param {string} root - the new root element.
- * @returns {(Document|DocumentFragement)} the new document.
+ * @returns {Document} the new document.
  */
 function _createDocument(root) {
-    var doc = undefined;
-    if (window.DocumentFragment) {
-        doc = new DocumentFragment();
-        const rootNode = document.createElementNS(undefined, root);
-        doc.append(rootNode);
-    } else {
-        doc = document.implementation.createDocument(null, root, null);
-    }
-    return doc;
+    return document.implementation.createDocument(null, root, null);
 }
 
 
diff --git a/src/core/js/edit_mode.js b/src/core/js/edit_mode.js
index 9df7505f1e81fd0234aaa4461444535846921885..c37dcb1b62b75e5e2078ffb10670288441d2be07 100644
--- a/src/core/js/edit_mode.js
+++ b/src/core/js/edit_mode.js
@@ -106,34 +106,47 @@ var edit_mode = new function() {
         if (typeof new_prop === "undefined" || !(new_prop instanceof HTMLElement)) {
             throw new TypeError("new_prop must instantiate HTMLElement");
         }
-        var rt = entity.getElementsByClassName("caosdb-properties")[0];
-        rt.appendChild(new_prop);
+        const drop_zone = $(entity).find(".caosdb-properties").find(".caosdb-f-edit-mode-property-dropzone");
+        drop_zone.before(new_prop);
         make_property_editable_cb(new_prop);
         new_prop.dispatchEvent(edit_mode.property_added);
     }
 
-    this.add_dropped_property = function(e, panel) {
+
+    /**
+     * Add a dropped property to the entity.
+     *
+     * @param {Event} e - the drop event.
+     * @param {HTMLElement} entity - the entity.
+     */
+    this.add_dropped_property = function(e, entity) {
         var propsrcid = e.dataTransfer.getData("text/plain");
         var tmp_id = propsrcid.split("-");
         var prop_id = tmp_id[tmp_id.length - 1];
         var entity_type = tmp_id[tmp_id.length - 2];
         if (entity_type == "p") {
             retrieve_dragged_property(prop_id).then(new_prop_doc => {
-                edit_mode.add_new_property(panel, new_prop_doc.firstChild);
+                edit_mode.add_new_property(entity, new_prop_doc.firstChild);
             }, edit_mode.handle_error);
         } else if (entity_type == "rt") {
             var name = $("#" + propsrcid).text();
             var dragged_rt = str2xml('<Response><Property id="' + prop_id + '" name="' + name + '" datatype="' + name + '"></Property></Response>');
             transformation.transformProperty(dragged_rt).then(new_prop_doc => {
-                edit_mode.add_new_property(panel, new_prop_doc.firstChild);
+                edit_mode.add_new_property(entity, new_prop_doc.firstChild);
             }, edit_mode.handle_error);
         }
     }
 
 
-    this.add_dropped_parent = function(e, panel) {
+    /**
+     * Add a dropped parent to the entity.
+     *
+     * @param {Event} e - the drop event.
+     * @param {HTMLElement} entity - the entity.
+     */
+    this.add_dropped_parent = function(e, entity) {
         var propsrcid = e.dataTransfer.getData("text/plain");
-        var parent_list = panel.getElementsByClassName("caosdb-f-parent-list")[0]
+        var parent_list = entity.getElementsByClassName("caosdb-f-parent-list")[0]
         var tmp_id = propsrcid.split("-");
         var prop_id = tmp_id[tmp_id.length - 1];
         var entity_type = tmp_id[tmp_id.length - 2];
@@ -150,14 +163,14 @@ var edit_mode = new function() {
                     is_parent=true
                 );
                 */
-                edit_mode.add_parent_delete_buttons(panel);
+                edit_mode.add_parent_delete_buttons(entity);
             }, edit_mode.handle_error);
 
         }
     }
 
     this.property_drop_listener = function(e) {
-        edit_mode._drop_listener.call(this, e, edit_mode.add_dropped_property); 
+        edit_mode._drop_listener.call(this, e, edit_mode.add_dropped_property);
     }
 
     this.parent_drop_listener = function(e) {
@@ -411,7 +424,7 @@ var edit_mode = new function() {
      *     entity in XML representation.
      */
     this.form_to_xml = function(entity_form) {
-        const obj = form_elements.form_to_object($(entity_form).find("form")[0]);
+        const obj = form_elements.form_to_object($(entity_form).find("form")[0])[0];
         var entityRole = getEntityRole(entity_form);
         var file_path = undefined;
         var file_checksum = undefined;
@@ -527,12 +540,12 @@ var edit_mode = new function() {
         return edit_mode_li[0];
     }
 
-    this.toggle_edit_mode = function() {
+    this.toggle_edit_mode = async function() {
         edit_mode.toggle_edit_panel();
         if (edit_mode.is_edit_mode()) {
-            edit_mode.leave_edit_mode();
+            await edit_mode.leave_edit_mode();
         } else {
-            edit_mode.enter_edit_mode();
+            await edit_mode.enter_edit_mode();
         }
     }
 
@@ -543,15 +556,15 @@ var edit_mode = new function() {
     this.leave_edit_mode = function() {}
 
 
-    this.enter_edit_mode = function(editApp = undefined) {
+    this.enter_edit_mode = async function(editApp = undefined) {
         window.localStorage.edit_mode = "true";
 
         var editPanel = edit_mode.get_edit_panel();
         removeAllWaitingNotifications(editPanel);
         this.add_wait_datamodel_info();
 
-        // TODO make enter_edit_mode ayncronous?
-        return edit_mode.retrieve_data_model().then(model => {
+        try {
+            const model = await edit_mode.retrieve_data_model();
             $(".caosdb-f-btn-toggle-edit-mode").text("Leave Edit Mode");
 
             edit_mode.init_tool_box(model);
@@ -567,7 +580,9 @@ var edit_mode = new function() {
             };
 
             return nextEditApp;
-        }, edit_mode.handle_error);
+        } catch (err) {
+            edit_mode.handle_error(err);
+        }
     }
 
 
@@ -650,6 +665,7 @@ var edit_mode = new function() {
         header.children().remove();
         const form = $('<form class="form-horizontal"></form>').append(inputs);
         header.append(form);
+        edit_mode.add_parent_dropzone(entity);
 
         edit_mode.make_datatype_input_logic(form[0]);
         edit_mode.add_parent_delete_buttons(header[0]);
@@ -839,7 +855,7 @@ var edit_mode = new function() {
     this.createElementForProperty = function(property, options) {
         var result;
         if (property.datatype == "TEXT") {
-            result = "<textarea>" + ( property.value || "" ) + "</textarea>";
+            result = `<textarea>${property.value || ""}</textarea>`;
         } else if (property.datatype == "DATETIME") {
             var dateandtime = [""];
             if(property.value) {
@@ -912,7 +928,9 @@ var edit_mode = new function() {
             this.parentElement.parentElement.parentElement.dispatchEvent(edit_mode.list_value_input_added);
         });
 
-        return $("<span></span>").append(deleteButton).append(insertButton)[0];
+        return $('<span class="caosdb-v-edit-value-list-buttons"></span>')
+            .append(deleteButton)
+            .append(insertButton)[0];
     }
 
 
@@ -1095,9 +1113,9 @@ var edit_mode = new function() {
      */
     this.add_toggle_list_checkbox = function (element, list, datatype) {
         var editfield = $(element).find(".caosdb-f-property-value");
-        var label = "List ";
+        var label = $('<label>List</label>');
         var checkbox = $('<input type="checkbox" class="caosdb-f-entity-is-list"/>');
-        $(element).find(".caosdb-property-edit").prepend(checkbox).prepend(label);
+        $(element).find(".caosdb-property-edit").prepend(label.append(checkbox));
 
         checkbox.prop("checked", list);
 
@@ -1162,7 +1180,11 @@ var edit_mode = new function() {
     this.make_property_editable = function(element) {
         caosdb_utils.assert_html_element(element, "param 'element'");
 
-        var editfield = $(element).find(".caosdb-f-property-value");
+        var editfield = $(element).find(".caosdb-f-property-value")
+            .removeClass("col-sm-8")
+            .addClass("col-sm-6")
+            .addClass("caosdb-v-property-value-inputs")
+            .after(`<div class="col-sm-2 caosdb-v-property-other-inputs caosdb-property-edit" style="text-align: right;"/>`);
         var property = getPropertyFromElement(element);
 
 
@@ -1487,6 +1509,9 @@ var edit_mode = new function() {
             for (var element of prop_elements) {
                 edit_mode.make_property_editable(element);
             }
+            if(getEntityRole(app.entity) != "Property") {
+              edit_mode.add_property_dropzone(app.entity);
+            }
             app.entity.dispatchEvent(edit_mode.start_edit);
         }
         app.onEnterWait = function(e) {
@@ -1580,6 +1605,18 @@ var edit_mode = new function() {
         });
     }
 
+    this.add_property_dropzone = function (entity) {
+        $(entity).find("ul.caosdb-properties")
+            .append('<li class="caosdb-v-edit-mode-dropzone caosdb-f-edit-mode-property-dropzone caosdb-v-edit-mode-property-dropzone">Drag and drop Properties and RecordTypes from the Edit Mode Toolbox here.</li>');
+    }
+
+    this.add_parent_dropzone = function (entity) {
+        $(entity).find(".caosdb-f-parent-list")
+            .addClass("caosdb-v-edit-mode-parent-dropzone")
+            .addClass("caosdb-v-edit-mode-dropzone")
+            .prepend('<div>Drag and drop RecordTypes from the Edit Mode Toolbox here.</div>');
+    }
+
     this.unfreeze = function() {
         $('.caosdb-f-main-entities').children().each(function(index) {
             edit_mode.unfreeze_entity(this);
@@ -1665,11 +1702,13 @@ var edit_mode = new function() {
     }
 
     this.highlight = function(entity) {
-        $(entity).addClass("caosdb-v-edit-mode-highlight").css("background-color", "lightgreen");
+        $(entity).find(".caosdb-v-edit-mode-dropzone")
+            .addClass("caosdb-v-edit-mode-highlight");
     }
 
     this.unhighlight = function() {
-        $('.caosdb-v-edit-mode-highlight').removeClass("caosdb-v-edit-mode-highlight").css("background-color", "");
+        $('.caosdb-v-edit-mode-highlight')
+            .removeClass("caosdb-v-edit-mode-highlight");
     }
 
     this.handle_error = function(err) {
@@ -1766,8 +1805,48 @@ var edit_mode = new function() {
         }
     }
 
+    /**
+     * List of all permissions which indicate that the edit button should be
+     * visible.
+     */
+    const UPDATE_PERMISSIONS = [
+       "UPDATE:DESCRIPTION",
+       "UPDATE:VALUE",
+       "UPDATE:ROLE",
+       "UPDATE:PARENT:REMOVE",
+       "UPDATE:PARENT:ADD",
+       "UPDATE:PROPERTY:REMOVE",
+       "UPDATE:PROPERTY:ADD",
+       "UPDATE:NAME",
+       "UPDATE:DATA_TYPE",
+       "UPDATE:FILE:REMOVE",
+       "UPDATE:FILE:ADD",
+       "UPDATE:FILE:MOVE",
+       "UPDATE:QUERY_TEMPLATE_DEFINITION",
+    ];
 
+    /**
+     * Add a button labeled "Edit" to the entity which opens the edit form for
+     * this entity.
+     *
+     * The button is added only when any of the `UPDATE:...` permissions are
+     * there.
+     *
+     * @param {HTMLElement} entity - the entity which gets the button.
+     * @parma {function} callback - the function which initializes and opens
+     *     the edit form.
+     */
     this.add_start_edit_button = function(entity, callback) {
+        var has_any_update_permission = false;
+        for (let permission of UPDATE_PERMISSIONS) {
+            if (hasEntityPermission(entity, permission)) {
+                has_any_update_permission = true;
+                break;
+            }
+        }
+        if (!has_any_update_permission) {
+            return;
+        }
         edit_mode.remove_start_edit_button(entity);
         var button = $('<button title="Edit this ' + getEntityRole(entity) + '." class="btn btn-link caosdb-update-entity-button caosdb-f-entity-start-edit-button">Edit</button>');
 
@@ -1782,6 +1861,9 @@ var edit_mode = new function() {
 
 
     this.add_new_record_button = function(entity, callback) {
+        if (!hasEntityPermission(entity, "USE:AS_PARENT")) {
+          return;
+        }
         edit_mode.remove_new_record_button(entity);
         var button = $('<button title="Create a new Record from this RecordType." class="btn btn-link caosdb-update-entity-button caosdb-f-entity-new-record-button">+Record</button>');
 
@@ -1795,6 +1877,9 @@ var edit_mode = new function() {
     }
 
     this.add_delete_button = function(entity, callback) {
+        if (!hasEntityPermission(entity, "DELETE")) {
+          return;
+        }
         edit_mode.remove_delete_button(entity);
         var button = $('<button title="Delete this ' + getEntityRole(entity) + '." class="btn btn-link caosdb-update-entity-button caosdb-f-entity-delete-button">Delete</button>');
 
diff --git a/src/core/js/ext_bottom_line.js b/src/core/js/ext_bottom_line.js
index e09ee3d635e78be91976437e84b0cfaa824470c6..51ca8ac976314bcbe45d338e11cd2c6321f8afe2 100644
--- a/src/core/js/ext_bottom_line.js
+++ b/src/core/js/ext_bottom_line.js
@@ -68,6 +68,19 @@
  * @requires ext_table_preview (module from ext_table_preview.js)
  */
 
+/**
+  * Helper function analogous to ext_references isOutOfViewport
+  *
+  * Check whether the bottom of an entity is within the viewport.
+  * Returns true when this is the case and false otherwise.
+  *
+  */
+function is_bottom_in_viewport(entity) {
+    var bounding = entity.getBoundingClientRect();
+    return bounding.bottom > 0 && bounding.bottom < (window.innerHeight ||
+                                                     document.documentElement.clientHeight);
+}
+
 var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntityPath, connection, UTIF, ext_table_preview) {
 
     /**
@@ -568,7 +581,7 @@ var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntit
         BottomLineWarning: BottomLineWarning,
     }
 }($, log.getLogger("ext_bottom_line"),
-  resolve_references.is_in_viewport_vertically, load_config, getEntityPath,
+  is_bottom_in_viewport, load_config, getEntityPath,
   connection, UTIF, ext_table_preview);
 
 
diff --git a/src/core/js/ext_jupyterdrag.js b/src/core/js/ext_jupyterdrag.js
index 87d2f3a964ced1411df66da04ff4ddd4d7db2f15..0d055170076e23dfecc3d282ffeee03886b1d15c 100644
--- a/src/core/js/ext_jupyterdrag.js
+++ b/src/core/js/ext_jupyterdrag.js
@@ -72,5 +72,7 @@ var ext_jupyterdrag = function($, logger, getEntityRole, getEntityID) {
 
 
 $(document).ready(function() {
-    caosdb_modules.register(ext_jupyterdrag);
+    if ("${BUILD_MODULE_EXT_JUPYTERDRAG}" == "ENABLED") {
+        caosdb_modules.register(ext_jupyterdrag);
+    }
 });
diff --git a/src/core/js/ext_revisions.js b/src/core/js/ext_revisions.js
deleted file mode 100644
index 3ee086e60ed34ac659c5bb92de71d78b38f30e2b..0000000000000000000000000000000000000000
--- a/src/core/js/ext_revisions.js
+++ /dev/null
@@ -1,229 +0,0 @@
-/*
- * ** 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';
-
-/**
- * The ext_revisions module extends the edit_mode update functionality.
- *
- * The edit_mode.update_entity function is overridden by this module with a
- * proxy pattern. That means, that the original function is still called, but a
- * proxy (or wrapper) function adds further functionality.
- *
- * The extended update function creates a back-up version of the updated entity
- * and adds a revisionOf property to the updated entity which references
- * the back-up. The back-up entity loses all of its original parents and gets
- * an "Obsolete" as only parent instead.
- *
- * Per default, the module assumes two Entities to be present in the
- * database. A RecordType named "Obsolete" and a Property named
- * "revisionOf". The initialization is aborted if these entities cannot be
- * found and the module remains inactive.
- *
- * @module ext_revisions
- * @version 0.1
- *
- * @requires jQuery
- * @requires log
- * @requires edit_mode
- * @requires getEntityID
- * @requires transaction
- * @requires _createDocument
- */
-var ext_revisions = function ($, logger, edit_mode, getEntityID, transaction, _createDocument) {
-
-
-    /**
-     * Default names for the two entities which are required by this module.
-     */
-    var _datamodel = { obsolete: "Obsolete", revisionOf: "revisionOf" };
-
-    /**
-     * Generate and insert the back-up entity which stores the old state of the
-     * entity which is to be updated.
-     *
-     * The obsolete entity has only one parent named "Obsolete".
-     * Apart from that, the obsolete entity has all the properties, name,
-     * description and so on from the original entity before the update.
-     *
-     * @param {string} id - the id of the entity which is to be updated.
-     * @returns {string} the id of the newly created obsolete entity.
-     */
-    var _insert_obsolete = async function (id) {
-        logger.debug("insert obsolete", id);
-
-        // create new obsolete entity from the original
-        const obsolete = await transaction.retrieveEntityById(id);
-        $(obsolete).attr("id", "-1");
-        $(obsolete).find("Permissions").remove();
-        $(obsolete).find("Parent").remove();
-        $(obsolete).append(`<Parent name="${_datamodel.obsolete}"/>`);
-
-        const doc = _createDocument("Request");
-        doc.firstElementChild.appendChild(obsolete);
-        const result = await transaction.insertEntitiesXml(doc);
-        const obsolete_id = $(result.firstElementChild).find("[id]").first().attr("id");
-        logger.trace("leave _insert_obsolete", obsolete_id);
-        return obsolete_id;
-    };
-
-    /**
-     * Generate a HTML string which represents a new "revisionOf" property
-     * which references the newly created obsolete entity. The property is
-     * meant to be appended to the property section of the entity which is to
-     * be updated.
-     *
-     * @param {string} obsolete_id - the id of the newly created obsolete
-     *     entity.
-     * @returns {string} A HTML represesentation of an entity property.
-     */
-    var _make_revision_of_property = async function (obsolete_id) {
-        logger.trace("enter _make_revision_of_property", obsolete_id);
-        const ret = (await transformation.transformProperty(str2xml(`<Response><Property id="${_datamodel._revisionOfId}" name="${_datamodel.revisionOf}" datatype="${_datamodel.obsolete}"></Property></Response>`))).firstElementChild;
-
-        $(ret).find(".caosdb-f-property-value").append(`<div class="caosdb-property-edit-value"><select><option value="${obsolete_id}" selected="selected"></option></select></div>`);
-
-        logger.trace("leave _make_revision_of_property", ret);
-        return ret;
-    }
-
-    /**
-     * Remove all properties from ent_element with the id of the "revisionOf"
-     * property.
-     *
-     * @param {HTMLElement} ent_element - entity in HTML representation.
-     */
-    var _remove_old_revision_of_property = function (ent_element) {
-        $(ent_element)
-            .find(".caosdb-f-entity-property")
-            .filter(function(index, property) {
-                if(_datamodel._revisionOfId === $(property)
-                    .find(".caosdb-property-id").text()) {
-                    return true;
-                }
-                return false;
-        }).remove();
-    }
-
-    /**
-     * Main functionality of this module is in here.
-     *
-     * This method is called by the (overridden) edit_mode.update_entity
-     * function before the actual update. It inserts a new obsolete entity
-     * which represesents the old state of the entity, deletes any revisionOf
-     * properties of the entity (if present) and adds a new revisionOf property
-     * which references the (newly inserted) obsolete entity.
-     *
-     * @param {HTMLElement} ent_element - The entity form which has been
-     * generated by the edit_mode with the changes by the user.
-     */
-    var _create_revision = async function (ent_element) {
-        logger.debug("create revision", ent_element);
-        var id = getEntityID(ent_element);
-
-        var obsolete_id = await _insert_obsolete(id);
-
-        // remove old revision of and add new one
-        _remove_old_revision_of_property(ent_element);
-        var revision_of_property = await _make_revision_of_property(obsolete_id);
-        var properties_section = ent_element.getElementsByClassName("caosdb-properties")[0];
-        properties_section.appendChild(revision_of_property);
-    };
-
-    /**
-     * Test whether the necessary entities exist ("revisionOf" and "Obsolete").
-     */
-    var _check_datamodel = async function() {
-        var results = Promise.all([
-            query(`FIND RecordType ${_datamodel.obsolete}`),
-            query(`FIND Property ${_datamodel.revisionOf}`)
-        ]);
-
-        for (let result of (await results)) {
-            if (result.length !== 1) {
-                throw new Error("Invalid datamodel");
-            }
-
-            var name = getEntityName(result[0]);
-            if (name && name.toLowerCase() === _datamodel.revisionOf.toLowerCase()) {
-                _datamodel._revisionOfId = getEntityID(result[0]);
-                _datamodel.revisionOf = name;
-            } else if (name && name.toLowerCase() === _datamodel.obsolete.toLowerCase()) {
-                _datamodel.obsolete = name;
-            }
-        }
-    };
-
-    /**
-     * Initialize the ext_revisions module.
-     *
-     * Per default, the module assumes two Entities to be present in the
-     * database. A RecordType named "Obsolete" and a Property named
-     * "revisionOf". The initialization is aborted if these entities cannot be
-     * found and the module remains inactive. For testing purposes the names of
-     * these entities can be set to different values via the respective
-     * parameters.
-     *
-     * @param {string} [obsolete] - The name of the obsolete RecordType.
-     * @param {string} [revisionOf] - The name of the revisionOf Property.
-     */
-    var init = async function (obsolete, revisionOf) {
-        if (typeof obsolete === "string") {
-            _datamodel.obsolete = obsolete;
-        }
-        if (typeof revisionOf === "string") {
-            _datamodel.revisionOf = revisionOf;
-        }
-
-        try {
-            await _check_datamodel();
-        } catch (err) {
-            logger.error("could not init ext_revisions", err);
-            return;
-        }
-
-        (function(proxied) {
-            edit_mode.update_entity = async function(ent_element) {
-                await _create_revision(ent_element);
-                return await proxied.apply(this, arguments)
-            };
-        })(edit_mode.update_entity);
-    }
-
-    return {
-        // public members, part of the API
-        init: init,
-        // private members, exposed for testing
-        _make_revision_of_property: _make_revision_of_property,
-        _datamodel: _datamodel,
-        _logger: logger,
-    }
-}($, log.getLogger("ext_revisions"), edit_mode, getEntityID, transaction, _createDocument);
-
-
-// this will be replaced by require.js in the future.
-$(document).ready(function () {
-    if ("${BUILD_MODULE_EXT_REVISIONS}" == "ENABLED") {
-        caosdb_modules.register(ext_revisions);
-    }
-});
diff --git a/src/core/js/ext_xls_download.js b/src/core/js/ext_xls_download.js
index d80bf4efa539f483811ca1d3b7f85b817489a572..f3eac54dde17bb7d385c31e5189806facc1948e2 100644
--- a/src/core/js/ext_xls_download.js
+++ b/src/core/js/ext_xls_download.js
@@ -71,7 +71,7 @@ var caosdb_table_export = new function () {
      * @return {string} cleaned up content
      */
     this._clean_cell = function(raw) {
-        return raw.replaceAll("\t"," ").replaceAll("\n"," ").replaceAll("\r"," ").replaceAll("\x1E"," ").replaceAll("\x15"," ")
+        return raw.replace(/\t/g," ").replace(/\n/g," ").replace(/\r/g," ").replace(/\x1E/g," ").replace(/\x15/g," ")
     }
 
     /**
diff --git a/src/core/js/form_elements.js b/src/core/js/form_elements.js
index d4cd4234a28953140fcc1f62104e2c43a3460cdb..8fe0af4ba633aaf257c48dd3e40ee22f4a8fc3c5 100644
--- a/src/core/js/form_elements.js
+++ b/src/core/js/form_elements.js
@@ -84,14 +84,17 @@ var form_elements = new function () {
     /**
      * The configuration for double, integer, date input elements.
      *
-     * There are specializations of this configuration object. See
-     * {@link ReferenceDropDownConfig}
+     * There are several specializations of this configuration object.
+     * {@link ReferenceDropDownConfig}, {@link RangeFieldConfig}, {@link SelectFieldConfig}, {@link FileFieldConfig}
      *
      * @typedef {object} FieldConfig
+     *
      * @property {string} name
      * @property {string} type
-     * @property {string} label
-     * @see {@link ReferenceDropDownConfig}
+     * @property {string} [label]
+     * @property {string} [help]
+     * @property {boolean} [required=false]
+     * @property {boolean} [cached=false]
      */
 
     this.version = "0.1";
@@ -99,7 +102,6 @@ var form_elements = new function () {
     this.logger = log.getLogger("form_elements");
     this.cancel_form_event = new Event("caosdb.form.cancel");
     this.submit_form_event = new Event("caosdb.form.submit");
-    this.form_ready_event = new Event("caosdb.form.ready");
     this.field_changed_event = new Event("caosdb.field.changed");
     this.field_enabled_event = new Event("caosdb.field.enabled");
     this.field_disabled_event = new Event("caosdb.field.disabled");
@@ -208,7 +210,7 @@ var form_elements = new 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`");
+            "config param `proceed_callback`");
 
         // define some defaults.
         const title = config.title ? `<h4>${config.title}</h4>` : "";
@@ -219,12 +221,14 @@ var form_elements = new function () {
 
         // 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;
-          }
+            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;
+            }
         }
 
         // create the alert
@@ -236,8 +240,8 @@ var form_elements = new function () {
         // 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.";
+            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);
@@ -293,12 +297,23 @@ var form_elements = new function () {
         if (typeof desc == "undefined") {
             desc = entity_id;
         }
-        var opt_str = '<option value="' + entity_id + '">' + desc +
+        return form_elements._make_option(entity_id, desc);
+    }
+
+    /**
+     * Return an `option` element for a `select`.
+     *
+     * @param {string} value - the actual value of the option element.
+     * @param {string} label - the string which is shown for this option in the
+     *     drop-down menu of the select input.
+     * @return {HTMLElement}
+     */
+    this._make_option = function (value, label) {
+        const opt_str = '<option value="' + value + '">' + label +
             "</option>";
         return $(opt_str)[0];
     }
 
-
     /**
      * (Re-)set this module's functions to standard implementation.
      */
@@ -320,10 +335,11 @@ var form_elements = new function () {
          *      parameter which is an entity in HTML representation.
          * @param {boolean} [multiple] - whether the select allows multiple
          *      options to be selected.
+         * @param {string} name - the name of the select element 
          * @returns {HTMLElement} SELECT element with entity options.
          */
         this.make_reference_select = async function (entities, make_desc,
-                make_value, multiple) {
+            make_value, name, multiple) {
             caosdb_utils.assert_array(entities, "param `entities`", false);
             if (typeof make_desc !== "undefined") {
                 caosdb_utils.assert_type(make_desc, "function",
@@ -333,12 +349,7 @@ var form_elements = new function () {
                 caosdb_utils.assert_type(make_value, "function",
                     "param `make_value`");
             }
-            const ret = $('<select class="selectpicker form-control" title="Nothing selected"/>');
-            if (multiple) {
-                ret.attr("multiple", "");
-            } else {
-                ret.append('<option style="display: none" selected="selected" value="" disabled="disabled"></option>');
-            }
+            const ret = $(form_elements._make_select(multiple, name));
             for (let entity of entities) {
                 this.logger.trace("add option", entity);
                 let entity_id = getEntityID(entity);
@@ -351,6 +362,30 @@ var form_elements = new function () {
             return ret[0];
         }
 
+        /**
+         * Return a new select element.
+         *
+         * This function is mainly used by other factory functions, e.g. {@link
+         * make_reference_select} and {@link make_select_input}.
+         *
+         * @param {boolean} multiple - the `multiple` attribute of the select element.
+         * @param {string} name - the name of the select element.
+         * @return {HTMLElement}
+         */
+        this._make_select = function (multiple, name) {
+            const ret = $(`<select class="selectpicker form-control" name="${name}" title="Nothing selected"/>`);
+            if (typeof name !== "undefined") {
+                caosdb_utils.assert_string(name, "param `name`");
+                ret.attr("name", name);
+            }
+            if (multiple) {
+                ret.attr("multiple", "");
+            } else {
+                ret.append('<option style="display: none" selected="selected" value="" disabled="disabled"></option>');
+            }
+            return ret[0];
+        }
+
         /**
          * Configuration object for a drop down menu for selecting references.
          * `make_reference_drop_down` generates such a drop down menu using a
@@ -369,10 +404,9 @@ 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
+         *
+         * @augments FieldConfig
          * @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
@@ -386,8 +420,6 @@ var form_elements = new function () {
          * @property {string} [type] - This should be "reference_drop_down" or
          *     undefined. This property is used by `make_form_field` to decide
          *     which type of field is to be generated.
-         *
-         * @see {@link FieldConfig}
          */
 
         this._query = async function (q) {
@@ -396,8 +428,23 @@ var form_elements = new function () {
             return result;
         }
 
+        /**
+         * Call a server-side script with the content of the given form and
+         * return the results.
+         *
+         * Note that the form should be one generated by this form_elements
+         * module. Otherwise it cannot be guaranteed that the form will be
+         * serialized (to json) correctly.
+         *
+         * @param {string} script - the path of the script
+         * @param {HTMLElements} form - a form generated by this module.
+         * @return {ScriptingResult} the results of the call.
+         */
         this._run_script = async function (script, form) {
-            const json_str = JSON.stringify(form_elements.form_to_object(form[0]));
+            const form_objects = form_elements.form_to_object(form[0]);
+            const json_str = JSON.stringify(form_objects[0]);
+
+            // append non-file form fields to the request
             const params = {
                 "-p0": {
                     "filename": "form.json",
@@ -406,6 +453,16 @@ var form_elements = new function () {
                     })
                 }
             };
+
+            // append files to the request
+            const files = form_objects[1];
+            for (let i = 0; i < files.length; i++) {
+                params[`file_${i}`] = {
+                    "filename": `${files[i]["fieldname"]}_${files[i]["filename"]}`,
+                    "blob": files[i]["blob"]
+                };
+            }
+
             const result = await connection.runScript(script, params);
             this.logger.debug("server-side script returned", result);
             return this.parse_script_result(result);
@@ -422,7 +479,8 @@ var form_elements = new function () {
      */
 
     /**
-     * Bla, TODO
+     * Convert the reponse of a server-side scripting call into a {@link
+     * ScriptingResult} object.
      *
      * @param {XMLDocument} result
      * @return {ScriptingResult}
@@ -456,15 +514,15 @@ var form_elements = new function () {
     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 loading = $(createWaitingNotification("loading..."))
+            .addClass("caosdb-f-field-not-ready");
         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,
+                entities, config.make_desc, config.make_value, config.name, config.multiple,
                 config.value));
-            select.attr("name", config.name);
             loading.remove();
             input_col.append(select);
             form_elements.init_select_picker(ret[0], config.value);
@@ -574,16 +632,27 @@ var form_elements = new function () {
     }
 
     /**
-     * generate a java script object representation of a form
+     * Generate a java script object representation of a form and extract the
+     * files from the form.
+     *
+     * The property names (aka keys) are the names of the form fields and
+     * subforms. The values are single strings or arrays of strings. If the
+     * field was had a file-type input, the value is a string identifying the
+     * file blob which belongs to this key.
      *
-     * @function
+     * Subforms lead to nested objects of the same structure.
+     *
+     * @param {HTMLElement} form - a form generated by this module.
+     * @return {object[]} - an array of length 2. The first element is an
+     *     object representing the fields of the form. The second contains a
+     *     list of file blobs resulting from file inputs in the 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);
+        const _to_json = (element, data, files) => {
+            this.logger.trace("enter element_to_json", element, data, files);
 
             for (const child of element.children) {
                 // ignore disabled fields and subforms
@@ -595,7 +664,7 @@ var form_elements = new function () {
                 if (is_subform) {
                     const subform = $(child).data("subform-name");
                     // recursive
-                    var subform_obj = _to_json(child, {});
+                    var subform_obj = _to_json(child, {}, files)[0];
                     if (typeof data[subform] === "undefined") {
                         data[subform] = subform_obj;
                     } else if (Array.isArray(data[subform])) {
@@ -606,7 +675,30 @@ var form_elements = new function () {
                 } else if (name && name !== "") {
                     // input elements
                     const not_checkbox = !$(child).is(":checkbox");
-                    if (not_checkbox || $(child).is(":checked")) {
+                    const is_file = $(child).is("input:file");
+                    if (is_file) {
+                        var fileList = child.files;
+                        if (fileList.length > 0) {
+                            for (let i = 0; i < fileList.length; i++) {
+                                // generate an identifyer for the file(s) of this input
+                                value = name + "_" + fileList[i].name;
+                                if (typeof data[name] === "undefined") {
+                                    // first and possibly only value
+                                    data[name] = value
+                                } else if (Array.isArray(data[name])) {
+                                    data[name].push(value);
+                                } else {
+                                    // there is a value present yet - convert to array.
+                                    data[name] = [data[name], value]
+                                }
+                                files.push({
+                                    "fieldname": name,
+                                    "filename": fileList[i].name,
+                                    "blob": fileList[i]
+                                });
+                            }
+                        }
+                    } else if (not_checkbox || $(child).is(":checked")) {
                         // checked or not a checkbox
                         var value = $(child).val();
                         if (typeof data[name] === "undefined") {
@@ -621,15 +713,15 @@ var form_elements = new function () {
                     }
                 } else if (child.children.length > 0) {
                     // recursive
-                    _to_json(child, data);
+                    _to_json(child, data, files);
                 }
             }
 
-            this.logger.trace("leave element_to_json", element, data);
-            return data;
+            this.logger.trace("leave element_to_json", element, data, files);
+            return [data, files];
         };
 
-        const ret = _to_json(form, {});
+        const ret = _to_json(form, {}, []);
         this.logger.trace("leave form_to_json", ret);
         return ret;
     }
@@ -649,11 +741,19 @@ var form_elements = new function () {
     }
 
     /**
-     * TODO make syncronous
+     * Return a new form field (or a subform).
+     *
+     * This function is intended to be called by make_form and recursively by
+     * other make_* functions which create subforms or other deeper structured
+     * form fields.
+     *
+     * This function also configures the caching, whether a form field is
+     * 'required' or not, and the help for each field.
      *
+     * @param {FieldConfig} config - the configuration of the form field
      * @return {HTMLElement}
      */
-    this.make_form_field = async function (config) {
+    this.make_form_field = function (config) {
         caosdb_utils.assert_type(config, "object", "param `config`");
         caosdb_utils.assert_string(config.type, "`config.type` of param `config`");
 
@@ -661,6 +761,8 @@ var form_elements = new function () {
         const type = config.type;
         if (type === "date") {
             field = this.make_date_input(config);
+        } else if (type === "file") {
+            field = this.make_file_input(config);
         } else if (type === "checkbox") {
             field = this.make_checkbox_input(config);
         } else if (type === "text") {
@@ -670,12 +772,13 @@ var form_elements = new function () {
         } else if (type === "integer") {
             field = this.make_integer_input(config);
         } else if (type === "range") {
-            field = await this.make_range_input(config);
+            field = this.make_range_input(config);
         } else if (type === "reference_drop_down") {
             field = this.make_reference_drop_down(config);
+        } else if (type === "select") {
+            field = this.make_select_input(config);
         } else if (type === "subform") {
-            // TODO handle cache and required for subforms
-            return await this.make_subform(config);
+            return this.make_subform(config);
         } else {
             throw new TypeError("undefined field type `" + type + "`");
         }
@@ -732,31 +835,18 @@ var form_elements = new function () {
     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 loading = $('<div>loading...</div>');
-        var logger = this.logger;
         var cancel = (e) => {
-            logger.trace("cancel form", e);
+            form_elements.logger.trace("cancel form", e);
             wrapper.remove();
         };
 
-        wrapper.append(loading);
-
-        Promise.resolve(form).then(form => {
-            // form ready
-            loading.remove();
-            wrapper.append(form);
-            wrapper[0].dispatchEvent(this.form_ready_event);
+        wrapper[0].addEventListener(this.cancel_form_event.type, cancel, true);
 
-        }).catch(err => {
-            logger.error("form loading error", err);
-            loading.remove();
-            wrapper.append(err);
-        });
 
-        wrapper[0].addEventListener(this.cancel_form_event.type, cancel, true);
+        var header = this.make_heading(config);
+        wrapper.append(header);
+        wrapper.append(form);
 
         return wrapper[0];
     }
@@ -782,9 +872,9 @@ var form_elements = new function () {
     /**
      * 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.
+     * The returned element is a container which contains a HTML form element.
+     * The fields are ready or they will emit a {@link field_ready_event} when
+     * they are.
      *
      * @param {FormConfig} config
      * @return {HTMLElement}
@@ -802,9 +892,20 @@ var form_elements = new function () {
     }
 
     /**
-     * TODO make syncronous
+     * @typedef {object} SubFormConfig
+     *
+     * @augments FieldConfig
+     * @property {FieldConfig[]} fields - array of fields. The order is the
+     *     order in which they appear in the resulting subform.
      */
-    this.make_subform = async function (config) {
+
+    /**
+     * Return a new subform.
+     *
+     * @param {SubFormConfig} config - the configuration of the subform.
+     * @return {HTMLElement}
+     */
+    this.make_subform = 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`");
@@ -815,7 +916,7 @@ var form_elements = new function () {
 
         for (let field of config.fields) {
             this.logger.trace("add subform field", field);
-            let elem = await this.make_form_field(field);
+            let elem = this.make_form_field(field);
             form.append(elem);
         }
 
@@ -865,6 +966,7 @@ var form_elements = new function () {
 
     this.disable_fields = function (fields) {
         $(fields).toggleClass("caosdb-f-field-disabled", true).hide();
+        $(fields).find(":input").prop("required", false);
         for (const field of $(fields)) {
             field.dispatchEvent(this.field_disabled_event);
         }
@@ -872,6 +974,7 @@ var form_elements = new function () {
 
     this.enable_fields = function (fields) {
         $(fields).toggleClass("caosdb-f-field-disabled", false).show();
+        $(fields).filter(".caosdb-f-form-field-required").find("input.caosdb-f-property-single-raw-value, select.selectpicker").prop("required", true);
         for (const field of $(fields)) {
             field.dispatchEvent(this.field_enabled_event);
         }
@@ -885,7 +988,7 @@ var form_elements = new function () {
         this.disable_fields($(form).find(".caosdb-f-field[data-field-name='" + name + "']").toArray());
     }
 
-    this.make_script_form = async function (config, script) {
+    this.make_script_form = function (config, script) {
         this.logger.trace("enter make_script_form");
 
         const submit_callback = async function (form) {
@@ -914,7 +1017,7 @@ var form_elements = new function () {
             name: script,
             submit: submit_callback
         }, config);
-        return await this.make_generic_form(new_config);
+        return this.make_generic_form(new_config);
     }
 
     /**
@@ -924,9 +1027,9 @@ var form_elements = new function () {
      * The `config.fields` array may contain `form_elements.field_config`
      * objects or HTMLElements.
      *
-     * TODO
+     * @return {HTMLElement}
      */
-    this.make_generic_form = async function (config) {
+    this.make_generic_form = function (config) {
         this.logger.trace("enter make_generic_form");
 
         caosdb_utils.assert_type(config, "object", "param `config`");
@@ -946,7 +1049,7 @@ var form_elements = new function () {
             if (field instanceof HTMLElement) {
                 form.append(field);
             } else {
-                let elem = await this.make_form_field(field);
+                let elem = this.make_form_field(field);
                 form.append(elem);
             }
         }
@@ -1125,11 +1228,24 @@ var form_elements = new function () {
     }
 
     /**
-     * TODO make syncronous
+     * @typedef {object} RangeFieldConfig
+     *
+     * @augments FieldConfig
+     * @property {FieldConfig} from - the start point of the range. This is
+     *     usually an integer or double input field.
+     * @property {FieldConfig] to -  the end point of the range. This is
+     *     usually an integer or a double input field.
      */
-    this.make_range_input = async function (config) {
 
-        // TODO 
+    /**
+     * Return a new form field representing a range of numbers.
+     *
+     * @param {RangeFieldConfig} config
+     * @return {HTMLElement}
+     */
+    this.make_range_input = 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
@@ -1144,8 +1260,8 @@ var form_elements = new function () {
             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 from_input = this.make_form_field(from_config);
+        const to_input = this.make_form_field(to_config);
 
         const ret = $(this._make_field_wrapper(config.name));
         if (config.label) {
@@ -1178,13 +1294,27 @@ var form_elements = new function () {
     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];
+            .css({
+                "padding": "0"
+            })[0];
     }
 
+    /**
+     * Return a new date field.
+     *
+     * @param {FieldConfig} config
+     * @return {HTMLElement}
+     */
     this.make_date_input = function (config) {
         return this._make_input(config);
     }
 
+    /**
+     * Return a new text field.
+     *
+     * @param {FieldConfig} config
+     * @return {HTMLElement}
+     */
     this.make_text_input = function (config) {
         return this._make_input(config);
     }
@@ -1195,14 +1325,20 @@ var form_elements = new function () {
      *
      * `config.type` is set to "number" and overrides any other type.
      *
-     * @param {form_elements.input_config} config.
+     * @param {FieldConfig} config.
      * @returns {HTMLElement} a double form field.
      */
     this.make_double_input = function (config) {
-        var clone = $.extend({}, config, {
+        const _config = $.extend({}, config, {
             type: "number"
         });
-        var ret = $(this._make_input(clone))
+        const ret = $(this._make_input(_config))
+        if (typeof config.min !== "undefined") {
+            ret.find("input").attr("min", config.min);
+        }
+        if (typeof config.max !== "undefined") {
+            ret.find("input").attr("max", config.max);
+        }
         ret.find("input").attr("step", "any");
         return ret[0];
     }
@@ -1213,7 +1349,7 @@ var form_elements = new function () {
      *
      * `config.type` is set to "number" and overrides any other type.
      *
-     * @param {form_elements.input_config} config.
+     * @param {FieldConfig} config.
      * @returns {HTMLElement} an integer form field.
      */
     this.make_integer_input = function (config) {
@@ -1222,6 +1358,79 @@ var form_elements = new function () {
         return ret[0];
     }
 
+    /**
+     * @typedef {object} FileFieldConfig
+     *
+     * @augments FieldConfig
+     * @property {boolean} [multiple=false] - whether to accept multiple files.
+     * @property {string} [accept] - a comma separated list of file extensions
+     *     which are accepted (exclusively).
+     */
+
+    /**
+     * Return a new form field for a file upload.
+     *
+     * @param {FileFieldConfig} config - configuration for this form field.
+     * @return {HTMLElement}
+     */
+    this.make_file_input = function (config) {
+        const ret = this._make_input(config);
+        $(ret)
+            .find(":input")
+            .prop("multiple", !!config.multiple)
+            .css({
+                "display": "block"
+            });
+        if (config.accept) {
+            $(ret)
+                .find(":input")
+                .attr("accept", config.accept);
+        }
+
+        return ret;
+    }
+
+    /**
+     * @typedef {object} SelectOptionConfig
+     *
+     * @property {string} value - the value of the select option.
+     * @property {string} [label] - a visible representation (think:
+     *     description) of the value of the select option. defaults to the
+     *     value itself.
+     */
+
+    /**
+     * @typedef {object} SelectFieldConfig
+     *
+     * @augments {FieldConfig}
+     * @property {SelectOptionConfig} - options
+     */
+
+    /**
+     * Return a select field.
+     *
+     * @param {SelectFieldConfig} config
+     * @returns {HTMLElement} a select field.
+     */
+    this.make_select_input = function (config) {
+        const options = config.options;
+        const select = $(form_elements._make_select(config.multiple, config.name));
+
+        for (let option of options) {
+            select.append(form_elements._make_option(option.value, option.label));
+        }
+        const ret = form_elements._make_input(config, select[0]);
+        // Here, the bootstrap-select features should be activated for the new
+        // select element. However, up until now, this only works when the
+        // select element is already a part of the dom tree - which is not the
+        // case when this method is called and is controlled by the client. So
+        // there is currently no other work-around than to call
+        // init_select_picker after the form creation explicitely :(
+        //form_elements.init_select_picker(select[0], config.value);
+
+        return ret;
+    }
+
 
     /**
      * Return a checkbox input field.
@@ -1340,25 +1549,29 @@ var form_elements = new function () {
      *
      * @param {object} config - config object with `name`, `type` and
      *      optional `label`
+     * @param {string} input - optional specification of the HTML input element.
+     *      `<input class="form-control caosdb-f-property-single-raw-value" type="' + type + '" name="' + name + '" />`
+     *      is used as default where `name` and `type` stem from the config 
+     *      object.
      * @returns {HTMLElement} a form field.
      */
-    this._make_input = function (config) {
+    this._make_input = function (config, input) {
         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 () {
+        const _input = $(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);
+        input_col.append(_input);
         if (value) {
-            input.val(value);
+            _input.val(value);
         }
         return ret.append(label, input_col)[0];
     }
diff --git a/src/core/js/webcaosdb.js b/src/core/js/webcaosdb.js
index 55337a36a97d6a7ad42aadfe673e429d6d942a0a..d6ae7825107fb0d39e1b46e925984c3b5da299eb 100644
--- a/src/core/js/webcaosdb.js
+++ b/src/core/js/webcaosdb.js
@@ -615,7 +615,7 @@ this.transformation = new function () {
      * @return {XMLDocument} xslt script
      */
     this.retrieveEntityXsl = async function _rEX(root_template) {
-        const _root = root_template || '<xsl:template match="/"><div class="root"><xsl:apply-templates select="Response/*" mode="entities"/></div></xsl:template>';
+        const _root = root_template || '<xsl:template match="/" xmlns="http://www.w3.org/1999/xhtml"><div class="root"><xsl:apply-templates select="Response/*" mode="entities"/></div></xsl:template>';
         var entityXsl = await transformation.retrieveXsltScript("entity.xsl");
         var commonXsl = await transformation.retrieveXsltScript("common.xsl");
         var errorXsl = await transformation.retrieveXsltScript('messages.xsl');
@@ -1661,16 +1661,25 @@ function xslt(xml, xsl, params) {
             }
         }
     }
-    if (typeof xsltProcessor.transformDocument == 'function') {
-        // old FFs
-        var retDoc = document.implementation.createDocument("", "", null);
-        xsltProcessor.transformDocument(xml, xsl, retDoc, null);
-        return retDoc.documentElement;
-    } else {
-        // modern browsers
-        xsltProcessor.importStylesheet(xsl);
-        return xsltProcessor.transformToFragment(xml, document);
+    var result = null;
+    try {
+        if (typeof xsltProcessor.transformDocument == 'function') {
+            // old FFs
+            var retDoc = document.implementation.createDocument("", "", null);
+            xsltProcessor.transformDocument(xml, xsl, retDoc, null);
+            result = retDoc.documentElement;
+        } else {
+            // modern browsers
+            xsltProcessor.importStylesheet(xsl);
+            result = xsltProcessor.transformToFragment(xml, document);
+        }
+    } catch (error) {
+        throw new Error(`XSL Transformation terminated with error: ${error.message}`);
     }
+    if (!result) {
+        throw new Error("XSL Transformation did not return any results");
+    }
+    return result;
 }
 
 /**
@@ -1681,13 +1690,18 @@ function getXSLScriptClone(source) {
 }
 
 /**
- * TODO
+ * Add a template rule to a XSL style sheet.
+ *
+ * The original document is cloned (copy-on-change) before the template rule is
+ * appended.
+ *
+ * @param {XMLDocument} orig_xsl - the original xsl style sheet
+ * @param {string} templateStr - the new template rule (an xml string)
+ * @return {XMLDocument} new xsl style sheet with one more rule.
  */
-function injectTemplate(orig_xsl, template) {
+function injectTemplate(orig_xsl, templateStr) {
     var xsl = getXSLScriptClone(orig_xsl);
-    var entry_t = xsl.createElement("xsl:template");
-    xsl.firstElementChild.appendChild(entry_t);
-    entry_t.outerHTML = template;
+    xsl.documentElement.insertAdjacentHTML("beforeend", templateStr);
     return xsl;
 }
 
diff --git a/src/core/xsl/entity.xsl b/src/core/xsl/entity.xsl
index cac9e87ea40bf6a687a19a1942b5534276c8faf7..e90b841e82eac361a397c53975e9b25cf0e5daf9 100644
--- a/src/core/xsl/entity.xsl
+++ b/src/core/xsl/entity.xsl
@@ -268,10 +268,9 @@
         </h5>
       </div>
       <!-- property value -->
-      <div class="col-sm-6 caosdb-f-property-value">
+      <div class="col-sm-8 caosdb-f-property-value">
         <xsl:apply-templates mode="property-value" select="."/>
       </div>
-      <div class="col-sm-2 caosdb-property-edit" style="text-align: right;"></div>
     </div>
   </xsl:template>
   <xsl:template name="single-value">
@@ -576,18 +575,18 @@
     <div class="modal-body">
       <table class="table table-hover">
         <thead>
-          <tr><div class="export-data">Entity ID</div><th/>
+          <tr><th><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>
+            <th class="hidden"><div class="export-data">URI</div></th>
           </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="hidden"><div class="export-data"><xsl:value-of select="$entityId"/></div></td>
             <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>
@@ -595,7 +594,7 @@
             </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>
+            <td class="hidden"><div class="export-data"><xsl:value-of select="concat($entitypath, $entityId, '@', @id)"/></div></td>
           </tr>
           <xsl:apply-templates mode="entity-version-modal-predecessor" select="Predecessor">
             <xsl:with-param name="entityId" select="$entityId"/>
@@ -648,7 +647,7 @@
     <!-- 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>
+    <td class="hidden"><div class="export-data"><xsl:value-of select="."/></div></td>
   </xsl:template>
 
   <xsl:template match="@date" mode="entity-version-date">
@@ -657,7 +656,7 @@
     <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>
+    <td class="hidden"><div class="export-data"><xsl:value-of select="."/></div></td>
   </xsl:template>
 
   <xsl:template match="Predecessor|Successor" mode="entity-version-modal-single-history-item">
@@ -665,7 +664,7 @@
     <xsl:param name="entityId"/>
     <xsl:param name="hint"/>
     <tr>
-      <div class="export-data"><xsl:value-of select="$entityId"/></div>
+      <td class="hidden"><div class="export-data"><xsl:value-of select="$entityId"/></div></td>
       <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">
@@ -676,7 +675,7 @@
       </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>
+      <td class="hidden"><div class="export-data"><xsl:value-of select="concat($entitypath, $entityId, '@', @id)"/></div></td>
     </tr>
   </xsl:template>
 
diff --git a/src/core/xsl/main.xsl b/src/core/xsl/main.xsl
index e414f9af4147714c5f6ff8a03b70309a02ec1446..d1c4def54178ea4991a8d0850730f11b297d92ba 100644
--- a/src/core/xsl/main.xsl
+++ b/src/core/xsl/main.xsl
@@ -210,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_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_file_download.js')"/>
@@ -260,11 +265,6 @@
         <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_bottom_line.js')"/>
       </xsl:attribute>
     </xsl:element>
-    <xsl:element name="script">
-      <xsl:attribute name="src">
-        <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_revisions.js')"/>
-      </xsl:attribute>
-    </xsl:element>
     <xsl:element name="script">
       <xsl:attribute name="src">
         <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_sss_markdown.js')"/>
diff --git a/src/core/xsl/messages.xsl b/src/core/xsl/messages.xsl
index 392d6e37e51e7426feafff8726382abb4012376b..1ac2e7b63c16bd389e9a6e0fe0c2f9541be34683 100644
--- a/src/core/xsl/messages.xsl
+++ b/src/core/xsl/messages.xsl
@@ -26,7 +26,7 @@
   <xsl:template match="Error|Warning|Info">
     <xsl:param name="class"/>
     <div>
-      <xsl:attribute name="class">alert
+      <xsl:attribute name="class">alert caosdb-v-server-message
                 <xsl:value-of select="$class"/> alert-dismissable fade in</xsl:attribute>
       <a class="close" data-dismiss="alert" href="#">
         <xsl:value-of select="$close-char"/>
diff --git a/src/doc/administration/comments.rst b/src/doc/administration/comments.rst
new file mode 100644
index 0000000000000000000000000000000000000000..f44561d12c247ae5c1c42d9ac1c912d13ab768e2
--- /dev/null
+++ b/src/doc/administration/comments.rst
@@ -0,0 +1,85 @@
+The comments feature of the caosdb webui
+========================================
+
+WebUI contains a feature that allows users to add comments to existing
+records.
+
+The feature is not enabled by default.
+
+You can manually activate it using the following steps: - Add a new
+RecordType (e.g. using the Edit Mode) called “Annotation” - Add a new
+RecordType called “CommentAnnotation” with parent “Annotation” - Add a
+new TEXT Property called “comment” - Add a new REFERENCE Property called
+“annotationOf”
+
+or using the following XML:
+
+.. code:: xml
+
+   <Property id="-1" name="comment" description="A comment on something." datatype="TEXT">
+   </Property>
+
+   <Property id="-2" name="annotationOf" description="The core property of the [Annotation] denoting which entity the annotation is annotating." datatype="REFERENCE">
+   </Property>
+
+    <RecordType id="-3" name="Annotation" description="Annotations annotate other entities in order to represent information about these entities without changing them. Mostly this will be comments by users on these entities or flags used by third party programs.">
+       <Property id="-2" name="annotationOf" description="The core property of the [Annotation] denoting which entity the annotation is annotating." datatype="REFERENCE" importance="OBLIGATORY">
+       </Property>
+   </RecordType>
+
+   <RecordType name="CommentAnnotation" description="CommentAnnotations represent user comments on other entities. As they are entities themselves they can be 'responded' by just annotating them with another CommentAnnotation.">
+       <Parent id="-3" name="Annotation" description="Annotations annotate other entities in order to represent information about these entities without changing them. Mostly this will be comments by users on these entities or flags used by third party programs." />
+       <Property id="-1" name="comment" description="A comment on something." datatype="TEXT" importance="OBLIGATORY">
+       </Property>
+   </RecordType>
+
+Additionally, on some servers the comment button might be disabled using
+CSS.
+
+E.g. on the demo server you would have to comment out the following
+lines in ``demoserver.css``:
+
+.. code:: css
+
+   .caosdb-new-comment-button {
+       visibility: hidden;
+   }
+
+Using the YAML-Datamodel-Interface
+----------------------------------
+
+It’s even easier to add the model using the yaml interface. Use the
+following yaml file:
+
+.. code:: yaml
+
+
+   Annotation:
+     description: Annotations annotate other entities in order to represent information about these entities without changing them. Mostly this will be comments by users on these entities or flags used by third party programs.
+     obligatory_properties:
+       annotationOf:
+         description: The core property of the [Annotation] denoting which entity the annotation is annotating.
+         datatype: REFERENCE
+
+   CommentAnnotation:
+     description: CommentAnnotations represent user comments on other entities. As they are entities themselves they can be 'responded' by just annotating them with another CommentAnnotation.
+     inherit_from_obligatory:
+       - Annotation
+     obligatory_properties:
+       comment:
+         description: A comment on something.
+         datatype: TEXT
+
+Save this file under “datamodel.yaml”.
+
+Make sure you have installed caosdb-models.
+
+Then sync the model:
+
+.. code:: python
+
+   import caosdb as db
+   from caosmodels.parser import parse_model_from_yaml
+
+   model = parse_model_from_yaml("datamodel.yaml")
+   model.sync_data_model(noquestion=True)
diff --git a/src/doc/administration/index.rst b/src/doc/administration/index.rst
new file mode 100644
index 0000000000000000000000000000000000000000..e04e1dea523f1d9d8aa83231429a9347ecbfb4de
--- /dev/null
+++ b/src/doc/administration/index.rst
@@ -0,0 +1,10 @@
+Administration
+==============
+
+.. toctree::
+   :maxdepth: 2
+   :caption: Contents:
+   :hidden:
+
+   comments
+   static-snapshots
diff --git a/src/doc/administration/static-snapshots.md b/src/doc/administration/static-snapshots.md
new file mode 100644
index 0000000000000000000000000000000000000000..b7f292ad37d4a35f63aed406600190e988bf84a6
--- /dev/null
+++ b/src/doc/administration/static-snapshots.md
@@ -0,0 +1,50 @@
+# Creating Static WebUI Snapshots
+
+It can be helpful to generate static snapshots of WebUI contents, e.g. for reviewing layouts or for presentation purposes. This is possible with a little bit of effort. Excitingly not only the layout can be exported, but also a lot of the javascript functionality can be maintained in the static pages.
+
+**NOTE: This manual page is currently work in progress.**
+
+## Create the static webui folder in the docker container
+
+We need a static version of the caosdb-webui. In principle it can be simply copied from e.g. a running docker container or from the public-directory. As it contains self-referencing (cyclic) symlinks a little bit of care has to be taken.
+
+### Using Docker
+
+Login to the caosdb/linkahead docker container as root:
+```bash
+docker exec -u 0 -ti linkahead /bin/bash
+```
+
+We need to be root (`-u 0`) in order to be able to create a copy of caosdb-webui within the container.
+
+Create the copy using `cp` and the option for following symlinks `-L`:
+
+```bash
+cp -L git/caosdb-server/caosdb-webui/public/ webui-copy
+```
+
+It will warn you that two symlinks (which are cyclic) cannot be created. That's fine, we will create these two symlinks later.
+
+```
+cp: cannot copy cyclic symbolic link 'git/caosdb-server/caosdb-webui/public/1602145811'  <- The number here is a "unique" build number
+cp: cannot copy cyclic symbolic link 'git/caosdb-server/caosdb-webui/public/webinterface'
+```
+
+**Please copy the build number somewhere, or make sure your terminal history does not get wiped.**
+
+Copy webui-copy from the docker container to the location where you want to store the snapshots:
+`docker cp linkahead:/opt/caosdb/webui-copy/ .`
+
+Create the two missing symlinks in webui-copy/public:
+```
+ln -s webui-copy/public webui-copy/public/1602145811
+ln -s webui-copy/public webui-copy/public/webinterface
+```
+
+You can now use the included xslt stylesheet to convert xml files to html using:
+```bash
+xsltproc webui-copy/public/webcaosdb.xsl test.xml > test.html
+```
+
+As the generated html file still contains invalid references to `/webinterface/1602145811`
+you have to replace all occurences of `/webinterface` with webui-copy/public`.
diff --git a/src/doc/extension/module.md b/src/doc/extension/module.md
new file mode 100644
index 0000000000000000000000000000000000000000..d4f9b56db508d7c3be9dc0a52953de5c82bf310d
--- /dev/null
+++ b/src/doc/extension/module.md
@@ -0,0 +1,72 @@
+# How to add a module to CaosDB WebUI
+The CaosDB WebUI is organized in modules which can easily be added and on a module basis enabled or disabled.
+
+There are a few steps necessary to create a new module.
+
+## Create the module file
+Create a new file in `src/core/js` starting with `ext_`. E.g. `ext_flight_preview.js`. This file should define one function that wraps every thing and which is enabled at the bottom of the file:
+
+```js
+/*
+ * ** header with license infoc
+ * ...
+ */
+
+'use strict';
+
+/**
+ * description of the module ...
+ *
+ * @module ext_flight_preview
+ * @version 0.1
+ *
+ * @requires somelibrary
+ * (pass the dependencies as arguments)
+ */
+var ext_flight_preview = function (somelibrary) {
+
+    var init = function (toolbox) {
+        /* initialization of the module */
+    }
+
+    /**
+     * doc string
+     */
+    var some_function = function (arg1, arg2) {
+    }
+
+    /* the main function must return the initialization of the module */
+    return {
+        init: init,
+    };
+//pass the dependencies as arguments here as well
+}(somelibrary);
+
+// this will be replaced by require.js in the future.
+$(document).ready(function() {
+    // use a variable starting with `BUILD_MODULE_` to enable your module
+    // the build variable has to be enabled in the `build.properties.d/` directory.
+    // Otherwise the module will not be activated.
+    if ("${BUILD_MODULE_EXT_BOTTOM_LINE}" === "ENABLED") {
+        caosdb_modules.register(ext_flight_preview);
+    }
+});
+```
+## Update xml
+Add a section to `src/core/xsl/main.xsl` to include your new file.
+
+```xsl
+<xsl:element name="script">       
+   <xsl:attribute name="src">         
+     <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_sss_markdown.js')"/>             
+   </xsl:attribute>     
+</xsl:element>
+```
+
+## Add to index.html in test
+If you have unittests (and you should), you need to add a line in :
+`test/core/index.html`.
+
+## Update the changelog
+
+## Create a merge request
\ No newline at end of file
diff --git a/src/doc/extension/xslt-debugging.md b/src/doc/extension/xslt-debugging.md
new file mode 100644
index 0000000000000000000000000000000000000000..80443dd8656da5645a5315f83f5ce26dfbbebce0
--- /dev/null
+++ b/src/doc/extension/xslt-debugging.md
@@ -0,0 +1,33 @@
+# XSLT Debugging
+
+The CaosDB WebUI uses [XSLT](https://en.wikipedia.org/wiki/XSLT) to transform the servers response into a web page.
+In the webui-repository these XSLT stylesheets can be found in `src/core/` and `src/core/xsl`.
+
+The XSLT stylesheet is typically interpreted on the client side, e.g. in Mozilla Firefox. Error output of the browser regarding XSLT problems are typically hard to debug. For example, Firefox typically does not print detailed information about the location of an exception in the sourcecode.
+
+So what options do we have to debug xslt stylesheets?
+
+* So called "printf-style" debugging
+* Using a different xslt processor
+
+I found this thread on Stack Overflow very helpful:
+https://stackoverflow.com/questions/218522/tools-for-debugging-xslt
+
+# "printf-style" debugging
+
+As mentioned in the Stack Overflow thread referenced above, `<xsl:message>` can be used to output debugging messages during XSLT processing.
+
+# Using different XSLT processors
+
+## xsltproc from libxslt
+
+`xsltproc` is a tool from libxslt that allows transforming XML using XSLT stylesheets on the command line. It is called using:
+```bash
+xsltproc <stylesheet> <xmlfile>
+```
+
+So a possible workflow for debugging an xslt script could be:
+* Save the test response from the server as `test.xml`.
+* Run `make` in repository `caosdb-webui`
+* Go to folder `public` in `caosdb-webui`
+* Run: `xsltproc webcaosdb.xsl test.xml`
diff --git a/src/doc/index.rst b/src/doc/index.rst
index 107c9052fd6cdafecd201eb17118d8e56f3da440..24c394349a045bf276c4252a2fde47feae6f533c 100644
--- a/src/doc/index.rst
+++ b/src/doc/index.rst
@@ -10,6 +10,7 @@ Welcome to the documentation of CaosDB's web UI!
    Getting started <getting_started>
    Tutorials <tutorials/index>
    Concepts <concepts>
+   administration/index.rst
    Extending the UI <extension>
    API <api/index>
 
diff --git a/test/core/index.html b/test/core/index.html
index ea7b63b9e37943f0bdd4e32499c6c1ea9310a618..a725205ea767d38c29931a074a7671a2fbd22bc2 100644
--- a/test/core/index.html
+++ b/test/core/index.html
@@ -69,7 +69,6 @@
   <script src="js/ext_map.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/ext_autocomplete.js"></script>
   <script src="js/ext_sss_markdown.js"></script>
   <script src="js/ext_trigger_crawler_form.js"></script>
@@ -91,7 +90,6 @@
   <script src="js/modules/ext_references.js.js"></script>
   <script src="js/modules/ext_map.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/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>
diff --git a/test/core/js/modules/caosdb.js.js b/test/core/js/modules/caosdb.js.js
index 20dc4b4eee0116fecf57b0a178f622c4385f08b2..8df5e2f9c2b933cd7b678286295961f2c73d7113 100644
--- a/test/core/js/modules/caosdb.js.js
+++ b/test/core/js/modules/caosdb.js.js
@@ -133,18 +133,7 @@ QUnit.test("available", function(assert) {
   * Test whether properties are parsed correctly from the document tree.
   */
 QUnit.test("getProperties", function(assert) {
-    try {
-        ps = getProperties();
-    }
-    catch (e) {
-        assert.equal(e.message, "element is undefined");
-    }
-    try {
-        ps = getProperties(undefined);
-    }
-    catch (e) {
-        assert.equal(e.message, "element is undefined");
-    }
+    assert.throws(getProperties, "undefined element throws");
 
     assert.equal(this.x.length, 4);
 
@@ -332,59 +321,55 @@ QUnit.test("headingAttributes", function(assert) {
   * @author Alexander Schlemmer
   * Test replication of entities.
   */
-QUnit.test("replicationOfEntities", function(assert) {
-    var done = assert.async();
+QUnit.test("replicationOfEntities", async function(assert) {
 
-    var reptest = function(ent, respxml) {
+    var reptest = async function(k, ent, respxml) {
         var oldprops = getProperties(ent);
         var oldpars = getParents(ent);
         var doc = createResponse(
             createEntityXML(getEntityRole(ent), getEntityName(ent), getEntityID(ent),
                             getProperties(ent), getParents(ent)));
-        assert.equal(xml2str(doc), respxml);
+        assert.equal(xml2str(doc).replace(/\s/g, ""), respxml.replace(/\s/g, ""));
 
 
         doc = createResponse(
             createEntityXML(getEntityRole(ent), getEntityName(ent), getEntityID(ent),
                             getProperties(ent), getParents(ent), true));
-        transformation.transformEntities(doc).then (x => {
-            ps = getProperties(x[0]);
-            pars = getParents(x[0]);
-
-            assert.equal(getEntityRole(ent), getEntityRole(x[0]));
-            assert.equal(getEntityName(ent), getEntityName(x[0]));
-            assert.equal(getEntityID(ent), getEntityID(x[0]));
-            assert.equal(ps.length, oldprops.length);
-            for (var i=0; i<ps.length; i++) {
-                assert.equal(ps[i].name, oldprops[i].name);
-                assert.deepEqual(ps[i].value, oldprops[i].value);
-                assert.equal(ps[i].datatype, oldprops[i].datatype);
-                assert.equal(ps[i].list, oldprops[i].list);
-                assert.equal(ps[i].reference, oldprops[i].reference);
-            }
-            assert.equal(pars.length, oldpars.length);
-            for (var i=0; i<pars.length; i++) {
-                assert.equal(pars[i].name, oldpars[i].name);
-                assert.equal(pars[i].id, oldpars[i].id);
-            }
-            funj += 1;
-            // console.log(funj, maxfunccall);
-            if (funj == maxfunccall) {
-                done();
-            }
-        });
+        var k_2 = k;
+        var doc2 = str2xml(xml2str(doc));
+        var x = await transformation.transformEntities(doc);
+        ps = getProperties(x[0]);
+        pars = getParents(x[0]);
+
+        assert.equal(getEntityRole(ent), getEntityRole(x[0]));
+        assert.equal(getEntityName(ent), getEntityName(x[0]));
+        assert.equal(getEntityID(ent), getEntityID(x[0]));
+        assert.equal(ps.length, oldprops.length);
+        for (var i=0; i<ps.length; i++) {
+            assert.equal(ps[i].name, oldprops[i].name);
+            assert.deepEqual(ps[i].value, oldprops[i].value);
+            assert.equal(ps[i].datatype, oldprops[i].datatype);
+            assert.equal(ps[i].list, oldprops[i].list);
+            assert.equal(ps[i].reference, oldprops[i].reference);
+        }
+        assert.equal(pars.length, oldpars.length);
+        for (var i=0; i<pars.length; i++) {
+            assert.equal(pars[i].name, oldpars[i].name);
+            assert.equal(pars[i].id, oldpars[i].id);
+        }
     };
 
     var respxmls = [
         '<Response><Record name="nameofrecord"><Parent name="bla"/><Property name="A">245</Property></Record></Response>',
         '<Response><Record><Parent name="bla"/></Record></Response>',
         '<Response><Record id="17" name="nameofrec"><Parent id="244" name="bla"/><Parent id="217" name="bla2"/><Property name="B">245</Property><Property name="A">245.0</Property><Property name="A">245</Property></Record></Response>',
-        '<Response><Record><Parent name="bla"/><Property name="B">245</Property><Property name="A">245</Property><Property name="A"><Value>245</Value></Property><Property name="A"/><Property name="A"><Value>245</Value><Value>245</Value></Property><Property name="A"><Value>245</Value><Value>247</Value><Value>299</Value></Property></Record></Response>'];
+        `<Response>
+      <Record>
+      <Parent name="bla"/>
+      <Property name="B">245</Property><Property name="A">245</Property><Property name="A"><Value>245</Value></Property><Property name="A"/><Property name="A"><Value>245</Value><Value>245</Value></Property><Property name="A"><Value>245</Value><Value>247</Value><Value>299</Value></Property></Record></Response>`];
 
-    var funj = 0;
-    var maxfunccall = this.x.length;
-    for (var i=0; i<this.x.length; i++) {
-        reptest(this.x[i], respxmls[i]);
+    for (var i=3; i<this.x.length; i++) {
+        var _ = await reptest(i, this.x[i], respxmls[i]);
     }
 });
 
diff --git a/test/core/js/modules/common.xsl.js b/test/core/js/modules/common.xsl.js
index d77135d6d1e4e1f77c669a937425ec8c9934ad28..16f84cafc3bdb0b48a42cc10a1cdc03445f48541 100644
--- a/test/core/js/modules/common.xsl.js
+++ b/test/core/js/modules/common.xsl.js
@@ -53,6 +53,17 @@ QUnit.test("trim", function(assert) {
     assert.equal(trimmed.firstChild.textContent, 'test\n\ttest\n   test', "trimmed");
 });
 
+QUnit.test("remove_leading_ws", function(assert) {
+    var inject = '<xsl:template match="root"><xsl:call-template name="remove_leading_ws"><xsl:with-param name="str" select="text()"/></xsl:call-template></xsl:template>';
+    console.log(inject);
+    var xsl = injectTemplate(this.commonXSL, inject);
+    var xml_str = '<root>  \n \t \n abcd</root>';
+    var xml = str2xml(xml_str);
+    console.log(xml);
+    var trimmed = xslt(xml, xsl);
+    console.log(trimmed);
+    assert.equal(trimmed.firstChild.textContent, 'abcd', "leading white spaces removed");
+});
 
 QUnit.test("reverse", function(assert) {
     var inject = '<xsl:template match="root"><xsl:call-template name="reverse"><xsl:with-param name="str" select="text()"/></xsl:call-template></xsl:template>';
diff --git a/test/core/js/modules/edit_mode.js.js b/test/core/js/modules/edit_mode.js.js
index ae11b04a380f162018de70a53409e34b4e6990c6..8fde3bac9ce33742c5d79ca9e5ca03e2d0e0d094 100644
--- a/test/core/js/modules/edit_mode.js.js
+++ b/test/core/js/modules/edit_mode.js.js
@@ -93,9 +93,9 @@ QUnit.test("add_new_property", function (assert) {
     var done = assert.async(2);
 
     // test case setup
-    var entity = $("<div><div class='caosdb-properties' /></div>")[0];
+    var entity = $(`<div><ul class='caosdb-properties'/><li class="caosdb-f-entity-property"><ol><li>value1</li></ol></li><li class="caosdb-f-edit-mode-property-dropzone"></li></ul>`)[0];
     $(document.body).append(entity);
-    var new_prop = $("<div id='test_new_prop'/>")[0];
+    var new_prop = $("<div class='test_new_prop'/>")[0];
 
     // test bad cases
     assert_throws(assert, () => {
@@ -113,8 +113,8 @@ QUnit.test("add_new_property", function (assert) {
 
 
     // test good cases
-    assert.equal($(entity).find("#test_new_prop").length, 0, "no property");
-    entity.addEventListener("caosdb.edit_mode.property_added", function (e) {
+    assert.equal($(entity).find(".test_new_prop").length, 0, "no property");
+    entity.addEventListener(edit_mode.property_added.type, function (e) {
         assert.ok(e.target === new_prop, "event fired on newprop");
         assert.ok(this === entity, "event detected on entity");
         done();
@@ -124,7 +124,7 @@ QUnit.test("add_new_property", function (assert) {
             "make_property_editable_cb called");
         done();
     });
-    assert.equal($(entity).find("#test_new_prop").length, 1, "one property");
+    assert.equal($(entity).find(".test_new_prop").length, 1, "one property");
     // event has been fired and property has been added.
 
     $(entity).remove();
@@ -255,7 +255,7 @@ QUnit.test("make_datatype_input", function (assert) {
     const no_dt_input = edit_mode.make_datatype_input(undefined);
     no_dt_input.addEventListener("caosdb.field.ready", function (e) {
         var obj = form_elements
-            .form_to_object($(form_wrapper).append(no_dt_input)[0]);
+            .form_to_object($(form_wrapper).append(no_dt_input)[0])[0];
         assert.propEqual(obj, {
             "atomic_datatype": "TEXT",
             "reference_scope": null,
@@ -266,7 +266,7 @@ QUnit.test("make_datatype_input", function (assert) {
     const text_dt_input = edit_mode.make_datatype_input("TEXT");
     text_dt_input.addEventListener("caosdb.field.ready", function (e) {
         var obj = form_elements
-            .form_to_object($(form_wrapper).append(text_dt_input)[0]);
+            .form_to_object($(form_wrapper).append(text_dt_input)[0])[0];
         assert.propEqual(obj, {
             "atomic_datatype": "TEXT",
             "reference_scope": null,
@@ -277,7 +277,7 @@ QUnit.test("make_datatype_input", function (assert) {
     const ref_dt_input = edit_mode.make_datatype_input("REFERENCE");
     ref_dt_input.addEventListener("caosdb.field.ready", function (e) {
         var obj = form_elements
-            .form_to_object($(form_wrapper).append(ref_dt_input)[0]);
+            .form_to_object($(form_wrapper).append(ref_dt_input)[0])[0];
         assert.propEqual(obj, {
             "atomic_datatype": "REFERENCE",
             "reference_scope": null,
@@ -288,7 +288,7 @@ QUnit.test("make_datatype_input", function (assert) {
     const file_dt_input = edit_mode.make_datatype_input("FILE");
     file_dt_input.addEventListener("caosdb.field.ready", function (e) {
         var obj = form_elements
-            .form_to_object($(form_wrapper).append(file_dt_input)[0]);
+            .form_to_object($(form_wrapper).append(file_dt_input)[0])[0];
         assert.propEqual(obj, {
             "atomic_datatype": "FILE",
             "reference_scope": null,
@@ -299,7 +299,7 @@ QUnit.test("make_datatype_input", function (assert) {
     const person_dt_input = edit_mode.make_datatype_input("Person");
     person_dt_input.addEventListener("caosdb.field.ready", function (e) {
         var obj = form_elements
-            .form_to_object($(form_wrapper).append(person_dt_input)[0]);
+            .form_to_object($(form_wrapper).append(person_dt_input)[0])[0];
         assert.propEqual(obj, {
             "atomic_datatype": "REFERENCE",
             "reference_scope": "Person",
@@ -310,7 +310,7 @@ QUnit.test("make_datatype_input", function (assert) {
     const list_text_dt_input = edit_mode.make_datatype_input("LIST<TEXT>");
     list_text_dt_input.addEventListener("caosdb.field.ready", function (e) {
         var obj = form_elements
-            .form_to_object($(form_wrapper).append(list_text_dt_input)[0]);
+            .form_to_object($(form_wrapper).append(list_text_dt_input)[0])[0];
         assert.propEqual(obj, {
             "atomic_datatype": "TEXT",
             "reference_scope": null,
@@ -322,7 +322,7 @@ QUnit.test("make_datatype_input", function (assert) {
     const list_ref_dt_input = edit_mode.make_datatype_input("LIST<REFERENCE>");
     list_ref_dt_input.addEventListener("caosdb.field.ready", function (e) {
         var obj = form_elements
-            .form_to_object($(form_wrapper).append(list_ref_dt_input)[0]);
+            .form_to_object($(form_wrapper).append(list_ref_dt_input)[0])[0];
         assert.propEqual(obj, {
             "atomic_datatype": "REFERENCE",
             "reference_scope": null,
@@ -334,7 +334,7 @@ QUnit.test("make_datatype_input", function (assert) {
     const list_file_dt_input = edit_mode.make_datatype_input("LIST<FILE>");
     list_file_dt_input.addEventListener("caosdb.field.ready", function (e) {
         var obj = form_elements
-            .form_to_object($(form_wrapper).append(list_file_dt_input)[0]);
+            .form_to_object($(form_wrapper).append(list_file_dt_input)[0])[0];
         assert.propEqual(obj, {
             "atomic_datatype": "FILE",
             "reference_scope": null,
@@ -346,7 +346,7 @@ QUnit.test("make_datatype_input", function (assert) {
     const list_per_dt_input = edit_mode.make_datatype_input("LIST<Person>");
     list_per_dt_input.addEventListener("caosdb.field.ready", function (e) {
         var obj = form_elements
-            .form_to_object($(form_wrapper).append(list_per_dt_input)[0]);
+            .form_to_object($(form_wrapper).append(list_per_dt_input)[0])[0];
         assert.propEqual(obj, {
             "atomic_datatype": "REFERENCE",
             "reference_scope": "Person",
@@ -486,9 +486,6 @@ QUnit.test("remove_delete_button", function (assert) {
 
 
 {
-    const sleep = function sleep(ms) {
-        return new Promise(resolve => setTimeout(resolve, ms));
-    }
 
     const datamodel = `
 <div><div class=\"btn-group-vertical\"><button type=\"button\" class=\"btn btn-default caosdb-f-edit-panel-new-button new-property\">Create new Property</button><button type=\"button\" class=\"btn btn-default caosdb-f-edit-panel-new-button new-recordtype\">Create new RecordType</button></div><div title=\"Drag and drop Properties from this panel to the Entities on the left.\" class=\"panel panel-default\"><div class=\"panel-heading\"><h5>Existing Properties</h5></div><div class=\"panel-body\"><div class=\"input-group\" style=\"width: 100%;\"><input class=\"form-control\" placeholder=\"filter...\" title=\"Type a name (full or partial).\" oninput=\"edit_mode.filter('properties');\" id=\"caosdb-f-filter-properties\" type=\"text\" /><span class=\"input-group-btn\"><button class=\"btn btn-default caosdb-f-edit-panel-new-button new-property caosdb-f-hide-on-empty-input\" title=\"Create this Property.\"><span class=\"glyphicon glyphicon-plus\"></span></button></span></div><ul class=\"caosdb-v-edit-list\"><li class=\"caosdb-f-edit-drag list-group-item caosdb-v-edit-drag\" id=\"caosdb-f-edit-p-20\">name</li><li class=\"caosdb-f-edit-drag list-group-item caosdb-v-edit-drag\" id=\"caosdb-f-edit-p-21\">unit</li><li class=\"caosdb-f-edit-drag list-group-item caosdb-v-edit-drag\" id=\"caosdb-f-edit-p-24\">description</li></ul></div></div><div title=\"Drag and drop RecordTypes from this panel to the Entities on the left.\" class=\"panel panel-default\"><div class=\"panel-heading\"><h5>Existing RecordTypes</h5></div><div class=\"panel-body\"><div class=\"input-group\" style=\"width: 100%;\"><input class=\"form-control\" placeholder=\"filter...\" title=\"Type a name (full or partial).\" oninput=\"edit_mode.filter('recordtypes');\" id=\"caosdb-f-filter-recordtypes\" type=\"text\" /><span class=\"input-group-btn\"><button class=\"btn btn-default caosdb-f-edit-panel-new-button new-recordtype caosdb-f-hide-on-empty-input\" title=\"Create this RecordType\"><span class=\"glyphicon glyphicon-plus\"></span></button></span></div><ul class=\"caosdb-v-edit-list\"><li class=\"caosdb-f-edit-drag list-group-item caosdb-v-edit-drag\" id=\"caosdb-f-edit-rt-30992\">Test</li><li class=\"caosdb-f-edit-drag list-group-item caosdb-v-edit-drag\" id=\"caosdb-f-edit-rt-31015\">Test2</li></ul></div></div></div>`;
diff --git a/test/core/js/modules/entity.xsl.js b/test/core/js/modules/entity.xsl.js
index 59a2f8a6f9a03cf4990fe11bfd751d437a3ffc3e..9415a10dc6bdbd51f913e9bfe1ce8f48f187d081 100644
--- a/test/core/js/modules/entity.xsl.js
+++ b/test/core/js/modules/entity.xsl.js
@@ -58,24 +58,9 @@ QUnit.test("Property names are not links anymore", function(assert) {
     assert.equal(html.firstElementChild.getElementsByClassName("caosdb-property-name")[0].outerHTML, '<strong class=\"caosdb-property-name\">pname</strong>', "link there");
 });
 
-// QUnit.test("parent name is bold link", function(assert) {
-//     // make this xsl sheet accessible
-//     let html = applyTemplates(str2xml('<Parent name="TestParent" id="1234" description="DESC"/>'), this.entityXSL, 'entity-body');
-//     assert.ok(html);
-
-//     var name_e = html.firstElementChild.getElementsByClassName("caosdb-parent-name")[0];
-//     assert.ok(name_e, "element is there");
-//     assert.equal(name_e.tagName, "A", "is link");
-//     assert.equal(name_e.getAttribute("href"), "/entitypath/1234", "href location");
-//     assert.equal(window.getComputedStyle(name_e)["font-weight"], "700", "font is bold");
-// });
-
 QUnit.test("TestRecordType data type is recognized as a reference", function(assert) {
-    // inject an entrance rule
-    var xsl = getXSLScriptClone(this.entityXSL);
-    var entry_t = xsl.createElement("xsl:template");
-    xsl.firstElementChild.appendChild(entry_t);
-    entry_t.outerHTML = '<xsl:template match="/"><xsl:apply-templates select="Property" mode="property-value"/></xsl:template>';
+    var tmpl = '<xsl:template match="/"><xsl:apply-templates select="Property" mode="property-value"/></xsl:template>';
+    var xsl = injectTemplate(this.entityXSL, tmpl);
 
     var xml_str = '<Property name="TestProperty" id="1234" description="DESC" type="TestRecordType">5678</Property>';
     var xml = str2xml(xml_str);
@@ -332,14 +317,19 @@ function applyTemplates(xml, xsl, mode, select = "*") {
     return xslt(xml, modXsl);
 }
 
-function callTemplate(xsl, template, params) {
-    let entryRuleStart = '<xsl:template priority="9" match="/"><xsl:call-template name="' + template + '">';
-    let entryRuleEnd = '</xsl:call-template></xsl:template>';
+function callTemplate(xsl, template, params, wrap_call) {
+    let entryRuleStart = '<xsl:call-template name="' + template + '">';
+    let entryRuleEnd = '</xsl:call-template>';
     var entryRule = entryRuleStart;
     for (name in params) {
         entryRule += '<xsl:with-param name="' + name + '"><xsl:value-of select="\'' + params[name] + '\'"/></xsl:with-param>';
     }
     entryRule += entryRuleEnd;
+    if (typeof wrap_call == "function") {
+        entryRule = wrap_call(entryRule);
+    }
+    entryRule = '<xsl:template xmlns="http://www.w3.org/1999/xhtml" priority="9" match="/">' +
+        entryRule + '</xsl:template>';
     let modXsl = injectTemplate(xsl, entryRule);
     return xslt(str2xml('<root/>'), modXsl);
 }
diff --git a/test/core/js/modules/ext_bottom_line.js.js b/test/core/js/modules/ext_bottom_line.js.js
index 610d95d1b00b3a8bf220a6d39e12ed71ecaa9090..48dc64f231c1dc5929eece1f44756d60cb17c0a1 100644
--- a/test/core/js/modules/ext_bottom_line.js.js
+++ b/test/core/js/modules/ext_bottom_line.js.js
@@ -24,10 +24,6 @@
 
 var ext_bottom_line_test_suite = function ($, ext_bottom_line, QUnit) {
 
-    const sleep = (ms) => {
-      return new Promise(res => setTimeout(res, ms))
-    }
-
     var test_config = { "version": 0.1,
       "fallback": "blablabla",
       "creators": [
diff --git a/test/core/js/modules/ext_revisions.js.js b/test/core/js/modules/ext_revisions.js.js
deleted file mode 100644
index e90fd7c97851e5f054690cb0d376be5e14d826e4..0000000000000000000000000000000000000000
--- a/test/core/js/modules/ext_revisions.js.js
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * ** 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';
-
-var ext_revisions_test_suite = function ($, ext_revisions, QUnit, edit_mode) {
-
-    var datamodel = ext_revisions._datamodel;
-
-    QUnit.module("ext_revisions.js", {
-        before: function (assert) {
-            // setup before module
-            this.original_update_entity = edit_mode.update_entity;
-            this.original_insert = transaction.insertEntitiesXml;
-            this.original_retrieve = transaction.retrieveEntityById;
-            this.original_query = query;
-            ext_revisions._logger.setLevel("trace");
-        },
-        beforeEach: function (assert) {
-            // setup before each test
-            datamodel.obsolete = "UNITTESTObsolete";
-            datamodel.revisionOf = "UNITTESTRevisionOf";
-        },
-        afterEach: function (assert) {
-            // teardown after each test
-            query = this.original_query;
-            edit_mode.update_entity = this.original_update_entity;
-            transaction.insertEntitiesXml = this.original_insert;
-            transaction.retrieveEntityById = this.original_retrieve;
-        },
-        after: function (assert) {
-            // teardown after module
-        }
-    });
-
-    QUnit.test("_make_revision_of_property", async function(assert) {
-        var p = await ext_revisions._make_revision_of_property("1234");
-        var editfield = $(p).find(".caosdb-property-edit-value");
-        var value = $(editfield).find("select").first()[0].selectedOptions[0].value;
-        assert.ok($(p).hasClass("caosdb-f-entity-property"), "is property");
-        assert.equal(value, "1234", "has value 1234");
-        assert.equal(getPropertyName(p), datamodel.revisionOf, "has revisionOf name");
-        assert.equal(getPropertyDatatype(p), datamodel.obsolete, "has Obsolete datatype");
-    });
-
-    /**
-     * This is a rather complete test, not a unit test.
-     */
-    QUnit.test("update calls update_entity through proxy", async function (assert) {
-        var done = assert.async(3);
-        var done_query = assert.async(2);
-        var ent_element = $('<div data-entity-id="15"><div class="caosdb-properties"/></div>')[0];
-
-        // mock server responses to several requests...
-        var retrieve_fun = async function(id) {
-            assert.equal(id, "15", "retrieve id 15");
-            done();
-            return $(`<Record id="15"><Parent name="ORIG_PARENT"/></Record>`)[0];
-        }
-        var insert_fun = async function(xml) {
-            var rec = xml.firstElementChild.firstElementChild;
-            assert.equal(rec.id, "-1", "insert with tmp id");
-            assert.equal($(rec).find("Parent").attr("name"), datamodel.obsolete, "Obsolete Parent");
-            xml.firstElementChild.firstElementChild.id = "2345";
-            console.log(xml2str(xml));
-            done();
-            return xml;
-        };
-        var update_fun = async function(ent_element) {
-            var prop = edit_mode.getProperties(ent_element)[0];
-            assert.equal(prop.name, datamodel.revisionOf, "has revisionOf");
-            assert.equal(prop.value, "2345", "revisionOf 2345");
-            done();
-        };
-        var query_fun = async function(query) {
-            assert.ok(query.startsWith("FIND") && ( query.endsWith(datamodel.obsolete) || query.endsWith(datamodel.revisionOf)), query);
-            done_query(); // called twice
-            return [$(`<div data-entity-name="${datamodel.revisionOf}" data-caosdb-id="3456"/>`)[0]];
-        }
-
-        // injecting the server mock-up responses.
-        transaction.retrieveEntityById = retrieve_fun;
-        transaction.insertEntitiesXml = insert_fun;
-        edit_mode.update_entity = update_fun;
-        query = query_fun;
-
-
-        // actual tests
-        assert.equal(update_fun, edit_mode.update_entity, "before init, the edit_mode.update_entity function has not been overridden.");
-
-        // call init which checks the datamodel and overwrites the
-        // edit_mode.update_entity function.
-        await ext_revisions.init();
-        assert.notEqual(update_fun, edit_mode.update_entity, "after init, the edit_mode.update_entity hab been overriden with a proxy calling the update_fun and the original function.");
-
-        // call edit_mode.update_entity which calls the insert_fun and the
-        // update_fun
-        await edit_mode.update_entity(ent_element);
-    });
-
-}($, ext_revisions, QUnit, edit_mode);
diff --git a/test/core/js/modules/ext_xls_download.js.js b/test/core/js/modules/ext_xls_download.js.js
index 5426580436933b942cec6750b521747ab62f2d00..74cdb244dcf0f5c2238c8d2503a3194580c35d90 100644
--- a/test/core/js/modules/ext_xls_download.js.js
+++ b/test/core/js/modules/ext_xls_download.js.js
@@ -32,7 +32,7 @@
  * @return {HTMLElement} DIV.caosdb-query-response
  */
 transformation.transformSelectTable = async function _tST (xml) {
-    var root_template = '<xsl:template match="/"><div class="root"><xsl:apply-templates select="Response/Query" mode="query-results"/></div></xsl:template>';
+    var root_template = '<xsl:template xmlns="http://www.w3.org/1999/xhtml" match="/"><div class="root"><xsl:apply-templates select="Response/Query" mode="query-results"/></div></xsl:template>';
     var queryXsl = await transformation.retrieveXsltScript("query.xsl");
     var entityXsl = await transformation.retrieveEntityXsl(root_template);
     insertParam(entityXsl, "uppercase", 'abcdefghijklmnopqrstuvwxyz');
@@ -64,40 +64,35 @@ QUnit.module("ext_xls_download", {
 });
 
 
-{
-  const sleep = function sleep(ms) {
-    return new Promise(resolve => setTimeout(resolve, ms));
-  }
 
-  QUnit.test("call downloadXLS", async function(assert) {
-    var done = assert.async(2);
+QUnit.test("call downloadXLS", async function(assert) {
+  var done = assert.async(2);
 
-    // mock server response (successful)
-    connection.runScript = async function(exec, param){
-        assert.equal(exec, "xls_from_csv.py", "call xls_from_csv.py");
-        done();
-        return str2xml('<response><script code="0" /><stdout>bla</stdout></response>');
-    }
+  // mock server response (successful)
+  connection.runScript = async function(exec, param){
+      assert.equal(exec, "xls_from_csv.py", "call xls_from_csv.py");
+      done();
+      return str2xml('<response><script code="0" /><stdout>bla</stdout></response>');
+  }
 
-    caosdb_table_export.go_to_script_results = function(filename) {
-        assert.equal(filename, "bla", "filename correct");
-        done();
-    }
+  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" />');
-    var modal = $('<div id="downloadModal"><div>');
-    $(document.body).append([tsv_data, modal]);
+  var tsv_data = $('<a id="caosdb-f-query-select-data-tsv" />');
+  var modal = $('<div id="downloadModal"><div>');
+  $(document.body).append([tsv_data, modal]);
 
 
-    var xsl_link = $("<a/>");
-    downloadXLS(xsl_link[0]);
+  var xsl_link = $("<a/>");
+  downloadXLS(xsl_link[0]);
 
-    await sleep(500);
+  await sleep(500);
 
-    tsv_data.remove();
-    modal.remove();
-  });
-}
+  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");
@@ -120,7 +115,7 @@ QUnit.test("_get_tsv_string", function(assert) {
     var f = caosdb_table_export._create_tsv_string
     var tsv_string = f(entities, ["Bag", "Number"], true);
     var prefix = "data:text/csv;charset=utf-8,"
-    assert.equal(tsv_string, 
+    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);
diff --git a/test/core/js/modules/form_elements.js.js b/test/core/js/modules/form_elements.js.js
index f8bf1ed1ac1495a8a3688aeb7b0cce387c8b69fb..aa2e281c7d25f03ce8ed109fb5728919635abec6 100644
--- a/test/core/js/modules/form_elements.js.js
+++ b/test/core/js/modules/form_elements.js.js
@@ -23,7 +23,7 @@
 'use strict';
 
 QUnit.module("form_elements.js", {
-    before: function(assert) {
+    before: function (assert) {
         markdown.init();
         var entities = [
             $('<div><div class="caosdb-id" data-entity-name="name12">id12</div></div>')[0],
@@ -32,27 +32,27 @@ QUnit.module("form_elements.js", {
             $('<div><div class="caosdb-id" data-entity-name="name15">id15</div></div>')[0],
         ];
 
-        form_elements._query = async function(query) {
+        form_elements._query = async function (query) {
             return entities;
         };
-        this.get_example_1 = async function() {
+        this.get_example_1 = async function () {
             return $(await $.ajax("html/form_elements_example_1.html"))[0];
         };
     },
-    after: function(assert) {
+    after: function (assert) {
         form_elements._init_functions();
     }
 });
 
-QUnit.test("availability", function(assert) {
+QUnit.test("availability", function (assert) {
     assert.equal(form_elements.version, "0.1", "test version");
     assert.ok(form_elements.init, "init available");
     assert.ok(form_elements.version, "version available");
 });
 
-QUnit.test("make_reference_option", function(assert) {
+QUnit.test("make_reference_option", function (assert) {
     assert.equal(typeof form_elements.make_reference_option, "function", "function available");
-    assert.throws(()=>form_elements.make_reference_option(), /is expected to be a string/, "noargs throws");
+    assert.throws(() => form_elements.make_reference_option(), /is expected to be a string/, "noargs throws");
     var option = form_elements.make_reference_option("id15");
     assert.equal($(option).val(), "id15", "value");
     assert.equal($(option).text(), "id15", "text");
@@ -62,14 +62,17 @@ QUnit.test("make_reference_option", function(assert) {
 });
 
 
-QUnit.test("make_reference_select", async function(assert) {
+QUnit.test("make_reference_select", async function (assert) {
     assert.equal(typeof form_elements.make_reference_select, "function", "function available");
-    //assert.throws(()=> unasync(form_elements.make_reference_select), /param `entities` is expected to be an array/, "undefined entities throws");
-    //assert.throws(()=> unasync(form_elements.make_reference_select, "test"), /param `entities` is expected to be an array/, "string entities throws");
-    var select = await form_elements.make_reference_select([
-        {dataset: {entityId : "id17"}},
-        {dataset: {entityId : "id18"}},
-    ]);
+    var select = await form_elements.make_reference_select([{
+        dataset: {
+            entityId: "id17"
+        }
+    }, {
+        dataset: {
+            entityId: "id18"
+        }
+    }, ]);
     assert.ok($(select).hasClass("selectpicker"), "selectpicker class from bootstrap-select");
     assert.notOk($(select).val(), "unselected");
     $(select).val(["id18"]);
@@ -86,7 +89,7 @@ QUnit.test("make_reference_select", async function(assert) {
 });
 
 
-QUnit.test("make_script_form", async function(assert) {
+QUnit.test("make_script_form", async function (assert) {
     assert.equal(typeof form_elements.make_script_form, "function", "function available");
 
     // TODO
@@ -96,12 +99,31 @@ QUnit.test("make_script_form", async function(assert) {
 
     var done = assert.async(3);
     var config = {
-        groups: [
-            { name: "group1", fields: ["date"], enabled: false },
-        ],
-        fields: [
-            {type: "date", name: "baldate"},
-        ],
+        groups: [{
+            name: "group1",
+            fields: ["date"],
+            enabled: false
+        }, ],
+        fields: [{
+            type: "date",
+            name: "baldate"
+        }, {
+            type: "select",
+            name: "Sex",
+            label: "Sex",
+            value: "female",
+            required: true,
+            options: [{
+                value: "female",
+                label: "female"
+            }, {
+                value: "diverse",
+                label: "diverse"
+            }, {
+                value: "male",
+                label: "male"
+            }]
+        }],
     };
 
     var script_form = await form_elements.make_script_form(config, "test_script");
@@ -115,24 +137,29 @@ QUnit.test("make_script_form", async function(assert) {
     assert.equal(cancel_button.length, 1, "has cancel button");
 
     var field = $(script_form).find(".caosdb-f-field");
-    assert.equal(field.length, 1, "has one field");
+    assert.equal(field.length, 2, "has two field");
     assert.equal(field.find("input[type='date']").length, 1, "has date input");
+    assert.equal(field.find("select").length, 1, "has select input");
 
-    script_form.addEventListener("caosdb.form.cancel", function(e) {
+    script_form.addEventListener("caosdb.form.cancel", function (e) {
         done();
     }, true);
     cancel_button.click();
 
 
-    script_form.addEventListener("caosdb.form.error", function(e) {
+    script_form.addEventListener("caosdb.form.error", function (e) {
         assert.equal($(script_form).find(".caosdb-f-form-elements-message-error").length, 2, "error message there (call and stderr)");
         done();
         script_form.remove();
     });
 
-    form_elements._run_script = async function(script, params) {
+    form_elements._run_script = async function (script, params) {
         done();
-        return {code: "1", stderr: "Autsch!", call: "none"};
+        return {
+            code: "1",
+            stderr: "Autsch!",
+            call: "none"
+        };
     };
 
     assert.equal($(script_form).find(".caosdb-f-form-error-message").length, 0, "no error message");
@@ -144,7 +171,7 @@ QUnit.test("make_script_form", async function(assert) {
 });
 
 
-QUnit.test("make_date_input", function(assert) {
+QUnit.test("make_date_input", function (assert) {
     assert.equal(typeof form_elements.make_date_input, "function", "function available");
 
     var config = {
@@ -164,7 +191,7 @@ QUnit.test("make_date_input", function(assert) {
 
 });
 
-QUnit.test("make_range_input", async function(assert) {
+QUnit.test("make_range_input", async function (assert) {
     assert.equal(typeof form_elements.make_range_input, "function", "function available");
 
     var config = {
@@ -192,20 +219,27 @@ QUnit.test("make_range_input", async function(assert) {
 
 });
 
-QUnit.test("make_form_field", async function(assert) {
+QUnit.test("make_form_field", async function (assert) {
     assert.equal(typeof form_elements.make_form_field, "function", "function available");
 
-    var cached = false; 
-    for ( var t of ["date", "range", "reference_drop_down", "subform", "checkbox", "double", "integer"] ) {
+    var cached = false;
+    for (var t of ["date", "range", "reference_drop_down", "subform", "checkbox", "double", "integer"]) {
         cached = !cached;
         var config = {
-            help: {title: "HELP", content: "help me, help me, help me-e-e!"},
+            help: {
+                title: "HELP",
+                content: "help me, help me, help me-e-e!"
+            },
             type: t,
             cached: cached,
             name: "a name",
             label: "a label",
-            from: {name: "from_bla"},
-            to: {name: "to_bla"},
+            from: {
+                name: "from_bla"
+            },
+            to: {
+                name: "to_bla"
+            },
             query: "FIND something",
             make_desc: getEntityName,
             fields: [],
@@ -220,8 +254,8 @@ QUnit.test("make_form_field", async function(assert) {
 });
 
 
-QUnit.test("make_subform", async function(assert) {
-    assert.equal(typeof form_elements.make_reference_drop_down, "function", "function available");
+QUnit.test("make_subform", async function (assert) {
+    assert.equal(typeof form_elements.make_subform, "function", "function available");
 
     const config = {
         type: "subform",
@@ -239,7 +273,7 @@ QUnit.test("make_subform", async function(assert) {
 });
 
 
-QUnit.test("make_reference_drop_down", async function(assert) {
+QUnit.test("make_reference_drop_down", async function (assert) {
     assert.equal(typeof form_elements.make_reference_drop_down, "function", "function available");
 
     var config = {
@@ -257,7 +291,7 @@ QUnit.test("make_reference_drop_down", async function(assert) {
     assert.equal(label.text(), "IceCore", "label has text");
 });
 
-QUnit.test("make_checkbox_input", function(assert) {
+QUnit.test("make_checkbox_input", function (assert) {
     assert.equal(typeof form_elements.make_checkbox_input, "function", "function available");
 
 
@@ -274,7 +308,7 @@ QUnit.test("make_checkbox_input", function(assert) {
     assert.equal(input.attr("name"), "approved", "input has name");
     assert.ok(input.is(":checked"), "input is checked");
 
-    var obj = form_elements.form_to_object(field);
+    var obj = form_elements.form_to_object(field)[0];
     assert.equal(obj["approved"], "yes!!!", "checked value");
 
 
@@ -286,36 +320,72 @@ QUnit.test("make_checkbox_input", function(assert) {
     assert.equal(input.attr("name"), "approved", "input has name");
     assert.notOk(input.is(":checked"), "input is not checked");
 
-    obj = form_elements.form_to_object(field);
+    obj = form_elements.form_to_object(field)[0];
     assert.equal(typeof obj["approved"], "undefined", "no checked value");
 
 
 });
 
-QUnit.test("form_to_object", async function(assert) {
+QUnit.test("form_to_object", async function (assert) {
     assert.equal(typeof form_elements.form_to_object, "function", "function available");
 
     var config = {
-        fields: [
-            { type: "date", name: "the-date" },
-            { type: "reference_drop_down", name: "icecore", query: "FIND Record IceCore"},
-            { type: "range", name: "the-range", from: {name: "fromblla"}, to: {name: "toblla"}},
-            { type: "subform", name: "subform1", fields: [
-                { type: "date", name: "the-other-date", },
-                { type: "checkbox", name: "rectangular", },
-            ],},
-        ],
+        fields: [{
+            type: "date",
+            name: "the-date"
+        }, {
+            type: "reference_drop_down",
+            name: "icecore",
+            query: "FIND Record IceCore"
+        }, {
+            type: "range",
+            name: "the-range",
+            from: {
+                name: "fromblla"
+            },
+            to: {
+                name: "toblla"
+            }
+        }, {
+            type: "subform",
+            name: "subform1",
+            fields: [{
+                type: "date",
+                name: "the-other-date",
+            }, {
+                type: "checkbox",
+                name: "rectangular",
+            }, ],
+        }, {
+            type: "select",
+            required: true,
+            cached: true,
+            name: "sex",
+            label: "Sex",
+            value: "d",
+            options: [{
+                "value": "f",
+                "label": "female"
+            }, {
+                "value": "d",
+                "label": "diverse"
+            }, {
+                "value": "m",
+                "label": "male"
+            }, ],
+        }, ],
     };
 
     var form = await form_elements.make_script_form(config, "bla");
 
-    var json = form_elements.form_to_object(form);
+    var json = form_elements.form_to_object(form)[0];
 
     assert.equal(typeof json["cancel"], "undefined", "cancel button not serialized");
     assert.equal(json["the-date"], "", "date");
     assert.equal(json["icecore"], null, "reference_drop_down");
     assert.equal(json["fromblla"], "", "range from");
     assert.equal(json["toblla"], "", "range to");
+    assert.equal(json["sex"], "d", "select");
     assert.equal(typeof json["the-other-date"], "undefined", "subform element not on root level");
 
     var subform = json["subform1"];
@@ -327,10 +397,13 @@ QUnit.test("form_to_object", async function(assert) {
 });
 
 
-QUnit.test("make_double_input", function(assert) {
+QUnit.test("make_double_input", function (assert) {
     assert.equal(typeof form_elements.make_double_input, "function", "function available");
 
-    var config = {type: "double", name: "d"};
+    var config = {
+        type: "double",
+        name: "d"
+    };
     var input = $(form_elements.make_double_input(config)).find("input");
     assert.ok(input.is("[type='number'][step='any']"), "double input");
 
@@ -340,10 +413,13 @@ QUnit.test("make_double_input", function(assert) {
 
 });
 
-QUnit.test("make_integer_input", function(assert) {
+QUnit.test("make_integer_input", function (assert) {
     assert.equal(typeof form_elements.make_integer_input, "function", "function available");
 
-    var config = {type: "integer", name: "i"};
+    var config = {
+        type: "integer",
+        name: "i"
+    };
     var input = $(form_elements.make_integer_input(config)).find("input");
     assert.ok(input.is("[type='number'][step='1']"), "integer input");
 
@@ -352,21 +428,26 @@ QUnit.test("make_integer_input", function(assert) {
     assert.equal(input.val("abc").val(), "", "abc not valid");
 });
 
-QUnit.test("make_form", function(assert) {
+QUnit.test("make_form", function (assert) {
     assert.equal(typeof form_elements.make_form, "function", "function available");
 
-    var form1 = form_elements.make_form({fields: []});
+    var form1 = form_elements.make_form({
+        fields: []
+    });
     assert.equal(form1.tagName, "DIV", "wrapper is div");
     assert.ok($(form1).hasClass("caosdb-f-form-wrapper"), "div has caosdb-f-form-wrapper class");
     assert.equal($(form1).find(".h3").length, 0, "no header");
 
-    var form2 = form_elements.make_form({fields: [], header: "bla"});
+    var form2 = form_elements.make_form({
+        fields: [],
+        header: "bla"
+    });
     assert.equal(form2.tagName, "DIV", "wrapper is div");
     assert.equal($(form2).find(".h3").length, 1, "one header");
     assert.equal($(form2).find(".h3").text(), "bla", "header text set");
 });
 
-QUnit.test("enable/disable_group", function(assert) {
+QUnit.test("enable/disable_group", function (assert) {
     assert.equal(typeof form_elements.disable_group, "function", "function available");
     assert.equal(typeof form_elements.enable_group, "function", "function available");
 
@@ -413,11 +494,11 @@ QUnit.test("enable/disable_group", function(assert) {
 });
 
 
-QUnit.test("parse_script_result", function(assert) {
+QUnit.test("parse_script_result", function (assert) {
     assert.equal(typeof form_elements.parse_script_result, "function", "function available");
 
     var result = str2xml(
-`<?xml version="1.0" encoding="UTF-8"?>
+        `<?xml version="1.0" encoding="UTF-8"?>
 <?xml-stylesheet type="text/xsl" href="https://localhost:10443/webinterface/webcaosdb.xsl" ?>
 <Response username="admin" realm="PAM" srid="256c14970dac2b2b5649973d52e4c06a" timestamp="1570785591824" baseuri="https://localhost:10443">
   <UserInfo username="admin" realm="PAM">
@@ -443,15 +524,15 @@ QUnit.test("parse_script_result", function(assert) {
 });
 
 
-QUnit.test("disable_name", function(assert) {
+QUnit.test("disable_name", function (assert) {
     assert.equal(typeof form_elements.disable_name, "function", "function available");
 });
 
-QUnit.test("enable_name", function(assert) {
+QUnit.test("enable_name", function (assert) {
     assert.equal(typeof form_elements.enable_name, "function", "function available");
 });
 
-QUnit.test("add_field_to_group", function(assert) {
+QUnit.test("add_field_to_group", function (assert) {
     assert.equal(typeof form_elements.add_field_to_group, "function", "function available");
 
     var field = $(form_elements._make_field_wrapper("field1"))[0];
@@ -462,7 +543,7 @@ QUnit.test("add_field_to_group", function(assert) {
 
 });
 
-QUnit.test("cache_form", async function(assert) {
+QUnit.test("cache_form", async function (assert) {
     var form = await this.get_example_1();
     assert.equal($(form).find("form").length, 1, "example form available");
 
@@ -476,7 +557,7 @@ QUnit.test("cache_form", async function(assert) {
 });
 
 
-QUnit.test("load_cached", async function(assert) {
+QUnit.test("load_cached", async function (assert) {
     var done = assert.async();
     var form = await this.get_example_1();
     assert.equal($(form).find("form").length, 1, "example form available");
@@ -498,7 +579,7 @@ QUnit.test("load_cached", async function(assert) {
 });
 
 
-QUnit.test("field_ready", function(assert) {
+QUnit.test("field_ready", function (assert) {
     var done = assert.async(3);
     var field1 = $('<div id="f1"><div class="caosdb-f-field-not-ready"/></div>')[0];
     var field2 = $('<div id="f2" class="caosdb-f-field-not-ready"/>')[0];
@@ -541,16 +622,13 @@ QUnit.test("field_ready", function(assert) {
     });
 });
 
-{
-const sleep = (ms) => {
-  return new Promise(res => setTimeout(res, ms))
-}
-
-QUnit.test("make_alert - cancel", async function(assert) {
+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");},
+        proceed_callback: () => {
+            assert.ok(false, "this should not be called");
+        },
         cancel_callback: cancel_callback,
     });
     $("body").append(_alert);
@@ -564,7 +642,7 @@ QUnit.test("make_alert - cancel", async function(assert) {
 
 });
 
-QUnit.test("make_alert - proceed", async function(assert) {
+QUnit.test("make_alert - proceed", async function (assert) {
     var proceed_callback = assert.async();
     var _alert = form_elements.make_alert({
         message: "message",
@@ -581,7 +659,7 @@ QUnit.test("make_alert - proceed", async function(assert) {
 
 });
 
-QUnit.test("make_alert - remember", async function(assert) {
+QUnit.test("make_alert - remember", async function (assert) {
     form_elements._set_alert_decision("unittests", "");
 
     var proceed_callback = assert.async(3);
@@ -628,4 +706,83 @@ QUnit.test("make_alert - remember", async function(assert) {
     assert.equal(typeof _alert, "undefined", "alert was not created, proceed callback was called third time");
 });
 
-}
+QUnit.test("make_select_input", function (assert) {
+    const config = {
+        name: "sex",
+        label: "Sex",
+        multiple: true,
+        options: [{
+            "value": "f",
+            "label": "female"
+        }, {
+            "value": "d",
+            "label": "diverse"
+        }, {
+            "value": "m",
+            "label": "male"
+        }, ],
+    }
+    const select = $(form_elements.make_select_input(config));
+    assert.equal(select.find("select").length, 1, "select input there");
+    assert.equal(select.find("select").attr("name"), "sex", "has select with correct name");
+    assert.equal(select.find("select option").length, 3, "three options there");
+});
+
+QUnit.test("select_input caching", async function (assert) {
+    const config = {
+        "name": "test-form",
+        "fields": [{
+            type: "select",
+            required: true,
+            cached: true,
+            name: "sex",
+            label: "Sex",
+            options: [{
+                "value": "f",
+                "label": "female"
+            }, {
+                "value": "d",
+                "label": "diverse"
+            }, {
+                "value": "m",
+                "label": "male"
+            }, ],
+        }, ],
+    }
+    const form_wrapper = $(form_elements.make_form(config));
+    await sleep(200);
+    const form = form_wrapper.find("form");
+    assert.equal(form.find("select").length, 1);
+
+
+    // write to cache
+    const cache = {};
+    const field = $(form_elements.get_fields(form[0], "sex"));
+    field.find("select").val("f");
+    assert.equal(form_elements.get_cache_value(field[0]), "f", "initial value set");
+    assert.equal(form_elements.get_cache_key(form[0], field[0]), "form_elements.cache.test-form.sex", "cache key correct");
+
+    form_elements.cache_form(cache, form[0]);
+    assert.equal(cache[form_elements.get_cache_key(form[0], field[0])], "f");
+
+    // read from cache and set the value
+    field.find("select").val("m");
+    assert.equal(form_elements.get_cache_value(field[0]), "m", "different value set");
+
+    form_elements.load_cached(cache, form[0]);
+    await sleep(200);
+    assert.equal(form_elements.get_cache_value(field[0]), "f", "value back to value from cache");
+});
+
+QUnit.test("make_file_input", function (assert) {
+    const config = {
+        name: "some_file",
+        multiple: true,
+        accept: ".tsv, .csv",
+    }
+    const file_input = $(form_elements.make_file_input(config));
+    assert.equal(file_input.find(":input").length, 1, "file input there");
+    assert.equal(file_input.find(":input").attr("name"), "some_file", "has file input with correct name");
+    assert.ok(file_input.find(":input").prop("multiple"), "is multiple");
+    assert.equal(file_input.find(":input").attr("accept"), ".tsv, .csv", "accept there");
+});
\ No newline at end of file
diff --git a/test/core/js/modules/navbar.xsl.js b/test/core/js/modules/navbar.xsl.js
index 0b43cae70cbd4694290885560c69594a5d4a555e..78113ce2bd7c9ada61205ae5fc3b2e098ee92e8f 100644
--- a/test/core/js/modules/navbar.xsl.js
+++ b/test/core/js/modules/navbar.xsl.js
@@ -57,25 +57,3 @@ QUnit.test("create navbar", function(assert){
 	assert.ok(html, "html ok");
 	assert.equal(html.firstChild.tagName, "NAV", "is nav element");
 });
-
-/* MISC FUNCTIONS */
-function getXSLScriptClone(source){
-	return str2xml(xml2str(source))
-}
-
-function injectTemplate(orig_xsl, template){
-	var xsl = getXSLScriptClone(orig_xsl);	
-	var entry_t = xsl.createElement("xsl:template");
-	xsl.firstElementChild.appendChild(entry_t);
-	entry_t.outerHTML = template;
-	return xsl;
-}
-
-function insertParam(xsl, name, value=null){
-	var param = xsl.createElement("xsl:param");
-	param.setAttribute("name", name);
-	if (value != null) {
-		param.setAttribute("select", "'"+value+"'");
-	}
-	xsl.firstElementChild.append(param);
-}
diff --git a/test/core/js/modules/query.xsl.js b/test/core/js/modules/query.xsl.js
index 6c371281e7d4b01db0050a00111064d1d3c488b0..5c37ad4e39971130912a9169ab64189d20e5a591 100644
--- a/test/core/js/modules/query.xsl.js
+++ b/test/core/js/modules/query.xsl.js
@@ -172,10 +172,17 @@ QUnit.test("template entity-link", function(assert){
 });
 
 QUnit.test("template select-table-row ", function(assert){
-    let row = callTemplate(this.queryXSL, "select-table-row", {"entity-id": "sdfg"}, str2xml('<Response>'));
-    assert.equal(row.firstElementChild.tagName, "TR", "tagName = TR");
-    assert.equal(row.firstElementChild.firstElementChild.tagName, "TD", "tagName = TD");
-    assert.equal(row.firstElementChild.firstElementChild.firstElementChild.tagName, "A", "tagName = A");
+    let row = callTemplate(this.queryXSL, "select-table-row", {"entity-id": "sdfg", "version-id": "dsfg", "ishead": "true"}, (x) => `<table><tbody>${x}</tbody></table>`);
+    var next = row.firstElementChild;
+    assert.equal(next.tagName, "TABLE", "tagName = TABLE");
+    next = next.firstElementChild;
+    assert.equal(next.tagName, "TBODY", "tagName = TBODY");
+    next = next.firstElementChild;
+    assert.equal(next.tagName, "TR", "tagName = TR");
+    next = next.firstElementChild;
+    assert.equal(next.tagName, "TD", "tagName = TD");
+    next = next.firstElementChild;
+    assert.equal(next.tagName, "A", "tagName = A");
 });
 
 /* MISC FUNCTIONS */
@@ -185,8 +192,6 @@ function getQueryForm(queryXSL) {
 }
 
 function getQueryFormContainer(queryXSL) {
-    var xsl = injectTemplate(queryXSL, '<xsl:template match="/"><xsl:call-template name="caosdb-query-panel"/></xsl:template>"')
-    var xml = str2xml("<root/>");
-    var html = xslt(xml, xsl);
+    var html = callTemplate(queryXSL, "caosdb-query-panel", {});
     return html.firstElementChild;
 }
diff --git a/test/core/js/modules/query_shortcuts.js.js b/test/core/js/modules/query_shortcuts.js.js
index f088b729001daac8a42ed276507e2370f3e08679..3798c5fe81ea6e860cd176797a9a6959a9a64895 100644
--- a/test/core/js/modules/query_shortcuts.js.js
+++ b/test/core/js/modules/query_shortcuts.js.js
@@ -141,7 +141,9 @@ QUnit.test("init_delete_shortcut_form", function(assert) {
     assert.equal(panel.find(".caosdb-f-form-wrapper").length, 1, "panel has form after");
 
     // test cancel button
-    panel[0].addEventListener("caosdb.form.cancel", function(e) {
+    var done = assert.async();
+    panel[0].addEventListener("caosdb.form.cancel", async function(e) {
+        await sleep(200);
         assert.equal(panel.find("form").length, 0, "form is gone");
         done();
     }, true);
@@ -187,23 +189,17 @@ QUnit.test("make_delete_form", function(assert) {
     }
 
     var form = query_shortcuts.make_delete_form(panel[0], delete_callback);
+    $('body').append(form);
 
-    // wait for form
-    form.addEventListener("caosdb.form.ready", function(e) {
-
-        assert.equal($(form).hasClass("caosdb-f-form-wrapper"), true, "is form");
-        assert.equal($(form).find("input[type='checkbox']").length, 3, "three checkboxes (for three user-defined shortcuts)");
-
-        // check two
-        $(form).find(":checkbox[name='id28']").click();
-        $(form).find(":checkbox[name='id29']").click();
-        $(form).find("[type='submit']").click();
-
-        $(form).find("button.caosdb-f-form-elements-cancel-button").click();
+    assert.equal($(form).hasClass("caosdb-f-form-wrapper"), true, "is form");
+    assert.equal($(form).find("input[type='checkbox']").length, 3, "three checkboxes (for three user-defined shortcuts)");
 
-    }, true);
+    // check two
+    $(form).find(":checkbox[name='id28']").click();
+    $(form).find(":checkbox[name='id29']").click();
+    $(form).find("[type='submit']").click();
 
-    $('body').append(form);
+    $(form).find("button.caosdb-f-form-elements-cancel-button").click();
 
 });
 
@@ -228,31 +224,24 @@ QUnit.test("transform_entities", async function(assert) {
 });
 
 QUnit.test("make_create_form", function(assert) {
-
-    var done = assert.async();
     var panel = $('<div/>');
     var form = query_shortcuts.make_create_form(panel[0], () => {});
     assert.ok($(form).hasClass("caosdb-f-form-wrapper"), "form created");
 
     $('body').append(form);
 
-    form.addEventListener(form_elements.form_ready_event.type, function(e) {
-        $(form).find(":input[name='templateDescription']").val("NEW DESC");
-        $(form).find(":input[name='Query']").val("NEW QUERY");
+    $(form).find(":input[name='templateDescription']").val("NEW DESC");
+    $(form).find(":input[name='Query']").val("NEW QUERY");
 
-        var entity = getEntityXML(form);
-        assert.equal(xml2str(entity), "<Record><Parent name=\"UserTemplate\"/><Property name=\"templateDescription\">NEW DESC</Property><Property name=\"Query\">NEW QUERY</Property></Record>", "entity extracted");
+    var entity = getEntityXML(form);
+    assert.equal(xml2str(entity), "<Record><Parent name=\"UserTemplate\"/><Property name=\"templateDescription\">NEW DESC</Property><Property name=\"Query\">NEW QUERY</Property></Record>", "entity extracted");
 
-        form_elements.dismiss_form(form);
-        done();
+    form_elements.dismiss_form(form);
 
-    }, true);
 
 });
 
 QUnit.test("make_update_form", function(assert) {
-
-    var done = assert.async();
     var panel = $('<div/>');
     var header = $('<span class="h3">Shortcuts</span>');
     var userTemplate1 = query_shortcuts.generate_user_shortcut("the_description", "FIND nothing", "id28");
@@ -271,16 +260,12 @@ QUnit.test("make_update_form", function(assert) {
 
     $('body').append(form);
 
-    form.addEventListener(form_elements.form_ready_event.type, function(e) {
-        $(form).find(":input[name='templateDescription']").val("NEW DESC");
-        $(form).find(":input[name='Query']").val("NEW QUERY");
-
-        var entity = getEntityXML(form);
-        assert.equal(xml2str(entity), "<Record id=\"id28\"><Parent name=\"UserTemplate\"/><Property name=\"templateDescription\">NEW DESC</Property><Property name=\"Query\">NEW QUERY</Property></Record>", "entity extracted");
+    $(form).find(":input[name='templateDescription']").val("NEW DESC");
+    $(form).find(":input[name='Query']").val("NEW QUERY");
 
-        form_elements.dismiss_form(form);
-        done();
+    var entity = getEntityXML(form);
+    assert.equal(xml2str(entity), "<Record id=\"id28\"><Parent name=\"UserTemplate\"/><Property name=\"templateDescription\">NEW DESC</Property><Property name=\"Query\">NEW QUERY</Property></Record>", "entity extracted");
 
-    }, true);
+    form_elements.dismiss_form(form);
 
 });
diff --git a/test/core/js/modules/webcaosdb.js.js b/test/core/js/modules/webcaosdb.js.js
index 7b3f7abf404f261668c18690df337b73794dd8bd..58cbc2e71c6e558652a30caa9445a38a78fad651 100644
--- a/test/core/js/modules/webcaosdb.js.js
+++ b/test/core/js/modules/webcaosdb.js.js
@@ -39,7 +39,7 @@ QUnit.module("webcaosdb.js", {
 /* TESTS */
 QUnit.test("xslt", function(assert) {
     let xml_str = '<root/>';
-    let xsl_str = '<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"><xsl:output method="html" /><xsl:template match="root"><newroot/></xsl:template></xsl:stylesheet>';
+    let xsl_str = '<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"><xsl:output method="html" /><xsl:template match="root"><newroot>content</newroot></xsl:template></xsl:stylesheet>';
     xml = str2xml(xml_str);
     xsl = str2xml(xsl_str);
     broken_xsl = str2xml('<blabla/>');
@@ -69,6 +69,28 @@ QUnit.test("xslt", function(assert) {
     }, "nu ll xsl throws exc.");
 });
 
+QUnit.test("markdown.textTohtml", function(assert) {
+    const str = `\# header\n\#\# another header\nparagraph`;
+    assert.equal(markdown.textToHtml(str), "<h1 id=\"header\">header</h1>\n<h2 id=\"anotherheader\">another header</h2>\n<p>paragraph</p>");
+
+});
+
+QUnit.test("injectTemplate", async function(assert) {
+    const xml_str = '<root/>';
+    const xsl_str = '<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"><xsl:output method="html" /></xsl:stylesheet>';
+    const xml = str2xml(xml_str);
+    const xsl = str2xml(xsl_str);
+
+    const result_xsl = injectTemplate(xsl, '<xsl:template xmlns="http://www.w3.org/1999/xhtml" match="root"><newroot>content</newroot></xsl:template>');
+
+    assert.equal(xml2str(result_xsl), "<xsl:stylesheet xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\" version=\"1.0\"><xsl:output method=\"html\"/><xsl:template xmlns=\"http://www.w3.org/1999/xhtml\" match=\"root\"><newroot>content</newroot></xsl:template></xsl:stylesheet>");
+    var result_xml = xslt(xml, result_xsl);
+    assert.equal(xml2str(result_xml), "<newroot xmlns=\"http://www.w3.org/1999/xhtml\">content</newroot>");
+
+    result_xml = await asyncXslt(xml, result_xsl);
+    assert.equal(xml2str(result_xml), "<newroot xmlns=\"http://www.w3.org/1999/xhtml\">content</newroot>");
+});
+
 QUnit.test("getEntityId", function(assert) {
     assert.ok(getEntityId, "function available");
     let okElem = $('<div><div class="caosdb-id">1234</div></div>')[0];
@@ -110,7 +132,7 @@ QUnit.test("asyncXslt", function(assert) {
 
     // broken xsl throws exception
     asyncXslt(xml, broken_xsl).catch((error) => {
-        assert.equal(/^\[Exception.*\]$/.test(error.toString()), true, "broken xsl thros exc.");
+        assert.equal(/^XSL Transformation.*$/.test(error.message), true, "broken xsl thros exc.");
         done();
     });
 });
@@ -125,7 +147,7 @@ QUnit.test("str2xml", function(assert) {
     assert.ok(xml);
 
     // make sure this is a document:
-    assert.equal(xml.contentType, "text/xml", "has contentType=text/xml");
+    assert.ok(xml.contentType.endsWith("/xml"), "has contentType=*/xml");
     assert.ok(xml.documentElement, "has documentElement");
     assert.equal(xml.documentElement.outerHTML, '<root/>', "has outerHTML");
 
@@ -214,9 +236,9 @@ QUnit.test("transformEntities", function(assert) {
 
 QUnit.test("mergeXsltScripts", function(assert) {
     assert.ok(transformation.mergeXsltScripts, 'function available.');
-    let xslMainStr = '<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"/>';
+    let xslMainStr = '<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"/>';
     assert.equal(xml2str(transformation.mergeXsltScripts(str2xml(xslMainStr), [])), xslMainStr, 'no includes returns same as xslMain.');
-    let xslIncludeStr = '<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"><xsl:template name="bla"/></xsl:stylesheet>'
+    let xslIncludeStr = '<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"><xsl:template name="bla"/></xsl:stylesheet>'
     let xslInclude = str2xml(xslIncludeStr);
     assert.ok($(transformation.mergeXsltScripts(str2xml(xslMainStr), [xslInclude])).find("[name='bla']")[0], 'template bla is there.');
 });
@@ -1522,7 +1544,7 @@ QUnit.test("convertNewCommentResponse", function(assert) {
     let expectedResult = "<li xmlns=\"http://www.w3.org/1999/xhtml\" class=\"list-group-item markdowned\"><div class=\"media\"><div class=\"media-left\"><h3>»</h3></div><div class=\"media-body\"><h4 class=\"media-heading\">someuser<small><i> posted on 2015-12-24T20:15:00</i></small></h4><p class=\"caosdb-comment-annotation-text\"><p>This is a comment</p></p></div></div></li>";
     convertNewAnnotationResponse(str2xml(testResponse), annotation.loadAnnotationXsl("../../")).then(function(result) {
         assert.equal(result.length, 1, "one element returned.");
-        assert.equal(xml2str(result[0]), expectedResult, "result converted correctly");
+        assert.equal(xml2str(result[0]).replace(/\n/g,""), expectedResult, "result converted correctly");
         done();
     }, function(error) {
         console.log(error);
@@ -1992,7 +2014,7 @@ QUnit.test("init_load_history_buttons and init_load_history_buttons", async func
 
     // load_button triggers retrieval of history
     load_button.click();
-    await sleep(200);
+    await sleep(500);
 
     var gone_button = $(html).find(".caosdb-f-entity-version-load-history-btn");
     assert.equal(gone_button.length, 0, "button is gone");
@@ -2008,7 +2030,3 @@ QUnit.test("init_load_history_buttons and init_load_history_buttons", async func
 
     $(html).remove();
 });
-
-const sleep = function sleep(ms) {
-  return new Promise(resolve => setTimeout(resolve, ms));
-}
diff --git a/test/core/js/setup.js b/test/core/js/setup.js
index 9894827988999e89a6388206f10bdd815098e81e..fac4fd78d0c867cf213917b16d874513d1871a09 100644
--- a/test/core/js/setup.js
+++ b/test/core/js/setup.js
@@ -46,3 +46,6 @@ QUnit.done(function( details ) {
     $.post("/done", report);
 });
 
+const sleep = function sleep(ms) {
+    return new Promise(resolve => setTimeout(resolve, ms));
+}
diff --git a/test/docker/Dockerfile b/test/docker/Dockerfile
index 026887097ac3b7dc13e6e429bf73c363e3adcbf3..92889dc526229475463ddcdfe6a2080a669b9d25 100644
--- a/test/docker/Dockerfile
+++ b/test/docker/Dockerfile
@@ -13,7 +13,7 @@ RUN  apt-get update \
     && apt-get install -f
 
 RUN pip3 install pylint pytest
-RUN pip3 install caosdb
+RUN pip3 install caosdb==0.5.1
 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
diff --git a/test/server_side_scripting/ext_table_preview/data/bad.csv b/test/server_side_scripting/ext_table_preview/data/bad.csv
deleted file mode 100644
index d29a9312a387186beb8bf4f77a8ec0e4b0ab80fa..0000000000000000000000000000000000000000
Binary files a/test/server_side_scripting/ext_table_preview/data/bad.csv and /dev/null 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
deleted file mode 100644
index d29a9312a387186beb8bf4f77a8ec0e4b0ab80fa..0000000000000000000000000000000000000000
Binary files a/test/server_side_scripting/ext_table_preview/data/bad.tsv and /dev/null differ
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
index 00d1c7f38746abe437abc76cd51b29600adcd049..c7c0cd3bc22c62ad4f1a214d4ed777718cdbf74a 100644
--- 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
@@ -67,8 +67,9 @@ class PreviewTest(unittest.TestCase):
             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 f in ["bad.xls", "bad.xlsx"]]
 
         for bfi in badfiles:
+            print("bfi: ", bfi)
             self.assertRaises(ValueError, read_file,
                               bfi, "."+bfi.split(".")[-1])