diff --git a/.gitignore b/.gitignore index cc9336b6256771f79eb104a8b1325a55352657e1..f69db87ad5a5226535559b6965e771d975ded103 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +# -*- mode:conf; -*- + # dot files .* !/.git* @@ -8,13 +10,19 @@ # the build dir /public +/sss_bin +/node_modules/ +/build +__pycache__ + +# auto-generated sources +/src/doc/api # screen logs screenlog.* xerr.log # extensions - conf/ext test/ext src/ext diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 548f86457c3216f5eed7b75b4b81d7f048351248..bcbcfcda9df7fa823ecd1454990bb8d6ff28ff79 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -6,6 +6,7 @@ # Copyright (C) 2019 Henrik tom Wörden # Copyright (C) 2020 Timm Fitschen (t.fitschen@indiscale.com) # Copyright (C) 2020 IndiScale GmbH (info@indiscale.com) +# Copyright (C) 2020 Daniel Hornung <d.hornung@indiscale.com> # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -58,6 +59,14 @@ test: script: - make run-qunit +test-server-side-scripting: + timeout: 10 minutes + tags: [ docker ] + stage: test + script: + - whereis pytest pytest3 py.test pytest-3 py.test-3 + - make test-sss + # Trigger building of server image and integration tests trigger_build: timeout: 15 minutes @@ -78,13 +87,33 @@ build-testenv: tags: [ cached-dind ] image: docker:19.03 stage: setup + timeout: 3 h script: - cd test/docker - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY # use here general latest or specific branch latest... - - docker pull $CI_REGISTRY_IMAGE:latest || true - docker build --pull - --cache-from $CI_REGISTRY_IMAGE:latest -t $CI_REGISTRY_IMAGE:latest . - docker push $CI_REGISTRY_IMAGE:latest + +# Build the sphinx documentation and make it ready for deployment by Gitlab Pages +# documentation: +# stage: deploy + +# Special job for serving a static website. See https://docs.gitlab.com/ee/ci/yaml/README.html#pages +pages: + tags: [ docker ] + stage: deploy + only: + - dev + script: + # TODO is this a good location here? + - npm install jsdoc + - npm install jsdoc-sphinx + - echo "Deploying" + - make doc + - rm -r public || true ; cp -r build/doc/html public + artifacts: + paths: + - public diff --git a/CHANGELOG.md b/CHANGELOG.md index a700e0a8670aae69b375e48cbe55d007f5c5f530..985d1541eaeb597156b4ddb3de377b55d71e021c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,51 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed (for now removed features) ### Fixed + +### Security (in case of vulnerabilities) + +## [0.3.0] - 2021-02-10 + +### Added (for new features, dependecies etc.) + +- The versioning model has a new styling and can show and tsv-export the full + version history now. +- Module `ext_bookmarks` which allows users to bookmark entities, store them in + a link or export them to TSV. +- table previews in the bottom line module +- added preview for tif images +* new function `form_elements.make_alert` which generates a proceed/cancel + dialog which intercepts a function call and asks the user for confirmation. +* Deleting entities prompts for user confirmation after hitting the "Delete" + button (edit_mode). +* Plotly preview has an additional parameter for a config object, + e.g., for disabling the plotly logo +- The map can now show entities that have no geo location but are related to + entities that have one. This also effects the search from the map. +* `getPropertyValues` function which generates a table of property values from + a xml representation of entities. +* After a SELECT statement now also all referenced files can be downloaded. +* Automated documentation builds: `make doc` +- documentation on queries + +### Changed (for changes in existing functionality) + +* ext_map version bumped to 0.4 +- enabled and enhanced autocompletion +* Login form is hidden behind another button. + +### Deprecated (for soon-to-be removed features) + +### Removed (for now removed features) + +### Fixed + +- #144 (Select with ANY VERSION OF). +- #136 (adding reference properties to entities in edit mode) - exclude configuration files when reading files from build.properties.d +- summaries when opening preview +- #125 special characters like "\t", \"n", "#" are replaced in table + download ### Security (in case of vulnerabilities) diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md new file mode 100644 index 0000000000000000000000000000000000000000..037dc9973705005ea2ee656e8381444c38ba5c60 --- /dev/null +++ b/DEPENDENCIES.md @@ -0,0 +1,2 @@ +* CaosDB Server == 0.3 +* Make 4.2.0 diff --git a/makefile b/Makefile similarity index 82% rename from makefile rename to Makefile index f0533f8c9f77db23fc69c5312f65493073b8cfb7..1f182e7864a2252e4c1263cde7fa4238d31a4269 100644 --- a/makefile +++ b/Makefile @@ -33,14 +33,17 @@ SQ=\' ROOT_DIR = $(abspath .) MISC_DIR = $(abspath misc) PUBLIC_DIR = $(abspath public) +SSS_BIN_DIR = $(abspath sss_bin) CONF_CORE_DIR = $(abspath conf/core) CONF_EXT_DIR = $(abspath conf/ext) SRC_CORE_DIR = $(abspath src/core) SRC_EXT_DIR = $(abspath src/ext) +SRC_SSS_DIR = $(abspath src/server_side_scripting) LIBS_DIR = $(abspath libs) TEST_CORE_DIR = $(abspath test/core/) TEST_EXT_DIR = $(abspath test/ext) -LIBS = fonts css/bootstrap.css js/bootstrap.js js/state-machine.js js/jquery.js js/showdown.js js/dropzone.js css/dropzone.css js/loglevel.js js/leaflet.js css/leaflet.css css/images js/leaflet-latlng-graticule.js js/proj4.js js/proj4leaflet.js js/leaflet-coordinates.js css/leaflet-coordinates.css js/leaflet-graticule.js js/bootstrap-select.js css/bootstrap-select.css js/bootstrap-autocomplete.min.js js/plotly.js +TEST_SSS_DIR =$(abspath test/server_side_scripting) +LIBS = fonts css/bootstrap.css js/bootstrap.js js/state-machine.js js/jquery.js js/showdown.js js/dropzone.js css/dropzone.css js/loglevel.js js/leaflet.js css/leaflet.css css/images js/leaflet-latlng-graticule.js js/proj4.js js/proj4leaflet.js js/leaflet-coordinates.js css/leaflet-coordinates.css js/leaflet-graticule.js js/bootstrap-select.js css/bootstrap-select.css js/bootstrap-autocomplete.min.js js/plotly.js js/pako.js js/utif.js TEST_LIBS = $(LIBS) js/qunit.js css/qunit.css $(subst $(TEST_CORE_DIR)/,,$(shell find $(TEST_CORE_DIR)/)) @@ -49,9 +52,9 @@ LIBS_SUBDIRS = $(addprefix $(LIBS_DIR)/, js css fonts) ALL: install -install: clean cp-src cp-ext cp-conf $(addprefix $(PUBLIC_DIR)/, $(LIBS)) build_properties merge_xsl +install: clean install-sss cp-src cp-ext cp-conf $(addprefix $(PUBLIC_DIR)/, $(LIBS)) build_properties merge_xsl -test: clean cp-src cp-ext cp-ext-test cp-conf $(addprefix $(PUBLIC_DIR)/, $(TEST_LIBS)) build_properties merge_xsl +test: clean install-sss cp-src cp-ext cp-ext-test cp-conf $(addprefix $(PUBLIC_DIR)/, $(TEST_LIBS)) build_properties merge_xsl @for f in $(shell find $(TEST_EXT_DIR) -type f -iname *.js) ; do \ sed -i "/EXTENSIONS/a \<script src=\"$${f#$(TEST_EXT_DIR)/}\" ></script>" $(PUBLIC_DIR)/index.html ; \ echo include $$f; \ @@ -129,18 +132,33 @@ run-qunit: test exit 1; \ fi +install-sss: + @set -a -e ; \ + pushd build.properties.files ; \ + for f in ../build.properties.d/* ; do source "$$f" ; done ; \ + popd ; \ + ./install-sss.sh $(SRC_SSS_DIR) $(SSS_BIN_DIR) + +PYTEST ?= pytest +PIP ?= pip3 +test-sss: install-sss + $(PIP) freeze + $(PYTEST) -vv $(TEST_SSS_DIR) + + +CMD_COPY_EXT_FILES = cp -i -r -L cp-ext: # TODO FIXME Base path for not-XSL-expanded files mkdir -p $(PUBLIC_DIR)/html for f in $(wildcard $(SRC_EXT_DIR)/html/*) ; do \ - echo "y" | cp -i -r "$$(realpath "$$f")" $(PUBLIC_DIR)/html/ ; \ + echo "y" | $(CMD_COPY_EXT_FILES) "$$(realpath "$$f")" $(PUBLIC_DIR)/html/ ; \ done for f in $(wildcard $(SRC_EXT_DIR)/js/*) ; do \ - echo "y" | cp -i -r "$$f" $(PUBLIC_DIR)/js/ ; \ + echo "y" | $(CMD_COPY_EXT_FILES) "$$f" $(PUBLIC_DIR)/js/ ; \ sed -i "/JS_EXTENSIONS/a \<xsl:element name=\"script\"><xsl:attribute name=\"src\"><xsl:value-of select=\"concat\(\$$basepath, 'webinterface/${BUILD_NUMBER}$${f#$(SRC_EXT_DIR)}'\)\" /></xsl:attribute></xsl:element>" $(PUBLIC_DIR)/xsl/main.xsl ; \ done for f in $(wildcard $(SRC_EXT_DIR)/css/*) ; do \ - echo "y" | cp -i -r "$$f" $(PUBLIC_DIR)/css/ ; \ + echo "y" | $(CMD_COPY_EXT_FILES) "$$f" $(PUBLIC_DIR)/css/ ; \ sed -i "/CSS_EXTENSIONS/a \<xsl:element name=\"link\"><xsl:attribute name=\"rel\">stylesheet</xsl:attribute><xsl:attribute name=\"href\"><xsl:value-of select=\"concat\(\$$basepath, 'webinterface/${BUILD_NUMBER}$${f#$(SRC_EXT_DIR)}'\)\" /></xsl:attribute></xsl:element>" $(PUBLIC_DIR)/xsl/main.xsl ; \ for html in $(PUBLIC_DIR)/html/* ; do \ echo "$$html"; \ @@ -148,26 +166,26 @@ cp-ext: done \ done for f in $(wildcard $(SRC_EXT_DIR)/pics/*) ; do \ - echo "y" | cp -i -r "$$(realpath "$$f")" $(PUBLIC_DIR)/pics/ ; \ + echo "y" | $(CMD_COPY_EXT_FILES) "$$(realpath "$$f")" $(PUBLIC_DIR)/pics/ ; \ done for f in $(wildcard $(SRC_EXT_DIR)/xsl/*) ; do \ - echo "y" | cp -i -r "$$(realpath "$$f")" $(PUBLIC_DIR)/xsl/ ; \ + echo "y" | $(CMD_COPY_EXT_FILES) "$$(realpath "$$f")" $(PUBLIC_DIR)/xsl/ ; \ done cp-ext-test: for f in $(wildcard $(TEST_EXT_DIR)/js/*) ; do \ - echo "y" | cp -i -r "$$f" $(PUBLIC_DIR)/js/ ; \ + echo "y" | $(CMD_COPY_EXT_FILES) "$$f" $(PUBLIC_DIR)/js/ ; \ sed -i "/JS_EXTENSIONS/a \<xsl:element name=\"script\"><xsl:attribute name=\"src\"><xsl:value-of select=\"concat\(\$$basepath, 'webinterface/${BUILD_NUMBER}$${f#$(SRC_EXT_DIR)}'\)\" /></xsl:attribute></xsl:element>" $(PUBLIC_DIR)/xsl/main.xsl ; \ done mkdir -p $(PUBLIC_DIR)/html for f in $(wildcard $(TEST_EXT_DIR)/html/*) ; do \ - echo "y" | cp -i -r "$$(realpath "$$f")" $(PUBLIC_DIR)/html/ ; \ + echo "y" | $(CMD_COPY_EXT_FILES) "$$(realpath "$$f")" $(PUBLIC_DIR)/html/ ; \ done for f in $(wildcard $(TEST_EXT_DIR)/pics/*) ; do \ - echo "y" | cp -i -r "$$(realpath "$$f")" $(PUBLIC_DIR)/pics/ ; \ + echo "y" | $(CMD_COPY_EXT_FILES) "$$(realpath "$$f")" $(PUBLIC_DIR)/pics/ ; \ done for f in $(wildcard $(TEST_EXT_DIR)/xsl/*) ; do \ - echo "y" | cp -i -r "$$(realpath "$$f")" $(PUBLIC_DIR)/xsl/ ; \ + echo "y" | $(CMD_COPY_EXT_FILES) "$$(realpath "$$f")" $(PUBLIC_DIR)/xsl/ ; \ done cp-conf: convert-yaml @@ -193,13 +211,13 @@ $(PUBLIC_DIR)/%: $(TEST_EXT_DIR)/% cp -r $< $@ $(LIBS_DIR)/fonts: unzip - ln -s $(LIBS_DIR)/bootstrap-3.3.7-dist/fonts $@ + ln -s $(LIBS_DIR)/bootstrap-3.4.1-dist/fonts $@ $(LIBS_DIR)/js/bootstrap.js: unzip $(LIBS_DIR)/js - ln -s $(LIBS_DIR)/bootstrap-3.3.7-dist/js/bootstrap.min.js $@ + ln -s $(LIBS_DIR)/bootstrap-3.4.1-dist/js/bootstrap.min.js $@ $(LIBS_DIR)/css/bootstrap.css: unzip $(LIBS_DIR)/css - ln -s $(LIBS_DIR)/bootstrap-3.3.7-dist/css/bootstrap.min.css $@ + ln -s $(LIBS_DIR)/bootstrap-3.4.1-dist/css/bootstrap.min.css $@ $(LIBS_DIR)/js/bootstrap-select.js: unzip $(LIBS_DIR)/js ln -s $(LIBS_DIR)/bootstrap-select-1.13.9/dist/js/bootstrap-select.min.js $@ @@ -208,7 +226,7 @@ $(LIBS_DIR)/css/bootstrap-select.css: unzip $(LIBS_DIR)/css ln -s $(LIBS_DIR)/bootstrap-select-1.13.9/dist/css/bootstrap-select.min.css $@ $(LIBS_DIR)/js/jquery.js: unzip $(LIBS_DIR)/js - ln -s $(LIBS_DIR)/jquery-3.3.1/jquery-3.3.1.min.js $@ + ln -s $(LIBS_DIR)/jquery-3.5.1/jquery-3.5.1.min.js $@ $(LIBS_DIR)/js/showdown.js: unzip $(LIBS_DIR)/js ln -s $(LIBS_DIR)/showdown-1.8.6/dist/showdown.min.js $@ @@ -259,17 +277,24 @@ $(LIBS_DIR)/css/leaflet-coordinates.css: unzip $(LIBS_DIR)/css ln -s $(LIBS_DIR)/Leaflet.Coordinates-0.1.5/dist/Leaflet.Coordinates-0.1.5.css $@ $(LIBS_DIR)/js/bootstrap-autocomplete.min.js: unzip $(LIBS_DIR)/js - ln -s $(LIBS_DIR)/bootstrap-autocomplete-2.3.0/dist/latest/bootstrap-autocomplete.min.js $@ + ln -s $(LIBS_DIR)/bootstrap-autocomplete-2.3.5/dist/latest/bootstrap-autocomplete.min.js $@ $(LIBS_DIR)/js/plotly.js: unzip $(LIBS_DIR)/js ln -s $(LIBS_DIR)/plotly.js-1.52.2/dist/plotly.min.js $@ +$(LIBS_DIR)/js/pako.js: unzip $(LIBS_DIR)/js + ln -s $(LIBS_DIR)/pako-dummy/pako.js $@ + +$(LIBS_DIR)/js/utif.js: unzip $(LIBS_DIR)/js + ln -s $(LIBS_DIR)/UTIF-8205c1f/UTIF.js $@ + $(addprefix $(LIBS_DIR)/, js css): mkdir $@ || true .PHONY: clean clean: + $(RM) -r $(SSS_BIN_DIR) $(RM) -r $(PUBLIC_DIR) for f in $(LIBS_SUBDIRS); do unlink $$f || $(RM) -r $$f || true; done for f in $(patsubst %.zip,%/,$(LIBS_ZIP)); do $(RM) -r $$f; done @@ -280,11 +305,13 @@ unzip: for f in $(LIBS_ZIP); do unzip -q -o -d libs $$f; done -PYLINT = pylint3 -d all -e E,F +PYLINT ?= pylint PYTHON_FILES = $(subst $(ROOT_DIR)/,,$(shell find $(ROOT_DIR)/ -iname "*.py")) pylint: $(PYTHON_FILES) - for f in $(PYTHON_FILES); do $(PYLINT) $$f || exit 1; done + for f in $(PYTHON_FILES); do $(PYLINT) -d all -e E,F $$f || exit 1; done + -PYLINT_LOCAL = /usr/bin/pylint3 -d all -e E,F -pylint-local: $(PYTHON_FILES) - for f in $(PYTHON_FILES); do $(PYLINT_LOCAL) $$f || exit 1; done +# Compile the standalone documentation +.PHONY: doc +doc: + $(MAKE) -C src/doc html diff --git a/README_SETUP.md b/README_SETUP.md index 4706efe28f798a0260b0bc46374491dca7c0d667..485835fdae10239cb4329b0f9b6708c1b020fa2a 100644 --- a/README_SETUP.md +++ b/README_SETUP.md @@ -21,12 +21,16 @@ * ** end header --> -# Folder Structure +# Getting Started with the Web Interface +Here, we document how to install and build the CaosDB Web Interface. If you are +only interested in how to use it, please continue [here](tutorials/first_steps.html) -* The `src` folder contains all source code for the webinterface. +## Folder Structure + +* The `src` folder contains all source code for the web interface. * The `libs` folder contains all necessary third-party libraries as zip files. -* The `test` folder contains the unittests for the webinterface. -* The `ext` folder contains extension for the webinterface. The make file will +* The `test` folder contains the unittests for the web interface. +* The `ext` folder contains extension for the web interface. The make file will copy all javascript files from `ext/js/` into the public folder and links the javascript in the `public/xsl/main.xsl`. * The `misc` folder contains a simple http server which is used for running the @@ -34,7 +38,7 @@ * The `build.properties.d/` folder contains configuration files for the build. -# Build Configuration +## Build Configuration The default configuration is defined in `build.properties.d/00_default.properties`. @@ -46,21 +50,37 @@ All files in that directory will be sourced during `make install` and `make test Thus any customized configuration can also be added to that folder by just placing files in there which override the default values from `00_default.properties`. -See `build.properties.d/00_default.properties` for more -information. +See `build.properties.d/00_default.properties` for more information. -# Setup +## Setup -* Run `make install` to compile/copy the webinterface to a newly created +* Run `make install` to compile/copy the web interface to a newly created `public` folder. +* Also, `make install` will copy the scripts from `src/server_side_scripting/` + to `sss_bin/`. If you want to make the server-side scripts callable for the + server as server-side scripts you need to include the `sss_bin/` directory + into the server property `SERVER_SIDE_SCRIPTING_BIN_DIRS`. -# Test +## Test -* Run `make test` to compile/copy the webinterface and the tests to a newly +* Run `make test` to compile/copy the web interface and the tests to a newly created `public` folder. * Run `make run-test-server` to start a python http server. * The test suite can be started with `firefox http://localhost:8000/`. -# Clean +## Clean * Run `make clean` to clean up everything. + +## Documentation # + +Build documentation in `build/` with `make doc`. + +### Requirements ## + +- sphinx +- sphinx-autoapi +- jsdoc (`npm install jsdoc`) +- jsdoc-sphinx (`npm install jsdoc-sphinx`) +- sphinx-js +- recommonmark diff --git a/References_button.png b/References_button.png new file mode 100644 index 0000000000000000000000000000000000000000..998ef42f7ccb17e32b0c88b8395c249b4529c97f Binary files /dev/null and b/References_button.png differ diff --git a/build.properties.d/00_default.properties b/build.properties.d/00_default.properties index 895820353eba3a8b680df238c2379e0c162f81f6..f6f800227321d557e000cb848d715ce37f9febaf 100644 --- a/build.properties.d/00_default.properties +++ b/build.properties.d/00_default.properties @@ -46,6 +46,11 @@ BUILD_MODULE_EXT_RESOLVE_REFERENCES=ENABLED BUILD_MODULE_EXT_SSS_MARKDOWN=DISABLED BUILD_MODULE_EXT_TRIGGER_CRAWLER_FORM=DISABLED BUILD_MODULE_EXT_ENTITY_STATE=ENABLED +BUILD_MODULE_EXT_AUTOCOMPLETE=ENABLED +BUILD_MODULE_EXT_BOTTOM_LINE=ENABLED +BUILD_MODULE_EXT_BOTTOM_LINE_TABLE_PREVIEW=DISABLED +BUILD_MODULE_EXT_BOTTOM_LINE_TIFF_PREVIEW=DISABLED +BUILD_MODULE_EXT_BOOKMARKS=ENABLED ############################################################################## # Navbar properties diff --git a/install-sss.sh b/install-sss.sh new file mode 100755 index 0000000000000000000000000000000000000000..bb2db57649000ba1e701786f56dba575753110eb --- /dev/null +++ b/install-sss.sh @@ -0,0 +1,17 @@ +SRC_DIR=$1 +INSTALL_DIR=$2 + +mkdir -p $INSTALL_DIR + +# from here on do your module-wise installing + +# ext_table_preview +if [ "${BUILD_MODULE_EXT_BOTTOM_LINE_TABLE_PREVIEW}" == "ENABLED" ]; then + mkdir -p $INSTALL_DIR/ext_table_preview + cp $SRC_DIR/ext_table_preview/*.py $INSTALL_DIR/ext_table_preview/ + echo "installed all server-side scripts for ext_table_preview" +fi +# ext_file_download; should always be installed - No build variable +mkdir -p $INSTALL_DIR/ext_file_download +cp $SRC_DIR/ext_file_download/*.py $INSTALL_DIR/ext_file_download/ +echo "installed all server-side scripts for ext_file_download" diff --git a/libs/UTIF-8205c1f.zip b/libs/UTIF-8205c1f.zip new file mode 100644 index 0000000000000000000000000000000000000000..7069cd288d5d629a7fc3a03d8d92415e78aeb728 Binary files /dev/null and b/libs/UTIF-8205c1f.zip differ diff --git a/libs/bootstrap-3.3.7-dist.zip b/libs/bootstrap-3.3.7-dist.zip deleted file mode 100644 index 6fbb95ebaa3867ce196ef3b54951a732107d94d2..0000000000000000000000000000000000000000 Binary files a/libs/bootstrap-3.3.7-dist.zip and /dev/null differ diff --git a/libs/bootstrap-3.4.1-dist.zip b/libs/bootstrap-3.4.1-dist.zip new file mode 100644 index 0000000000000000000000000000000000000000..9002b8521706bc582546f41635da3437edf20c3c Binary files /dev/null and b/libs/bootstrap-3.4.1-dist.zip differ diff --git a/libs/bootstrap-autocomplete-2.3.0.zip b/libs/bootstrap-autocomplete-2.3.0.zip deleted file mode 100644 index 206c00f49c87794e996c46da2f086d0a1d118071..0000000000000000000000000000000000000000 Binary files a/libs/bootstrap-autocomplete-2.3.0.zip and /dev/null differ diff --git a/libs/bootstrap-autocomplete-2.3.5.zip b/libs/bootstrap-autocomplete-2.3.5.zip new file mode 100644 index 0000000000000000000000000000000000000000..8cc2e03067955193bb89af2972a35ee9e0260b35 Binary files /dev/null and b/libs/bootstrap-autocomplete-2.3.5.zip differ diff --git a/libs/jquery-3.3.1.zip b/libs/jquery-3.3.1.zip deleted file mode 100644 index 404fac0639caf20bd4cf55419aa9a5f5bb768029..0000000000000000000000000000000000000000 Binary files a/libs/jquery-3.3.1.zip and /dev/null differ diff --git a/libs/jquery-3.5.1.zip b/libs/jquery-3.5.1.zip new file mode 100644 index 0000000000000000000000000000000000000000..c554bbf416786da9c18bef62061cba932646f996 Binary files /dev/null and b/libs/jquery-3.5.1.zip differ diff --git a/libs/pako-dummy.zip b/libs/pako-dummy.zip new file mode 100644 index 0000000000000000000000000000000000000000..e493ee9d673c81a523ad8e488c509c392765da52 Binary files /dev/null and b/libs/pako-dummy.zip differ diff --git a/misc/versioning_test_data.py b/misc/versioning_test_data.py index eaa83e46f61ea2f20263b487e4bb42c37678c94f..5ec7073aeaffc894916ee8a6c4cfdc82bc25a4f1 100755 --- a/misc/versioning_test_data.py +++ b/misc/versioning_test_data.py @@ -91,3 +91,8 @@ else: str(rec1.id), str(rec1.id)]) rec4.insert() + +for i in range(4,11): + rec1.name = f"TestRecord1-{i}thVersion" + rec1.description = f"This is the {i}th version." + rec1.update() diff --git a/model.svg b/model.svg new file mode 100644 index 0000000000000000000000000000000000000000..2602cb43f15976305d48e6f2d5efeb3821e1d669 --- /dev/null +++ b/model.svg @@ -0,0 +1,632 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + contentScriptType="application/ecmascript" + contentStyleType="text/css" + height="502" + preserveAspectRatio="none" + version="1.1" + viewBox="0 0 407 502" + width="407" + zoomAndPan="magnify" + id="svg233" + sodipodi:docname="model.svg" + inkscape:version="0.92.4 5da689c313, 2019-01-14"> + <metadata + id="metadata237"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1920" + inkscape:window-height="1043" + id="namedview235" + showgrid="false" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:zoom="1.1817368" + inkscape:cx="112.55875" + inkscape:cy="257" + inkscape:window-x="1920" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="svg233" /> + <defs + id="defs11"> + <filter + height="3" + id="f64vrt8w3qxjw" + width="3" + x="-1" + y="-1"> + <feGaussianBlur + result="blurOut" + stdDeviation="2.0" + id="feGaussianBlur2" /> + <feColorMatrix + in="blurOut" + result="blurOut2" + type="matrix" + values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .4 0" + id="feColorMatrix4" /> + <feOffset + dx="4.0" + dy="4.0" + in="blurOut2" + result="blurOut3" + id="feOffset6" /> + <feBlend + in="SourceGraphic" + in2="blurOut3" + mode="normal" + id="feBlend8" /> + </filter> + </defs> + <rect + style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.13385832;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="rect4867" + width="407" + height="502" + x="0" + y="0" /> + <polygon + id="polygon13" + style="fill:#dddddd;stroke:#000000;stroke-width:1.5;filter:url(#f64vrt8w3qxjw)" + points="533.5,526 126.5,526 126.5,24 236.5,24 243.5,46.2969 533.5,46.2969 " + transform="translate(-126.5,-24)" /> + <line + id="line15" + y2="22.296902" + y1="22.296902" + x2="117" + x1="0" + style="stroke:#000000;stroke-width:1.5" /> + <text + style="font-weight:bold;font-size:14px;font-family:sans-serif;fill:#000000" + id="text17" + y="38.995098" + x="130.5" + textLength="104" + lengthAdjust="spacingAndGlyphs" + font-weight="bold" + font-size="14" + transform="translate(-126.5,-24)">RecordTypes</text> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text42" + y="144.7104" + x="461" + textLength="0" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)" /> + <rect + y="411" + x="16" + width="116" + style="fill:#fefece;stroke:#a80036;stroke-width:1.5;filter:url(#f64vrt8w3qxjw)" + id="Manufacturer" + height="60.804699" /> + <circle + r="11" + id="ellipse47" + style="fill:#ff1111;stroke:#a80036;stroke-width:1" + cy="427" + cx="31" /> + <path + inkscape:connector-curvature="0" + id="path49" + d="m 33.9688,432.6406 q -0.5782,0.2969 -1.2188,0.4375 -0.6406,0.1563 -1.3437,0.1563 -2.5,0 -3.8282,-1.6406 -1.3125,-1.6563 -1.3125,-4.7813 0,-3.125 1.3125,-4.7812 1.3282,-1.6563 3.8282,-1.6563 0.7031,0 1.3437,0.1563 0.6563,0.1562 1.2188,0.4531 v 2.7187 q -0.625,-0.5781 -1.2188,-0.8437 -0.5937,-0.2813 -1.2187,-0.2813 -1.3438,0 -2.0313,1.0782 -0.6875,1.0625 -0.6875,3.1562 0,2.0938 0.6875,3.1719 0.6875,1.0625 2.0313,1.0625 0.625,0 1.2187,-0.2656 0.5938,-0.2813 1.2188,-0.8594 z" /> + <text + style="font-size:12px;font-family:sans-serif;fill:#000000" + id="text51" + y="455.1543" + x="171.5" + textLength="84" + lengthAdjust="spacingAndGlyphs" + font-size="12" + transform="translate(-126.5,-24)">Manufacturer</text> + <line + id="line53" + y2="443" + y1="443" + x2="131" + x1="17" + style="stroke:#a80036;stroke-width:1.5" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text55" + y="481.21039" + x="152.5" + textLength="0" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)" /> + <line + id="line57" + y2="463.80469" + y1="463.80469" + x2="131" + x1="17" + style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" /> + <rect + y="235" + x="16" + width="174" + style="fill:#fefece;stroke:#a80036;stroke-width:1.5;filter:url(#f64vrt8w3qxjw)" + id="MusicalInstrument" + height="101.6211" /> + <circle + r="11" + id="ellipse60" + style="fill:#ff1111;stroke:#a80036;stroke-width:1" + cy="251" + cx="43.600006" /> + <path + inkscape:connector-curvature="0" + id="path62" + d="m 46.5688,256.6406 q -0.5782,0.2969 -1.2188,0.4375 -0.6406,0.1563 -1.3437,0.1563 -2.5,0 -3.8282,-1.6406 -1.3125,-1.6563 -1.3125,-4.7813 0,-3.125 1.3125,-4.7812 1.3282,-1.6563 3.8282,-1.6563 0.7031,0 1.3437,0.1563 0.6563,0.1562 1.2188,0.4531 v 2.7187 q -0.625,-0.5781 -1.2188,-0.8437 -0.5937,-0.2813 -1.2187,-0.2813 -1.3438,0 -2.0313,1.0782 -0.6875,1.0625 -0.6875,3.1562 0,2.0938 0.6875,3.1719 0.6875,1.0625 2.0313,1.0625 0.625,0 1.2187,-0.2656 0.5938,-0.2813 1.2188,-0.8594 z" /> + <text + style="font-size:12px;font-family:sans-serif;fill:#000000" + id="text64" + y="279.1543" + x="186.89999" + textLength="114" + lengthAdjust="spacingAndGlyphs" + font-size="12" + transform="translate(-126.5,-24)">MusicalInstrument</text> + <line + id="line66" + y2="267" + y1="267" + x2="189" + x1="17" + style="stroke:#a80036;stroke-width:1.5" /> + <line + id="line68" + y2="281.40231" + y1="281.40231" + x2="73.5" + x1="17" + style="stroke:#a80036;stroke-width:1.5" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text70" + y="308.71039" + x="200" + textLength="59" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">Properties</text> + <line + id="line72" + y2="281.40231" + y1="281.40231" + x2="189" + x1="132.5" + style="stroke:#a80036;stroke-width:1.5" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text74" + y="341.2222" + x="148.5" + textLength="86" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">price (DOUBLE)</text> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text76" + y="354.02689" + x="148.5" + textLength="162" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">Manufacturer (Manufacturer)</text> + <line + id="line78" + y2="300.60941" + y1="300.60941" + x2="62" + x1="17" + style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text80" + y="327.91751" + x="188.5" + textLength="82" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">recommended</text> + <line + id="line82" + y2="300.60941" + y1="300.60941" + x2="189" + x1="144" + style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" /> + <rect + y="411" + x="167.5" + width="65" + style="fill:#fefece;stroke:#a80036;stroke-width:1.5;filter:url(#f64vrt8w3qxjw)" + id="Violin" + height="60.804699" /> + <circle + r="11" + id="ellipse85" + style="fill:#ff1111;stroke:#a80036;stroke-width:1" + cy="427" + cx="182.5" /> + <path + inkscape:connector-curvature="0" + id="path87" + d="m 185.4688,432.6406 q -0.5782,0.2969 -1.2188,0.4375 -0.6406,0.1563 -1.3437,0.1563 -2.5,0 -3.8282,-1.6406 -1.3125,-1.6563 -1.3125,-4.7813 0,-3.125 1.3125,-4.7812 1.3282,-1.6563 3.8282,-1.6563 0.7031,0 1.3437,0.1563 0.6563,0.1562 1.2188,0.4531 v 2.7187 q -0.625,-0.5781 -1.2188,-0.8437 -0.5937,-0.2813 -1.2187,-0.2813 -1.3438,0 -2.0313,1.0782 -0.6875,1.0625 -0.6875,3.1562 0,2.0938 0.6875,3.1719 0.6875,1.0625 2.0313,1.0625 0.625,0 1.2187,-0.2656 0.5938,-0.2813 1.2188,-0.8594 z" /> + <text + style="font-size:12px;font-family:sans-serif;fill:#000000" + id="text89" + y="455.1543" + x="323" + textLength="33" + lengthAdjust="spacingAndGlyphs" + font-size="12" + transform="translate(-126.5,-24)">Violin</text> + <line + id="line91" + y2="443" + y1="443" + x2="231.5" + x1="168.5" + style="stroke:#a80036;stroke-width:1.5" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text93" + y="481.21039" + x="304" + textLength="0" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)" /> + <line + id="line95" + y2="463.80469" + y1="463.80469" + x2="231.5" + x1="168.5" + style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" /> + <rect + y="397" + x="267.5" + width="119" + style="fill:#fefece;stroke:#a80036;stroke-width:1.5;filter:url(#f64vrt8w3qxjw)" + id="Guitar" + height="88.816399" /> + <circle + r="11" + id="ellipse98" + style="fill:#ff1111;stroke:#a80036;stroke-width:1" + cy="413" + cx="304.54999" /> + <path + inkscape:connector-curvature="0" + id="path100" + d="m 307.5188,418.6406 q -0.5782,0.2969 -1.2188,0.4375 -0.6406,0.1563 -1.3437,0.1563 -2.5,0 -3.8282,-1.6406 -1.3125,-1.6563 -1.3125,-4.7813 0,-3.125 1.3125,-4.7812 1.3282,-1.6563 3.8282,-1.6563 0.7031,0 1.3437,0.1563 0.6563,0.1562 1.2188,0.4531 v 2.7187 q -0.625,-0.5781 -1.2188,-0.8437 -0.5937,-0.2813 -1.2187,-0.2813 -1.3438,0 -2.0313,1.0782 -0.6875,1.0625 -0.6875,3.1562 0,2.0938 0.6875,3.1719 0.6875,1.0625 2.0313,1.0625 0.625,0 1.2187,-0.2656 0.5938,-0.2813 1.2188,-0.8594 z" /> + <text + style="font-size:12px;font-family:sans-serif;fill:#000000" + id="text102" + y="441.1543" + x="449.95001" + textLength="38" + lengthAdjust="spacingAndGlyphs" + font-size="12" + transform="translate(-126.5,-24)">Guitar</text> + <line + id="line104" + y2="429" + y1="429" + x2="385.5" + x1="268.5" + style="stroke:#a80036;stroke-width:1.5" /> + <line + id="line106" + y2="443.40231" + y1="443.40231" + x2="297.5" + x1="268.5" + style="stroke:#a80036;stroke-width:1.5" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text108" + y="470.71039" + x="424" + textLength="59" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">Properties</text> + <line + id="line110" + y2="443.40231" + y1="443.40231" + x2="385.5" + x1="356.5" + style="stroke:#a80036;stroke-width:1.5" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text112" + y="503.2222" + x="400" + textLength="107" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">electric (BOOLEAN)</text> + <line + id="line114" + y2="462.60941" + y1="462.60941" + x2="286" + x1="268.5" + style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text116" + y="489.91751" + x="412.5" + textLength="82" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">recommended</text> + <line + id="line118" + y2="462.60941" + y1="462.60941" + x2="385.5" + x1="368" + style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" /> + <rect + y="255.5" + x="225.5" + width="165" + style="fill:#fefece;stroke:#a80036;stroke-width:1.5;filter:url(#f64vrt8w3qxjw)" + id="SoundQualityAnalyzer" + height="60.804699" /> + <circle + r="11" + id="ellipse121" + style="fill:#ff1111;stroke:#a80036;stroke-width:1" + cy="271.5" + cx="240.5" /> + <path + inkscape:connector-curvature="0" + id="path123" + d="m 243.4688,277.1406 q -0.5782,0.2969 -1.2188,0.4375 -0.6406,0.1563 -1.3437,0.1563 -2.5,0 -3.8282,-1.6406 -1.3125,-1.6563 -1.3125,-4.7813 0,-3.125 1.3125,-4.7812 1.3282,-1.6563 3.8282,-1.6563 0.7031,0 1.3437,0.1563 0.6563,0.1562 1.2188,0.4531 v 2.7187 q -0.625,-0.5781 -1.2188,-0.8437 -0.5937,-0.2813 -1.2187,-0.2813 -1.3438,0 -2.0313,1.0782 -0.6875,1.0625 -0.6875,3.1562 0,2.0938 0.6875,3.1719 0.6875,1.0625 2.0313,1.0625 0.625,0 1.2187,-0.2656 0.5938,-0.2813 1.2188,-0.8594 z" /> + <text + style="font-size:12px;font-family:sans-serif;fill:#000000" + id="text125" + y="299.6543" + x="381" + textLength="133" + lengthAdjust="spacingAndGlyphs" + font-size="12" + transform="translate(-126.5,-24)">SoundQualityAnalyzer</text> + <line + id="line127" + y2="287.5" + y1="287.5" + x2="389.5" + x1="226.5" + style="stroke:#a80036;stroke-width:1.5" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text129" + y="325.71039" + x="362" + textLength="0" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)" /> + <line + id="line131" + y2="308.30469" + y1="308.30469" + x2="389.5" + x1="226.5" + style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" /> + <rect + y="35" + x="20" + width="268" + style="fill:#fefece;stroke:#a80036;stroke-width:1.5;filter:url(#f64vrt8w3qxjw)" + id="Analysis" + height="140.0352" /> + <circle + r="11" + id="ellipse134" + style="fill:#ff1111;stroke:#a80036;stroke-width:1" + cy="51" + cx="124.75" /> + <path + inkscape:connector-curvature="0" + id="path136" + d="m 127.7188,56.6406 q -0.5782,0.2969 -1.2188,0.4375 -0.6406,0.1563 -1.3437,0.1563 -2.5,0 -3.8282,-1.6406 -1.3125,-1.6563 -1.3125,-4.7813 0,-3.125 1.3125,-4.7812 1.3282,-1.6563 3.8282,-1.6563 0.7031,0 1.3437,0.1563 0.6563,0.1562 1.2188,0.4531 v 2.7187 q -0.625,-0.5781 -1.2188,-0.8437 -0.5937,-0.2813 -1.2187,-0.2813 -1.3438,0 -2.0313,1.0782 -0.6875,1.0625 -0.6875,3.1562 0,2.0938 0.6875,3.1719 0.6875,1.0625 2.0313,1.0625 0.625,0 1.2187,-0.2656 0.5938,-0.2813 1.2188,-0.8594 z" /> + <text + style="font-size:12px;font-family:sans-serif;fill:#000000" + id="text138" + y="79.154297" + x="271.75" + textLength="50" + lengthAdjust="spacingAndGlyphs" + font-size="12" + transform="translate(-126.5,-24)">Analysis</text> + <line + id="line140" + y2="67" + y1="67" + x2="287" + x1="21" + style="stroke:#a80036;stroke-width:1.5" /> + <line + id="line142" + y2="81.402298" + y1="81.402298" + x2="124.5" + x1="21" + style="stroke:#a80036;stroke-width:1.5" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text144" + y="108.7104" + x="251" + textLength="59" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">Properties</text> + <line + id="line146" + y2="81.402298" + y1="81.402298" + x2="287" + x1="183.5" + style="stroke:#a80036;stroke-width:1.5" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text148" + y="141.2222" + x="152.5" + textLength="134" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">quality_factor (DOUBLE)</text> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text150" + y="154.0269" + x="152.5" + textLength="92" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">date (DATETIME)</text> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text152" + y="166.8315" + x="152.5" + textLength="111" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">report (REFERENCE)</text> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text154" + y="179.6362" + x="152.5" + textLength="256" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">SoundQualityAnalyzer (SoundQualityAnalyzer)</text> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text156" + y="192.4409" + x="152.5" + textLength="220" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">MusicalInstrument (MusicalInstrument)</text> + <line + id="line158" + y2="100.6094" + y1="100.6094" + x2="113" + x1="21" + style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text160" + y="127.9175" + x="239.5" + textLength="82" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">recommended</text> + <line + id="line162" + y2="100.6094" + y1="100.6094" + x2="287" + x1="195" + style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" /> + <path + inkscape:connector-curvature="0" + style="fill:none;stroke:#a80036;stroke-width:1" + id="MusicalInstrument-Violin" + d="m 145.51,354.27 c 12.48,19.76 25.51,40.37 35.69,56.48" /> + <polygon + id="polygon211" + style="fill:none;stroke:#a80036;stroke-width:1" + points="261.26,361.26 277.86,374.43 266.03,381.91 " + transform="translate(-126.5,-24)" /> + <path + inkscape:connector-curvature="0" + style="fill:none;stroke:#a80036;stroke-width:1" + id="MusicalInstrument-Guitar" + d="m 192.64,348.42 c 25.04,17.17 51.64,35.39 74.51,51.06" /> + <polygon + id="polygon214" + style="fill:none;stroke:#a80036;stroke-width:1" + points="302.54,361.05 322.99,366.58 315.08,378.13 " + transform="translate(-126.5,-24)" /> + <path + inkscape:connector-curvature="0" + style="fill:none;stroke:#a80036;stroke-width:1" + id="MusicalInstrument-Manufacturer" + d="m 91.08,350.09 c -3.97,21 -8.2,43.41 -11.46,60.66" /> + <polygon + id="polygon217" + style="fill:#a80036;stroke:#a80036;stroke-width:1" + points="220,361.26 214.9551,366.4126 217.771,373.0512 222.8159,367.8986 " + transform="translate(-126.5,-24)" /> + <path + inkscape:connector-curvature="0" + style="fill:none;stroke:#a80036;stroke-width:1" + id="Analysis-SoundQualityAnalyzer" + d="m 222.3,185.39 c 21.35,24.82 43.61,50.69 60.09,69.84" /> + <polygon + id="polygon220" + style="fill:#a80036;stroke:#a80036;stroke-width:1" + points="340.04,199.21 340.9231,206.3668 347.869,208.3043 346.9859,201.1475 " + transform="translate(-126.5,-24)" /> + <path + inkscape:connector-curvature="0" + style="fill:none;stroke:#a80036;stroke-width:1" + id="Analysis-MusicalInstrument" + d="m 130.66,187.93 c -4.56,16 -9.21,32.33 -13.37,46.92" /> + <polygon + id="polygon223" + style="fill:#a80036;stroke:#a80036;stroke-width:1" + points="260.78,199.21 255.287,203.8819 257.4868,210.7493 262.9798,206.0774 " + transform="translate(-126.5,-24)" /> +</svg> diff --git a/src/core/css/webcaosdb.css b/src/core/css/webcaosdb.css index 05e36429b7dfe6144915b522de2fa6c36375c484..69a700376423a44bcb28a9920f1f3d15ef9a3b90 100644 --- a/src/core/css/webcaosdb.css +++ b/src/core/css/webcaosdb.css @@ -27,6 +27,55 @@ body { flex-direction: column; } + +div.export-data { + display: none; +} + +tr:not(:hover) .caosdb-v-entity-version-hint-cur { + color: #DDD; +} + +tr:hover .caosdb-v-entity-version-hint { + color: unset; +} + +.caosdb-v-entity-version-hint { + color: #DDD; +} + +tbody:not(:hover) tr .caosdb-v-entity-version-hint-cur { + color: unset; +} + +.caosdb-v-entity-version-no-related { + color: #DDD; +} + +.caosdb-v-entity-version-no-related:hover { + color: unset; +} + +#top-navbar>ul>li>a { + margin: 8px 0px; + padding: 6px 12px; + display: inline-block; +} + +.caosdb-v-bookmark-button, +.caosdb-v-bookmark-button:focus, +.caosdb-v-bookmark-button:hover { + color: #333; + position: relative; + top: 3px; +} + +.caosdb-v-bookmark-button:active, +.caosdb-v-bookmark-button.active { + color: red; +} + + .caosdb-v-navbar-toolbox li a:hover, .caosdb-v-navbar-toolbox li input:hover, .caosdb-v-navbar-toolbox li button:hover { diff --git a/src/core/js/autocomplete.js b/src/core/js/autocomplete.js deleted file mode 100644 index 345d06b37bc6456645ef02947c70b3f2f5d6c44f..0000000000000000000000000000000000000000 --- a/src/core/js/autocomplete.js +++ /dev/null @@ -1,109 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2019 IndiScale GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ - -'use strict'; - -var autocomplete = new function() { - this.version = "0.1"; - - /** - * @type {Promise} promise for a list of entity names. - */ - this.candidates = undefined; - var logger = log.getLogger("autocomplete"); - this.init = function() { - this.candidates = undefined; - this.toggle_completion(); - logger.setLevel("trace"); - }; - - this.retrieve_names = async function () { - var response = $(await connection.get(transaction.generateEntitiesUri( - ["names"]))).find("Response [name]") - - return response.toArray().map(x=> $(x).attr("name")); - }; - - /** - * @return {Promise} promise for a list of entity names. - */ - this.get_candidates = function () { - - if (typeof this.candidates === "undefined"){ - - this.candidates = this.retrieve_names(); - }; - return this.candidates; - }; - - this.filter = function (x, qry){ - return x.toLowerCase().startsWith(qry.toLowerCase()) - }; - - this.search = async function (qry, callback, origJQElement) { - const names = await autocomplete.get_candidates(); - callback(names.filter(x => autocomplete.filter(x, qry))); - }; - - this.typed = function (newValue, origJQElement) { - var cursorpos = origJQElement[0].selectionEnd; - var beginning_of_word = newValue.slice(0, cursorpos).lastIndexOf(" "); - var word = newValue.slice(beginning_of_word+1, cursorpos); - return word; - }; - - this.searchPost = function (resultsFromServer, origJQElement) { - var cursorpos = origJQElement[0].selectionEnd; - var newValue = origJQElement[0].value - var beginning_of_word = newValue.slice(0, cursorpos).lastIndexOf( - " "); - var start = newValue.slice(0, beginning_of_word+1); - var end = origJQElement[0].value.slice(cursorpos); - var result = resultsFromServer.map( x => { - return { - text: start + x + end, - html: x - }}); - return result; - }; - - this.toggle_completion = function () { - var field = $("#caosdb-query-textarea"); - field.toggleClass("basicAutoComplete", true); - field.autoComplete({ - events: { - search: this.search, - typed: this.typed, - searchPost: this.searchPost, - } - } - ); - return field; - }; -}; - -// this will be replaced by require.js in the future. -$(document).ready(function() { - if ("${BUILD_MODULE_EXT_AUTOCOMPLETE}" === "ENABLED") { - caosdb_modules.register(autocomplete); - } -}); diff --git a/src/core/js/caosdb.js b/src/core/js/caosdb.js index 7fe4e9441822bf6c706a09c95aa65f0a2fef06a8..0c63fbe038908903dcf426d1c711d4fad6d3bdd2 100644 --- a/src/core/js/caosdb.js +++ b/src/core/js/caosdb.js @@ -1,30 +1,32 @@ /* -* ** header v3.0 -* This file is a part of the CaosDB Project. -* -* Copyright (C) 2019 IndiScale GmbH -* -* This program is free software: you can redistribute it and/or modify -* it under the terms of the GNU Affero General Public License as -* published by the Free Software Foundation, either version 3 of the -* License, or (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU Affero General Public License for more details. -* -* You should have received a copy of the GNU Affero General Public License -* along with this program. If not, see <https://www.gnu.org/licenses/>. -* -* ** end header -*/ + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2018-2020 Alexander Schlemmer + * Copyright (C) 2018 Research Group Biomedical Physics, + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2019-2020 IndiScale GmbH (info@indiscale.com) + * Copyright (C) 2019-2020 Timm Fitschen (t.fitschen@indiscale.com) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ 'use strict'; /** * JavaScript client for CaosDB - * A. Schlemmer, 08/2018 - * T. Fitschen, 02/2019 * * Dependency: jquery * Dependency: webcaosdb @@ -69,8 +71,7 @@ function getUserRealm() { * @return Array containing the roles of the user. */ function getUserRoles() { - return Array.from(document.getElementsByClassName("caosdb-user-role") - ).map(el => el.innerText); + return Array.from(document.getElementsByClassName("caosdb-user-role")).map(el => el.innerText); } /** @@ -189,9 +190,9 @@ function getPropertyDatatype(element) { x => x.classList.contains("caosdb-property-datatype"), x => x.classList.contains("caosdb-preview-container")); - if(dt_elem.length == 1){ + if (dt_elem.length == 1) { return $(dt_elem[0]).text(); - } else if (dt_elem.length > 1){ + } else if (dt_elem.length > 1) { throw new Error("The datatype of this property could not uniquely be determined."); } @@ -219,14 +220,63 @@ function getEntityName(element) { } /** - * Return the path of element. + * Return the path of an entity. + * + * This attribute is always set for file entities. + * * If the corresponding label can not be found or the label is ambigious undefined is returned. - * @return A string containing the name of the element. + * + * @param {HTMLElement} entity - entity in HTML representation. + * @return A string containing the path of the entity. */ function getEntityPath(element) { + const path = $(element).find('.caosdb-f-entity-path').val(); + if (typeof path !== 'undefined') { + return path; + } + return getEntityHeadingAttribute(element, "path"); } +/** + * Return the checksum of an entity. + * + * This attribute is always set for file entities. + * + * If the corresponding label can not be found or the label is ambigious undefined is returned. + * + * @param {HTMLElement} entity - entity in HTML representation. + * @return A string containing the checksum of the entity. + */ +function getEntityChecksum(element) { + const checksum = $(element).find('.caosdb-f-entity-checksum').val(); + if (typeof checksum !== 'undefined') { + return checksum; + } + + return getEntityHeadingAttribute(element, "checksum"); +} + +/** + * Return the size of element. This attribute is always set for file entities. + * If the corresponding label can not be found or the label is ambigious undefined is returned. + * @return A string containing the size of the element. + */ +function getEntitySize(element) { + // TODO: check if this if block is needed + // it is analogous to getEntityDescription + // if ($(element).find('[data-entity-size]').length == 1) { + // return $(element).find('[data-entity-size]')[0].dataset.entitySize; + // } + + if (typeof $(element).find('.caosdb-f-entity-size').val() !== 'undefined') { + // This is needed for the edit mode to work properly: + return $(element).find('.caosdb-f-entity-size').val(); + } + + return getEntityHeadingAttribute(element, "size"); +} + /** * Return the id of an entity. * @param element The element holding the entity. @@ -286,6 +336,32 @@ function input2caosdbDate(date, time) { return date + "T" + time; } +/** + * Return true if the current user has the given permission for the given + * entity. + * + * @param {HTMLElement} entity + * @return {boolean} + */ +var hasEntityPermission = function (entity, permission) { + if (userHasRole("administration")) { + // administration is a special role. It has * permissions. + return true; + } + const permissions = getAllEntityPermissions(entity); + return permissions.indexOf(permission.toUpperCase()) > -1; +} + +/** + * Get all permissions the current user has for this entity. + * @param {HTMLElement} entity + * @return {string[]} array of permissions. + */ +var getAllEntityPermissions = function (entity) { + const permissions = $(entity).find("[data-permission]").toArray().map(x => x.getAttribute("data-permission")); + return permissions; +} + /** * Take a datetime from caosdb and return a date and a time * suitable for html inputs. @@ -355,6 +431,7 @@ function getEntityDescription(element) { if ($(element).find('[data-entity-description]').length == 1) { return $(element).find('[data-entity-description]')[0].dataset.entityDescription; } else if (typeof $(element).find('.caosdb-f-entity-description').val() !== 'undefined') { + // This is needed for the edit mode to work properly: return $(element).find('.caosdb-f-entity-description').val(); } @@ -423,7 +500,7 @@ function getEntityXML(ent_element) { function getPropertyName(element) { var name_element = element.getElementsByClassName("caosdb-property-name"); - if(name_element.length > 0) { + if (name_element.length > 0) { return name_element[0].textContent; } else if ($(element).is("[data-property-name]")) { return $(element).attr("data-property-name"); @@ -522,7 +599,7 @@ function getPropertyFromElement(propertyelement, names = undefined) { var value_string = undefined; if (valel && valel.textContent.length > 0) { value_string = valel.textContent; - } else if (valel && valel.value && valel.value.length > 0 ) { + } else if (valel && valel.value && valel.value.length > 0) { value_string = valel.value; } @@ -539,7 +616,7 @@ function getPropertyFromElement(propertyelement, names = undefined) { if (typeof value_string !== "undefined") { // This is set to true, when there is a reference or a list of references: - if(typeof property.reference === "undefined") { + if (typeof property.reference === "undefined") { property.reference = (valel.getElementsByClassName("caosdb-id").length > 0); } @@ -620,6 +697,91 @@ function getProperties(element) { return list; } + +/** + * Construct XPath expression from selectors. + * + * Used by getPropertyValues. + * + * @param {String[][]} selectors + * @return {String[]} XPath expressions. + */ +var _constructXpaths = function (selectors) { + const xpaths = []; + for (let sel of selectors) { + var expr = "Property"; + if (sel[0] == "id") { + expr = ""; + } + for (let i = 0; i < sel.length; i++) { + const segment = sel[i]; + if (segment == "id") { + expr += `@id`; + } else if (segment) { + expr += `[@name='${segment}']`; + } + + if (i+1 < sel.length) { + expr += "//Property" + } + } + xpaths.push(expr); + } + return xpaths; +} + +/** + * Return a table where each row represents an entity and each column a property. + * + * This also works for entities from select queries, where the properties + * are deeply nested, e.g. when each entity references a "Geo Location" + * record which have latitude and longitude properties: + * + * `getPropertyValues(entities, [["Geo Location", "latitude"], ["Geo Location", "longitude"]])` + * + * Use empty strings for selector elements when the property name is irrelevant: + * + * `getPropertyValues(entities, [["", "latitude"], ["", "longitude"]])` + * + * Limitations: + * + * 1. Currently, this implementation assumes that properties (and subproperties + * for that matter) have unique names, entity-wide and do have a LIST + * datatype. + * + * 2. It only handles one of the many special cases, which is "id". Other + * special cases ("name", "description", "unit", etc.) are to be added when + * needed. + * + * @param {XMLElement[]) entities + * @param {String[][]} selectors + * @return {String[][]} A table of the property values for each entity. + */ +var getPropertyValues = function (entities, selectors) { + const entity_iter = entities.evaluate("/Response/Record", entities); + + const table = []; + const xpaths = _constructXpaths(selectors) + + var current_entity = entity_iter.iterateNext(); + while (current_entity) { + const row = []; + for (let expr of xpaths) { + const property = entities.evaluate(expr, current_entity).iterateNext(); + if (typeof property != "undefined" && property != null) { + row.push(property.textContent.trim()); + } else { + row.push(undefined) + } + + } + table.push(row); + current_entity = entity_iter.iterateNext(); + } + + return table; +} + /** * Sets a property with some basic type checking. * @@ -663,8 +825,8 @@ function setPropertySafe(valueelement, property, propold) { } } else { /* DEPRECATED css class .caosdb-property-text-value - Use - * .caosdb-f-property-single-raw-value or introduce new - * .caosdb-v-property-text-value */ + * .caosdb-f-property-single-raw-value or introduce new + * .caosdb-v-property-text-value */ valueelement.innerHTML = "<span class='caosdb-property-text-value'>" + property.value + "</span>"; } } @@ -711,7 +873,7 @@ function setProperty(element, property) { * equivalent). * @returns {string} The value of the the property with property_name or `undefined` when this property is not available for this entity. */ -function getProperty(element, property_name, case_sensitive=true) { +function getProperty(element, property_name, case_sensitive = true) { var props; if (case_sensitive) { props = getProperties(element).filter(el => el.name == property_name); @@ -808,7 +970,7 @@ function appendProperty(doc, element, property, append_datatype = false) { * * @param {string} root - the new root element. * @returns {(Document|DocumentFragement)} the new document. - */ + */ function _createDocument(root) { var doc = undefined; if (window.DocumentFragment) { @@ -827,17 +989,20 @@ function _createDocument(root) { * This function uses the object notation. * @see getProperties * @see getParents - * @param role Record, RecordType or Property + * @param role Record, RecordType, Property or File (in case of files the three file arguments must be used!) * @param name The name of the entity. Can be undefined. * @param id The id of the entity. Can be undefined. * @param properties A list of properties. * @param parents A list of parents. + * @param description A description for this entity. * @return {Document|DocumentFragment} - An xml document holding the newly * created entity. * */ function createEntityXML(role, name, id, properties, parents, - append_datatypes = false, datatype = undefined, description = undefined, unit = undefined) { + append_datatypes = false, datatype = undefined, description = undefined, + unit = undefined, + file_path = undefined, file_checksum = undefined, file_size = undefined) { var doc = _createDocument(role); var nelnode = doc.children[0]; @@ -869,9 +1034,49 @@ function createEntityXML(role, name, id, properties, parents, appendProperty(doc, nelnode, properties[i], append_datatypes); } } + + if (role.toLowerCase() == "file") { + /* + File path, checksum and size are needed for File entities. + + An error is raised when these arguments are not set. + */ + if (file_path === undefined || file_checksum === undefined || file_size === undefined) { + throw "Path, checksum and size must not be undefined in case of file entities."; + } + + $(nelnode).attr("path", file_path); + $(nelnode).attr("checksum", file_checksum); + $(nelnode).attr("size", file_size); + } return doc; } +/** + * Create an XML for a file entity. + * This is a convenience function for creating XML from file entities. + * This function uses the object notation. + * @see getProperties + * @see getParents + * @param name The name of the entity. Can be undefined. + * @param id The id of the entity. Can be undefined. + * @param parents A list of parents. + * @param file_path The path of the file in the CaosDB file system. + * @param file_checksum The checksum of the file. + * @param file_size The size of the file in bytes. + * @param description A description for this entity. + * @return {Document|DocumentFragment} - An xml document holding the newly + * created entity. + * + */ +function createFileXML(name, id, parents, + file_path, file_checksum, file_size, + description = undefined) { + return createEntityXML("File", name, id, {}, parents, + false, undefined, description, undefined, + file_path, file_checksum, file_size); +} + /** * Helper function to wrap xml documents into another node which could e.g. be * Update, Response, Delete. @@ -885,7 +1090,7 @@ function wrapXML(root, xmls) { caosdb_utils.assert_string(root, "param `root`"); var doc = _createDocument(root); - for (var i=0; i < xmls.length; i++) { + for (var i = 0; i < xmls.length; i++) { doc.firstElementChild.appendChild(xmls[i].firstElementChild); } diff --git a/src/core/js/edit_mode.js b/src/core/js/edit_mode.js index 0c52f403d5d9b2f0b2feb60a9dc21403c12dc0b2..9df7505f1e81fd0234aaa4461444535846921885 100644 --- a/src/core/js/edit_mode.js +++ b/src/core/js/edit_mode.js @@ -42,6 +42,11 @@ var edit_mode = new function() { */ this.start_edit = new Event("caosdb.edit_mode.start_edit") + /** + * Fired by the entity element after editing (canceled or saved). + */ + this.end_edit = new Event("caosdb.edit_mode.end_edit") + /** * Fired by a list-property when an (input) element is added to the list during editing. */ @@ -407,8 +412,20 @@ var edit_mode = new function() { */ this.form_to_xml = function(entity_form) { const obj = form_elements.form_to_object($(entity_form).find("form")[0]); + var entityRole = getEntityRole(entity_form); + var file_path = undefined; + var file_checksum = undefined; + var file_size = undefined; + if (entityRole.toLowerCase() == "file") { + file_path = getEntityPath(entity_form); + file_checksum = getEntityChecksum(entity_form); + file_size = getEntitySize(entity_form); + console.log(file_path); + console.log(file_checksum); + console.log(file_size); + } return createEntityXML( - getEntityRole(entity_form), + entityRole, getEntityName(entity_form), getEntityID(entity_form), edit_mode.getProperties(entity_form), @@ -417,6 +434,7 @@ var edit_mode = new function() { edit_mode.get_datatype_str(obj), getEntityDescription(entity_form), obj.unit, + file_path, file_checksum, file_size ); } @@ -625,6 +643,8 @@ var edit_mode = new function() { header.attr("title", ""); } else if (getEntityRole(roleElem[0]) == "File") { inputs.push(this.make_input("path", getEntityPath(entity))); + inputs.push(this.make_input("checksum", getEntityChecksum(entity))); + inputs.push(this.make_input("size", getEntitySize(entity))); } // remove other stuff header.children().remove(); @@ -1188,7 +1208,7 @@ var edit_mode = new function() { */ this.delete_action = async function(entity) { var app = edit_mode.app; - // this is the callback of the delete button + // show waiting notification edit_mode.smooth_replace(entity, app.waiting); // send delete request @@ -1533,6 +1553,7 @@ var edit_mode = new function() { } hintMessages.hintMessages(app.entity); edit_mode.unfreeze(); + app.entity.dispatchEvent(edit_mode.end_edit); resolve_references.init(); preview.init(); } @@ -1780,7 +1801,24 @@ var edit_mode = new function() { $(entity).find(".caosdb-f-edit-mode-entity-actions-panel").append(button); $(button).click(() => { - edit_mode.delete_action(entity); + // hide other elements + const _alert = form_elements.make_alert({ + title: "Warning", + message: "You are going to delete this entity permanently. This cannot be undone.", + proceed_callback: () => { + edit_mode.delete_action(entity) + }, + cancel_callback: () => { + $(_alert).remove(); + $(entity).find(".caosdb-f-edit-mode-entity-actions-panel").show(); + }, + proceed_text: "Yes, delete!", + remember_my_decision_id : "delete_entity", + }); + $(_alert).addClass("text-right"); + + $(entity).find(".caosdb-f-edit-mode-entity-actions-panel").after(_alert).hide(); + }); } diff --git a/src/core/js/ext_autocomplete.js b/src/core/js/ext_autocomplete.js new file mode 100644 index 0000000000000000000000000000000000000000..9d99241485245ca6232a7626c04f7998579174e8 --- /dev/null +++ b/src/core/js/ext_autocomplete.js @@ -0,0 +1,161 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2019 IndiScale GmbH, Henrik tom Wörden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ + +/* + * This module uses and extends bootstrap-autocomplete to provide + * autocompletion to the query field. + * Currently, completion exists for names that are stored in CaosDB + */ +'use strict'; + +var ext_autocomplete = new function () { + this.CQL_WORDS = [ + "FIND", + "FILE", + "ENTITY", + "SELECT", + "COUNT", + "RECORD", + "PROPERTY", + "RECORDTYPE", + "REFERENCES", + "REFERENCED BY", + "WHICH", + "WITH", + "CREATED BY", + "CREATED ON", + "SOMEONE", + "STORED AT", + "HAS A PROPERTY", + "HAS BEEN", + "ANY VERSION OF", + ]; + this.version = "0.1"; + + + /** + * configure logging + */ + var logger = log.getLogger("ext_autocomplete"); + + /** + * Initialize this module. + * + * Fetch candidates and prepare bootstrap-autocomplete. + */ + this.init = async function () { + this.candidates = await this.retrieve_names(); + this.switch_on_completion(); + }; + + + /** + * creates a list of names from the server resource Entity/names + */ + this.retrieve_names = async function () { + var response = $(await connection.get(transaction.generateEntitiesUri( + ["names"]))).find("Property[name],RecordType[name],Record[name]") + + response = response.toArray().map(x => $(x).attr("name")); + response = response.concat(ext_autocomplete.CQL_WORDS); + + return response + }; + + /** + * case insensitive filter that returns elements that start with qry + */ + this.starts_with_filter = function (x, qry) { + return x.toLowerCase().startsWith(qry.toLowerCase()) + }; + + /** + * Overwrites the default search function of bootstrap-autocomplete + * uses candidates and filters the list using filter + */ + this.search = async function (qry, callback, origJQElement) { + callback(ext_autocomplete.candidates.filter( + x => ext_autocomplete.starts_with_filter(x, qry))); + }; + + /** + * Overwrites the default typed function of bootstrap-autocomplete + * This assures that only the word before the cursor is used for completion + */ + this.typed = function (newValue, origJQElement) { + var cursorpos = origJQElement[0].selectionEnd; + var beginning_of_word = newValue.slice(0, cursorpos).lastIndexOf(" "); + var word = newValue.slice(beginning_of_word + 1, cursorpos); + return word; + }; + + /** + * Overwrites the default searchPost function of bootstrap-autocomplete + * This assures that only the word before the cursor is replaced. + * It is achieved by returning not a simple list, but an object where each + * element has "text" and "html" fields, where "html" is what is visible + * in the dropdown and "text" will be inserted. + */ + this.searchPost = function (resultsFromServer, origJQElement) { + var cursorpos = origJQElement[0].selectionEnd; + var newValue = origJQElement[0].value + var beginning_of_word = newValue.slice(0, cursorpos).lastIndexOf( + " "); + var start = newValue.slice(0, beginning_of_word + 1); + var end = origJQElement[0].value.slice(cursorpos); + var result = resultsFromServer.map(x => { + return { + text: start + x + end, + html: x + } + }); + return result; + }; + + /** + * enables autocompletion in the query field + */ + this.switch_on_completion = function () { + var field = $("#caosdb-query-textarea"); + field.attr("autocomplete", "off"); + field.toggleClass("basicAutoComplete", true); + field.autoComplete({ + events: { + search: this.search, + typed: this.typed, + searchPost: this.searchPost, + }, + noResultsText: 'No autocompletion suggestions', + bootstrapVersion: "3", + + }); + + return field; + }; +}; + +// this will be replaced by require.js in the future. +$(document).ready(function () { + if ("${BUILD_MODULE_EXT_AUTOCOMPLETE}" == "ENABLED") { + caosdb_modules.register(ext_autocomplete); + } +}); diff --git a/src/core/js/ext_bookmarks.js b/src/core/js/ext_bookmarks.js new file mode 100644 index 0000000000000000000000000000000000000000..d99ff362d8a26ee4818a76a624c80f55f757c419 --- /dev/null +++ b/src/core/js/ext_bookmarks.js @@ -0,0 +1,730 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ + +'use strict'; + + +/** + * Keep track of bookmarked entities and provide functions for export, viewing + * all entities and resetting the bookmarks. + * + * @module ext_bookmarks + * @version 0.2 + * + * @param jQuery - well-known library. + * @param log - singleton from loglevel library or javascript console. + * @param {ext_bookmarks_config} config + * + * @type ext_bookmarks_config + * @property {function} [get_context_root] - param-less function which returns the + * uri of the collection resource which returns the bookmarked entities. + * @property {string} [data_attribute=data-bmval] - name of the data-attribute + * which contains the bookmark key. + * @property {Storage} [bookmark_storage=localStorage] - the Storage + * implementation which is used for storing bookmarks. + * @property {function} [set_collection_link] - a function which sets the + * collection link. + * @property {function} [set_counter] - a function which sets the counter + * @property {function} [getPaging] - a function which returns the paging string + * @property {function} [set_export_button_click] - a function which sets the click + * event handler of the export button. + * @property {function} [set_clear_button_click] - a function which sets the click + * event handler of the clear button. + * @property {string[]} [tsv_columns=["URI"]] + * @property {function} [get_export_table_row] - function which receives the + * bookmarked id and must return an array of columns. + * @property {object} [data_getters={}] - a dictionary of functions which + * retrieve bookmark data for a given bookmark id and a data_key. + * @property {string[]} [data_no_cache] - an array of data_keys which are not + * to be cached. + */ +var ext_bookmarks = function ($, logger, config) { + + config = config || {}; + + /** + * This counter is used as a cache for the number of current bookmarks. It + * is used to quickly change the counter in the bookmarks menu and lazily + * updated. + */ + var counter = 0; + + /** + * Currently this is mainly usefull for testing, but in the future it might + * be desirable to have multiple bookmark collection at the same time. It + * would be easy to extent this class for this because the collection_id is + * used in the generated links and as part of the storage key in the + * bookmark_storage. + */ + var collection_id = config["collection_id"] || 0; + + const data_getters = config["data_getters"] || { + "URI": (id) => get_context_root() + id + }; + const data_no_cache = config["data_no_cache"] || ["URI"]; + + const data_attribute = config["data_attribute"] || "data-bmval"; + + /** + * Return all bookmark buttons on this page or which are children of scope. + * + * @param {HTMLElement|string} [scope='body'] + * @return {HTMLElement[]} array of bookmark buttons. + */ + const get_bookmark_buttons = function (scope) { + return $(scope || "body").find(`[${data_attribute}]`).toArray(); + } + + /** + * Sets the click event handler of the clear button. + * + * @param {function} cb - event handler. + */ + const set_clear_button_click = config["set_clear_button_click"] || function (cb) { + $("#caosdb-f-bookmarks-clear") + .toggleClass("disabled", !cb) + .on("click", cb); + } + + /** + * Sets the click event handler of the export button. + * + * @param {function} cb - event handler. + */ + const set_export_button_click = config["set_export_button_click"] || function (cb) { + $("#caosdb-f-bookmarks-export-link") + .toggleClass("disabled", !cb) + .on("click", cb); + } + + const getPaging = config["getPaging"] || (() => "?P=0L10"); + + /** + * The storage backend for the bookmarks. + */ + const bookmark_storage = config["bookmark_storage"] || window.localStorage; + + /** + * Set the href attribute of the bookmark collection link. + * + * @param {string} uri + */ + const set_collection_link = config["set_collection_link"] || function (uri) { + const link = $("#caosdb-f-bookmarks-collection-link") + .toggleClass("disabled", !uri) + .find("a"); + if (uri) { + link.attr("href", uri); + } else { + link.removeAttr("href"); + } + } + + /** + * Set the counter badge in the bookmark menu. + */ + const update_counter = config["set_counter"] || function (counter) { + $("#caosdb-f-bookmarks-collection-counter").text(counter); + } + + const get_context_root = config["get_context_root"] || (() => ""); + + /** + * This is used as a prefix of the key in the bookmark_storage. + */ + const key_prefix = "_bm_"; + + /** + * This marker is used to identify uris which specify a bookmark collection + * which should be reloaded. + */ + const uri_marker = "_bm_"; + + /** + * Extract the bookmark id from the bookmark button. + * + * @param {HTMLElement} button - the bookmark button. + * @return {string} the bookmark id. + */ + const get_value = function (button) { + const result = $(button).attr(data_attribute); + return result; + } + + /** + * Construct the prefix of the key for the bookmark_storage. + * + * This can be used to construct the item key and the data key + * to delete all storage keys which belong to the current bookmark + * collection. + * + * @param {string} + */ + const get_collection_prefix = function () { + return key_prefix + collection_id; + } + + /** + * Generate the key for the bookmark_storage. + * + * @param {string} val - the value which is used to generate the key. + */ + const get_key = function (val) { + return get_collection_prefix() + '_it_' + val; + } + + + /** + * These will be the columns in the TSV file. For each column there should + * exist a data_getter. + */ + const tsv_columns = config["tsv_columns"] || ["URI"]; + + /** + * Generate a single TSV row + * + * @return {string[]} array of row columns + */ + const get_export_table_row = async function (id) { + const row = []; + for (var col of tsv_columns) { + row.push(await get_bookmark_data(id, col)); + } + return row; + } + + /** + * Generate the TSV data for the export callback with all current + * bookmarks. + * + * TODO merge with caosdb_utils.create_tsv_table. + * + * @param {string[]} bookmarks - array of ids. + * @param {string} [preamble="data:text/csv;charset=utf-8,"] - the preamble + * which is used for generating tables which can be downloaded by + * browsers. + * @param {string} [tab="%09"] - the tab string. + * @param {string} [newline="%0A"] - the newline string. + * @param {string[]} [leading_comments] - comment lines which are to be put + * even before the header line. They should be appropriately escaped (e.g. + * with "%23"). + */ + const get_export_table = async function (bookmarks, preamble, tab, newline, leading_comments) { + // TODO merge with related code in the module "caosdb_table_export". + preamble = ((typeof preamble == 'undefined') ? "data:text/csv;charset=utf-8,": preamble); + tab = tab || "%09"; + newline = newline || "%0A"; + leading_comments = (leading_comments ? leading_comments.join(newline) + newline : ""); + const header = leading_comments + tsv_columns.join(tab) + newline; + const rows = []; + for (let i = 0; i < bookmarks.length; i++) { + rows.push((await get_export_table_row(bookmarks[i])).join(tab)); + } + return `${preamble}${header}${rows.join(newline)}`; + } + + /** + * Download the table with a given filename. + * + * This method adds a temporay <A> element to the dom tree and triggers + * "click" because otherwise the filename cannot be set. + * + * See also: + * https://stackoverflow.com/questions/21177078/javascript-download-csv-as-file + */ + const download = function (table, filename) { + console.log("download"); + const link = $(`<a style="display: none" download="${filename}" href="${table}"/>`); + $("body").append(link); + link[0].click(); + link.remove(); + } + + /** + * Trigger the download of the TSV table with all current bookmarks. + * + * This is the call-back for the export button. + */ + const export_bookmarks = async function () { + const ids = get_bookmarks(); + const versioned_bookmarks = [] + for (let id of ids) { + if (id.indexOf("@") > -1) { + versioned_bookmarks.push(id); + } else { + versioned_bookmarks.push(id + "@" + (await get_bookmark_data(id, "Version"))); + } + } + const uri = get_collection_link(ids); + const leading_comments = [encodeURIComponent(`#Link to all entities: ${uri}`)]; + const export_table = await get_export_table(ids, undefined, undefined, undefined, leading_comments); + download(export_table, "bookmarked_entities.tsv"); + } + + /** + * Return all current bookmarks. + * + * @return {string[]} array of bookmarked ids. + */ + const get_bookmarks = function () { + const result = []; + + const storage_key_prefix = get_key(""); + for (let i = 0; i < bookmark_storage.length; i++) { + const key = bookmark_storage.key(i); + if (key.indexOf(storage_key_prefix) > -1) { + result.push(bookmark_storage[key]); + } + } + return result; + } + + /** + * Update the clear button (i.e. add a click handler which resets the bookmarks.) + * + * @param {string[]} bookmarks - array of ids. + */ + const update_clear_button = function (bookmarks) { + if (bookmarks.length > 0) { + set_clear_button_click(clear_bookmark_storage); + } else { + set_clear_button_click(false); + } + } + + /** + * Update the export button (i.e. add a click handler which generates the + * tsv file.) + * + * @param {string[]} bookmarks - array of ids. + */ + const update_export_link = function (bookmarks) { + if (bookmarks.length > 0) { + set_export_button_click(export_bookmarks); + } else { + set_export_button_click(false); + } + } + + /** + * Generate the uri for the collection of all bookmarked entities. + * + * @param {string[]} bookmarks - array of ids. + * @return {string} uri + */ + const get_collection_link = function (bookmarks) { + const uri_segment = bookmarks.join("&"); + return get_context_root() + uri_segment + getPaging() + + "#" + uri_marker + collection_id; + } + + /** + * Update the link of the collection of bookmarks in the bookmark drop down + * menu. + * + * @param {string[]} bookmarks - array of ids. + */ + const update_collection_link = function (bookmarks) { + if (bookmarks.length > 0) { + const link = get_collection_link(bookmarks); + set_collection_link(link); + } else { + set_collection_link(false); + } + } + + /** + * Syncronize the bookmark_storage and currently visible bookmark button and + * update all buttons and other visible elements and the bookmarks drop + * down menu. + * + * @param {string[]} [bookmarks] - array of ids. If omitted, the + * get_bookmarks function is being called. + */ + const update_collection = function (bookmarks) { + bookmarks = bookmarks || get_bookmarks(); + update_counter(bookmarks.length); + update_collection_link(bookmarks); + update_export_link(bookmarks); + update_clear_button(bookmarks); + } + + /** + * Toggle the active class of the button and change the title of the button + * accordingly. + * + * @param {HTMLElement} button - bookmark button + * @param {boolean} is_active - whether the new state is active or not. + */ + const set_button_state = function (button, is_active) { + $(button).toggleClass("active", is_active); + if (is_active) { + $(button).attr("title", "Remove bookmark"); + } else { + $(button).attr("title", "Add bookmark"); + } + } + + /** + * Event handler for the click event of the bookmark buttons. + * + * Toggles the buttons state and adds or removes the bookmark. + * + * @param {Event} e - the click event; + */ + const toggle_bookmark_active = function (e) { + const button = $(this); + + const new_is_active = !button.is(".active"); + set_button_state(button, new_is_active); + + const value = get_value(button[0]); + const key = get_key(value); + if (new_is_active) { + bookmark_storage.setItem(key, value); + update_counter(++counter); + + // fill the cache immediately. This is a good idea, because many + // data_getters can work on the DOM tree when the bookmark is being + // selected. Later, when the user has left the current page, the + // getters might need to request the database. We want to prevent + // that. + collect_bookmark_data(value); + } else { + bookmark_storage.removeItem(key); + update_counter(--counter); + remove_bookmark_data(value); + } + update_collection(); + } + + /** + * Fill the cache with data for the export for a bookmark. + * + * @param {string} id - bookmark id. + */ + const collect_bookmark_data = function (id) { + for (let data_key in data_getters) { + if (data_no_cache.indexOf(data_key) == -1) { + // do nothing, only trigger the fetching + get_bookmark_data(id, data_key) + } + } + } + + /** + * Remove all data item which belong to a bookmark. + * + * @param {string} id - bookmark id. + */ + const remove_bookmark_data = function (id) { + const data_key_prefix = get_data_key(id, ""); + remove_from_storage_by_prefix(data_key_prefix); + } + + /** + * Initialize a single bookmark button. + * + * Fetch the state from the bookmark_storage and set the bookmark button to + * active or inactive. Also add the onclick handler which toggles the + * bookmark state. + * + * @param {HTMLElement} button - The bookmark button + */ + const init_button = function (button) { + // load state + const key = get_key(get_value(button)); + const is_bookmarked = !!key && !!bookmark_storage[key]; + set_button_state(button, is_bookmarked); + + // onlick handler + button.onclick = toggle_bookmark_active; + } + + /** + * Remove all items in the bookmark_storage by a prefix. + * + * Useful for resetting the whole bookmark_storage or just deleting a + * single item along with its data items. + * + * @param {string} prefix + */ + const remove_from_storage_by_prefix = function (prefix) { + const keys = []; + for (let i = 0; i < bookmark_storage.length; i++) { + const key = bookmark_storage.key(i); + if (key.indexOf(prefix) > -1) { + keys.push(key); + } + } + for (let i = 0; i < keys.length; i++) { + bookmark_storage.removeItem(keys[i]); + } + } + + /** + * Remove all bookmarks, clear the counter and reset the buttons + */ + const clear_bookmark_storage = function () { + counter = 0; + update_collection([]); + + // reset all buttons + get_bookmark_buttons().forEach((x) => { + set_button_state(x, false); + }); + + const storage_key_prefix = get_collection_prefix() + remove_from_storage_by_prefix(storage_key_prefix); + } + + /** + * Add all bookmarks to storage. + * + * @param {string[]} ids - an array of ids. + */ + const add_all_bookmarks_to_storage = function (ids) { + counter = counter + ids.length; + for (let i = 0; i < ids.length; i++) { + const id = ids[i]; + if (id) { + bookmark_storage[get_key(id)] = id + } + } + update_collection(); + } + + /** + * Parse a list of bookmarked entities and the collection_id from and URI. + * + * @param {string} uri + * @return {object} dict with two keys: + * {string[]} bookmarks + * {string} _collection_id + */ + const parse_uri = function (uri) { + var cut_index = uri.indexOf("#" + uri_marker); + if (cut_index > -1) { + // get collection id + const _collection_id = uri + .substring(cut_index + uri_marker.length + 1) + // remove query + const query_marker = uri.indexOf("?"); + if (query_marker > -1) { + cut_index = query_marker; + } + const uri_segments = uri.substring(0, cut_index).split("/") + const ids = uri_segments[uri_segments.length - 1].split("&"); + logger.debug("found ids in uri", ids); + return { + bookmarks: ids, + collection_id: _collection_id + }; + } + return undefined; + } + + + /** + * Initialize all bookmark buttons which are children of scope. + * + * @param {HTMLElement|string} [scope="body"] - element or jquery selector. + */ + const init_bookmark_buttons = function (scope) { + logger.trace("enter init_bookmark_buttons", scope); + $(get_bookmark_buttons(scope)).each((idx, button) => { + init_button(button); + }); + } + + /** + * Setter for the collection_id. + * + * @param {string} id + */ + const set_collection_id = function (id) { + collection_id = id; + } + + /** + * Initialize this module. + */ + const init = async function (scope) { + logger.info("init ext_bookmarks"); + //add_bookmark_buttons(); + counter = 0; + const parsed_uri = parse_uri(window.location.href); + if (typeof parsed_uri != "undefined") { + // this hack removes the "#_bm" marker from the uri without + // reloading the page. + window.location.href = "#"; + + clear_bookmark_storage(); + collection_id = parsed_uri["collection_id"]; + add_all_bookmarks_to_storage(parsed_uri["bookmarks"]); + } + + init_bookmark_buttons(scope); + update_collection(); + if (edit_mode) { + window.document.body.addEventListener(edit_mode.end_edit.type, (e) => { + init_bookmark_buttons(e.target); + }, true); + } + } + + /** + * Construct the key for data items in the bookmark_storage. + */ + const get_data_key = function (id, data_key) { + return get_collection_prefix() + '_da_' + id + "_" + data_key; + } + + /** + * Get a specific data item which belongs to a bookmark. + * + * This is currently prominently used by the tsv-export. + * + * @param {string} id - the bookmarked id + * @param {string} data_key - an identifier for the data item to be + * retrieved. + * @returns {string} the `data_key` of bookmark `id`. + */ + const get_bookmark_data = async function (id, data_key) { + // get full key (needed for the cache) + const full_data_key = get_data_key(id, data_key); + + // retrieve from cache + const cached = bookmark_storage[full_data_key]; + if (typeof cached != "undefined") { + return cached; + } + + // not in cache, try the data_getters + var uncached = undefined + if (data_getters[data_key]) { + uncached = (await data_getters[data_key](id)) + } + + // don't cache if getting the information is trivial or there are other + // reasons why this is in the data_no_cache array. + if (data_no_cache.indexOf(data_key) == -1) { + bookmark_storage[full_data_key] = uncached || ""; + } + return uncached; + } + + return { + init: init, + parse_uri: parse_uri, + get_bookmarks: get_bookmarks, + get_key: get_key, + get_value: get_value, + set_collection_id: set_collection_id, + bookmark_storage: bookmark_storage, + get_export_table: get_export_table, + clear_bookmark_storage, + clear_bookmark_storage, + update_clear_button: update_clear_button, + update_export_link: update_export_link, + update_collection_link: update_collection_link, + get_collection_link: get_collection_link, + get_bookmark_buttons: get_bookmark_buttons, + init_button: init_button, + get_bookmark_data: get_bookmark_data, + } +}; + + + +$(document).ready(function () { + if ("${BUILD_MODULE_EXT_BOOKMARKS}" == "ENABLED") { + // The following is the configuration for the CaosDB WebUI. + const get_context_root = (() => connection.getBasePath() + "Entity/"); + + // This getter retrieves a file's path from the page or, if necessary, + // from the server. + const get_path = async function (id) { + if (id.indexOf("@") > -1) { + const entity = $(`[data-bmval='${id}']`); + if (entity.length > 0) { + return getEntityPath(entity[0]) || ""; + } + } + return $(await transaction.retrieveEntityById(id)).attr("path"); + } + + // This retrieves the head version id + const get_version = async function (id) { + return $(await transaction.retrieveEntityById(id)).find("Version").attr("id"); + } + + const get_name = async function (id) { + if (id.indexOf("@") > -1) { + const entity = $(`[data-bmval='${id}']`); + if (entity.length > 0) { + return getEntityName(entity[0]) || ""; + } + } + return $(await transaction.retrieveEntityById(id)).attr("name"); + } + + const get_rt = async function (id) { + if (id.indexOf("@") > -1) { + const entity = $(`[data-bmval='${id}']`); + if (entity.length > 0) { + return getParents(entity[0]).join("/"); + } + } + const parent_names = $(await transaction.retrieveEntityById(id)) + .find("Parent").toArray().map(x => x.getAttribute("name")) + return parent_names.join("/"); + } + + // these columns will be in the export + const tsv_columns = ["ID", "Version", "URI", "Path", "Name", "RecordType"]; + // functions for collecting the export data for a particular bookmarked id. + const data_getters = { + "ID": (id) => id.indexOf("@") > -1 ? id.split("@")[0] : id, + "Version": async (id) => id.indexOf("@") > -1 ? id.split("@")[1] : await get_version(id), + "Path": get_path, + "URI": async (id) => get_context_root() + id + (id.indexOf("@") > -1 ? "" : ("@" + await get_version(id))), + "Name": get_name, + "RecordType": get_rt, + }; + + // we cannot cache these because the the values might change unnoticed + // when the head moves to a newer version. + const data_no_cache = ["ID", "Version", "URI", "Path", "Name", "RecordType"]; + + const config = { + get_context_root: get_context_root, + tsv_columns: tsv_columns, + data_getters: data_getters, + data_no_cache: data_no_cache, + }; + + ext_bookmarks = ext_bookmarks($, log.getLogger("ext_bookmarks"), config); + caosdb_modules.register(ext_bookmarks); + } +}); diff --git a/src/core/js/ext_bottom_line.js b/src/core/js/ext_bottom_line.js index 8c657698cfb9d076b1252f99cab8d57e01239f50..afeecfa0893957ede661f6c0ae560a39fa92bbd3 100644 --- a/src/core/js/ext_bottom_line.js +++ b/src/core/js/ext_bottom_line.js @@ -23,6 +23,28 @@ 'use strict'; +/** + * @typedef {BottomLineConfig} + * @property {string|HTMLElement} fallback - Fallback content if none of + * the creators are applicable. + * @property {string} version - the version of the configuration which must + * match this module's version. + * @property {CreatorConfig[]} creators - an array of creators. + */ + +/** + * @typedef {CreatorConfig} + * @property {string} [id] - a unique id for the creator. optional, for + * debuggin purposes. + * @property {function|string} is_applicable - If this is a string this has + * to be valid javascript! An asynchronous function which accepts one + * parameter, an entity in html representation, and which returns true + * iff this creator is applicable for the given entity. + * @property {string} create - This has to be valid javascript! An + * asynchronous function which accepts one parameter, an entity in html + * representation. It returns a HTMLElement or text node which will be + * shown in the bottom line container iff the creator is applicable. + */ /** * Add a special section to each entity one the current page where a thumbnail, @@ -42,8 +64,10 @@ * @requires load_config (function from caosdb.js) * @requires getEntityPath (function from caosdb.js) * @requires connection (module from webcaosdb.js) + * @requires UTIF (from utif.js library) + * @requires ext_table_preview (module from ext_table_preview.js) */ -var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntityPath, connection, ext_applicable) { +var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntityPath, connection, UTIF, ext_table_preview, ext_applicable) { const contentShownEvent = new Event("ext_bottom_line.content.shown"); const contentReadyEvent = new Event("ext_bottom_line.content.ready"); @@ -57,28 +81,7 @@ var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntit * (entity) Note: This property can as well be a * javascript string which evaluates to a function. */ - /** - * @type {BottomLineConfig} - * @property {string|HTMLElement} fallback - Fallback content if none of - * the creators are applicable. - * @property {string} version - the version of the configuration which must - * match this module's version. - * @property {CreatorConfig[]} creators - an array of creators. - */ - /** - * @type {CreatorConfig} - * @property {string} [id] - a unique id for the creator. optional, for - * debuggin purposes. - * @property {function|string} is_applicable - If this is a string this has - * to be valid javascript! An asynchronous function which accepts one - * parameter, an entity in html representation, and which returns true - * iff this creator is applicable for the given entity. - * @property {string} create - This has to be valid javascript! An - * asynchronous function which accepts one parameter, an entity in html - * representation. It returns a HTMLElement or text node which will be - * shown in the bottom line container iff the creator is applicable. - */ /** @@ -89,7 +92,91 @@ var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntit */ const _create_video_preview = function(entity) { var path = connection.getFileSystemPath() + getEntityPath(entity); - return $(`<div class="caosdb-v-bottom-line-video-preview"><video controls="controls"><source src="${path}"/></video></div>`)[0]; + return $(`<div class="caosdb-v-bottom-line-video-preview"> + <video controls="controls"><source src="${path}"/></video></div>`)[0]; + } + + /** + * Error class which has the special to_html method. + * + * The to_html method creates a html representation of the error which is + * intended for displaying in the bottom_line container. + */ + const BottomLineError = function(arg) { + this._is_bottom_line_error = true; + + if (arg.message) { + // arg is an Error object + this.message = arg.message; + this.stack = arg.stack; + } else { + this.message = arg; + } + + this.to_html = function() { + return $(`<div><p>An error occured while loading this preview.<p>${ + this.message}<div>`)[0]; + } + } + + const BottomLineWarning = function (arg) { + this._is_bottom_line_error = true; + + if (arg.message) { + // arg is an Error object + this.message = arg.message; + this.stack = arg.stack; + } else { + this.message = arg; + } + + this.to_html = function() { + return $(`<div>${this.message}<div>`)[0]; + } + } + + /** + * Create a preview for tiff files. + * + * Tiff files are decompressed if necessary and converted into png by UTIF library. + * + * @param {HTMLElement} entity + * @return {Promise | HTMLElement} Promise for an IMG element. + */ + const _create_tiff_preview = function(entity) { + const path = connection.getFileSystemPath() + getEntityPath(entity); + const result = $(`<div class="caosdb-v-bottom-line-image-preview"></div>`); + const img = $(`<img src="${path}"/>`)[0]; + result.append(img); + + /** + * Promise which retrieves the tiff file and calls the UTIF library for + * decompression and conversion into png. + */ + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + UTIF._xhrs.push(xhr); + UTIF._imgs.push(img); + xhr.open("GET", path); + xhr.responseType = "arraybuffer"; + xhr.onload = (e) => { + try { + // decompress and convert tiff file + UTIF._imgLoaded(e); + + // return the result if no error occured. + resolve(result); + } catch(err) { + // throw errors from UTIF to the awaiting caller. + reject(new BottomLineError(err)); + } + } + // throw http errors to the awaiting caller. + xhr.onerror = reject; + + // this finally triggers the retrieval + xhr.send(); + }); } /** @@ -105,6 +192,8 @@ var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntit var fallback_preview = undefined; + const _tiff_preview_enabled = "${BUILD_MODULE_EXT_BOTTOM_LINE_TIFF_PREVIEW}" == "ENABLED"; + /** * Default creators. * @@ -114,12 +203,21 @@ var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntit id: "_default_creators.pictures", is_applicable: (entity) => ext_applicable.helpers.path_has_file_extension( entity, ["jpg", "png", "gif", "svg"]), - create: _create_picture_preview + create: _create_picture_preview, + }, { + id: "_default_creators.tiff_images", + is_applicable: (entity) => _tiff_preview_enabled && _path_has_file_extension( + entity, ["tif", "tiff","dng","cr2","nef"]), + create: _create_tiff_preview, }, { // videos id: "_default_creators.videos", is_applicable: (entity) => ext_applicable.helpers.path_has_file_extension( entity, ["mp4", "mov", "webm"]), create: _create_video_preview, + }, { // tables + id: "_default_creators.table_preview", + is_applicable: (e) => ext_table_preview.is_table(e), + create: (e) => ext_table_preview.get_preview(e), }, ]; @@ -137,6 +235,71 @@ var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntit _css_class_preview_container}`)[0]; } + /** + * Append a preview to the entity and removes any pre-existing preview. + * + * If the preview is Promise for a preview a waiting notification is added + * to the entity instead and the actual preview is added after the Promise + * is resolved. If the Promise is rejected, a correspondig error is shown + * instead. + * + * @see root_preview_handler + * + * @async + * @param {HTMLElement} entity + * @param {string|HTMLElement|Promise} preview - A preview for an entity or + * a Promise for a preview (which resolves as a string or an HTMLElement as well). + */ + /* + var set_preview = async function(entity, preview) { + try { + const wait = "Please wait..."; + set_preview_container(entity, wait); + const result = await preview; + set_preview_container(entity, result); + if (result) { + entity.dispatchEvent(previewReadyEvent); + } + } catch (err) { + logger.error(err); + if (!err._is_bottom_line_error) { + err = new BottomLineError(err); + } + set_preview_container(entity, err.to_html()); + } + }*/ + + /** + * Create and return a preview for a given entity. + * + * This root_preview_creator iterates over all the registered creators and + * uses the first match, i.e. the first creator object which return true + * for the `is_applicable(entity)` method of the creator object. + * + * If a creator throws an error during checking whether it `is_applicable` + * or during the `create` the error is logged and the creator is treated as + * if it were not applicable. + * + * @async + * @param {HTMLElement} entity - the entity for which the preview is to be + * created. + * @returns {String|HTMLElement|Promise} A preview which can be added to + * the entity DOM representation or a Promise for such a preview. + */ + /* + var root_preview_creator = async function(entity) { + for (let c of _creators) { + try { + if (await c.is_applicable(entity)) { + return c.create(entity); + } + } catch (err) { + logger.error(err); + } + } + return undefined; + };*/ + /** * Add a preview container to the entity. * @@ -265,6 +428,9 @@ var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntit } } + /** + * @exports ext_bottom_line + */ return { contentReadyEvent: contentReadyEvent, contentShownEvent: contentShownEvent, @@ -274,7 +440,7 @@ var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntit get_container: get_container, _css_class_preview_container: _css_class_preview_container, } -}($, log.getLogger("ext_bottom_line"), resolve_references.is_in_viewport_vertically, load_config, getEntityPath, connection, ext_applicable); +}($, log.getLogger("ext_bottom_line"), resolve_references.is_in_viewport_vertically, load_config, getEntityPath, connection, UTIF, ext_table_preview, ext_applicable); /** @@ -297,20 +463,23 @@ var plotly_preview = function(logger, ext_bottom_line, plotly) { * to be plotted. * @param {object[]} layout - dictionary of settings defining the layout of * the plot. + * @param {object[]} settings - object containing additional + * settings for the plot. * @returns {HTMLElement} the element which contains the plot. */ const create_plot = function(data, - layout = { - margin: { - t: 0 - }, - height: 400, - widht: 400 - }) { + layout = { + margin: { + t: 0 + }, + height: 400, + widht: 400 + }, + settings = { + responsive: true + }) { var div = $('<div/>')[0]; - plotly.newPlot(div, data, layout, { - responsive: true - }); + plotly.newPlot(div, data, layout, settings); return div; } @@ -338,7 +507,7 @@ var plotly_preview = function(logger, ext_bottom_line, plotly) { // this will be replaced by require.js in the future. $(document).ready(function() { - if ("${BUILD_MODULE_EXT_BOTTOM_LINE}" === "ENABLED") { + if ("${BUILD_MODULE_EXT_BOTTOM_LINE}" == "ENABLED") { caosdb_modules.register(plotly_preview); caosdb_modules.register(ext_bottom_line); } diff --git a/src/core/js/ext_file_download.js b/src/core/js/ext_file_download.js new file mode 100644 index 0000000000000000000000000000000000000000..477349b421e03d74aa28fddf6b4748f64ffa9f7b --- /dev/null +++ b/src/core/js/ext_file_download.js @@ -0,0 +1,186 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2020 Henrik tom Wörden <h.tomwoerden@indiscale.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ + +'use strict'; + +/** + * The ext_file_download module provides a very basic preview for table files. + * + * The preview is generated using a server side script. + * + * @module ext_file_download + * @version 0.1 + * + * @requires jQuery + * @requires log + */ +var ext_file_download = function ($, logger) { + + /** + * collect the file ids that will be passed to the zip script + * + * @return {string[]} array of entity ids + */ + const collect_ids = function(){ + var properties = $(".caosdb-f-property-value").find( + ".caosdb-id.caosdb-f-property-single-raw-value"); + var id_list = properties.toArray().map(x=>x.textContent); + var entities = $("tr[data-entity-id]") + id_list = id_list.concat(entities.toArray().map( + x=>x.attributes['data-entity-id'].value)); + return id_list + }; + + /** + * chunk list to smaller pieces. + * + * @param {string[]} list - array of (usually) ids + * @param {Number} size - chunk size (use integer) + * @return {string[][]} array of array of string from the original list. + */ + const chunk_list = function(list, size){ + size = size || 20; + var pieces = []; + var index = 0; + while (index < list.length){ + pieces.push(list.slice(index,index+size)); + index+=size; + } + + return pieces + }; + + + /** + * create select statement to find files. + * + * @param {string[]} id_list - array of ids + * @return {string} a query string + */ + const query_files_str = function(id_list){ + var povs = id_list.map(x=>` id=${x} `); + const query_str="SELECT ID FROM FILE WITH " + povs.join("or"); + return query_str; + }; + + /** + * Reduce id list to files. Throw away all ids which do not belong to a + * file entity. + * + * @param {string[]} id_list - array of ids + * @return {string[]} array of file ids. + */ + const reduce_ids = async function(id_list){ + var file_ids = [] + for (var part of chunk_list(id_list)) { + // query for files + var result = await query(query_files_str(part)); + file_ids=file_ids.concat(result.map(x => getEntityID(x))); + } + return file_ids; + }; + + + /** + * Callback function for the download files link. + * + * Collects all the file entities and sends them to a server-side script + * which then puts the files into a zip. + * + * @param {HTMLElement} zip_link - the link element which triggered this + * call and which will be disabled during the execution of this + * function in order to prevent the user from triggering the process + * twice. + */ + const download_files = async function (zip_link) { + const onClickValue = zip_link.getAttribute("onClick"); + try { + // remove click event handler which called this function in the first place + zip_link.removeAttribute("onClick"); + + // add loading info. TODO make an animated one + $("#downloadModal").find(".caosdb-f-modal-footer-left").append( + createWaitingNotification("Collecting files...") + ); + + var ids = collect_ids(); + ids = await reduce_ids(ids); + if (ids.length == 0){ + alert ("There are no file entities in this table."); + return; + } + + var table = await ext_bookmarks.get_export_table(ids, "", "\t", "\n"); + + const result = await connection.runScript( + "ext_file_download/zip_files.py", + { + "-p0": ids, + "-p1": table, + } + ); + const code = result.getElementsByTagName("script")[0].getAttribute("code"); + if (parseInt(code) > 0) { + throw ("An error occurred during execution of the server-side script:\n" + + result.getElementsByTagName("script")[0].outerHTML); + } + const filename = result.getElementsByTagName("stdout")[0].textContent; + if (filename.length == 0) { + throw("Server-side script produced no file or did not return the file name: \n" + + result.getElementsByTagName("script")[0].outerHTML); + } + + + // trigger download of generated file + caosdb_table_export.go_to_script_results(filename); + + //close modal + $("#downloadModal").find(".modal-footer").find(".btn")[0].click(); + } catch (e) { + globalError(e); + } finally { + removeAllWaitingNotifications($("#downloadModal")[0]); + // restore the old click handler - hence a new file is generated with each click. + zip_link.setAttribute("onClick", onClickValue); + } + + }; + + const init = function () { + // only enable when init is being called + logger.info("init ext_file_download"); + if (userIsAnonymous()) { + $("#caosdb-f-query-select-files").parent().hide(); + } + }; + + return { + init: init, + download_files: download_files, + collect_ids: collect_ids, + chunk_list: chunk_list, + }; + +}($, log.getLogger("ext_file_download")); + +// This module is registered by caosdb_table_export. diff --git a/src/core/js/ext_map.js b/src/core/js/ext_map.js index 67c7bfe799c8c96eb945ec9334ad87fa3f7d70f9..3b89f72be05d2307cdcc2e624e4b8a69684d591f 100644 --- a/src/core/js/ext_map.js +++ b/src/core/js/ext_map.js @@ -25,7 +25,7 @@ /** * @module caosdb_map - * @version 0.3 + * @version 0.4 * * For displaying a geographical map which shows entities at their associated * geolocation. @@ -34,7 +34,7 @@ * `conf/ext/json/ext_map.json` and comply with the {@link MapConfig} type * which is described below. * - * The current version is 0.3. It is not considered to be stable because + * The current version is 0.4. It is not considered to be stable because * implementation of the graticule still is not satisfactory. * * Apart from that the specification of the configuration and the @@ -43,7 +43,7 @@ var caosdb_map = new function () { var logger = log.getLogger("caosdb_map"); - this.version = "0.3"; + this.version = "0.4"; this.dependencies = ["log", { "L": ["latlngGraticule", "Proj"] }, "navbar", "caosdb_utils"]; @@ -84,10 +84,14 @@ var caosdb_map = new function () { /** * The SelectConfig object configures the custom {@link select_handler} * plugin for the Leaflet.js module, especially the query generation (for - * searching for Entities in the selected area). + * searching for Entities in the selected area) and when retrieving + * entities to be shown. * - * The generated query has the pattern <code>FIND {@link query.role} {@link - * query.entity} WITH ...<code>. The dots stand for the area filter here. + * The generated query for a selected area has the pattern + * <code>FIND {@link query.role} {@link query.entity} WITH PATH AREA<code>. + * PATH can be empty or represent a configured path to some entity, e.g. + * `WITH RT1 WITH RT2`. + * AREA stand for the area filter here. * * The default values of the {@link query} result in queries for any Record * in the selected map area. @@ -98,6 +102,8 @@ var caosdb_map = new function () { * are to be searched in the selected ares. * @property {string} [query.entity] The (parent) entity to be searched * for in the area. Defaults to empty string. + * @property {object} [paths] - A dictionary of paths that define from + * which entities the geographic location shall be taken. */ /** @@ -332,6 +338,7 @@ var caosdb_map = new function () { "role": "RECORD", "entity": "", }, + "paths": {}, }, } @@ -373,21 +380,190 @@ var caosdb_map = new function () { */ /** - * Implements {@link mapEntityGetter}. + * Generates a Property Operator Value (POV) expression by chaining the + * provided arguments with "WITH". + * + * @param {string[]} props - array with the names of RecordTypes + * @returns {string} string with the the filter + */ + this._get_with_POV = function (props) { + var pov = "" + for (let p of props) { + pov = pov + ` WITH ${p} `; + } + return pov; + } + + /** + * Generates a Property Operator Value (POV) by joining ids with OR. + * + * @param {number[]} ids - array of ids for the filter + * @returns {string} string with the the filter + */ + this._get_id_POV = function (ids) { + ids = ids.map(x => "id=" + x); + return "WITH " + ids.join(" or ") + } + + /** + * Generates a SELECT query string that applies the provided path of + * properties as POV and as selector + * + * If ids is provided, the condition is not created from the path, but + * from ids. + * + * @param {DataModelConfig} datamodel - datamodel of the entities to be returned. + * @param {string[]} path - array with the names of RecordTypes + * @param {number[]} ids - array of ids for the filter + * @returns {string} query string */ - this._get_current_page_entities = function ( - datamodel, north, south, west, east) { - const container = $(".caosdb-f-main-entities")[0]; + this._get_select_with_path = function (datamodel, path, ids) { + if (typeof datamodel === "undefined") { + throw new Error("Supply the datamodel.") + } + if (typeof path === "undefined" || path.length == 0) { + throw new Error("Supply at least a RecordType.") + } + const recordtype = path[0]; + const props = path.slice(1, path.length) + var selector = props.join(".") + if (selector != "") { + selector = selector + "." + } + var pov = undefined; + if (typeof ids === "undefined") { + pov = (caosdb_map._get_with_POV(props) + + ` WITH ${datamodel.lat} AND ${datamodel.lng}`); + + } else { + pov = caosdb_map._get_id_POV(ids); + } + return `SELECT parent,${selector}${datamodel.lat},${selector}${datamodel.lng} FROM ENTITY ${recordtype} ${pov} `; + + } + + + /** + * Returns a dictionary where for each top level record in the xmldoc + * The long and lat is assigned. + * + * depth: the depth of the tree including the top level record type: + * e.g. RT1->prop1->prop2->lat/long would be a depth=3 + * + * @param {XMLDocument} xmldoc - xml document containing the entities + * @param {number} depth - the depth at which the properties shall be taken + * @param {DataModelConfig} datamodel - datamodel of the entities to be returned. + * @returns {Object} a dictionary where for each id as key the location ist + * stored as [lat, lng] + */ + this._get_leaf_prop = function (xmldoc, depth, datamodel) { + const paths = [ + ["id"], + // The following creates a list: ["", "", ... (depth times), lat/long] + ("__split__".repeat(depth) + datamodel.lat).split("__split__"), + ("__split__".repeat(depth) + datamodel.lng).split("__split__"), + ]; + const propertyValues = getPropertyValues(xmldoc, paths); + + const leaves = {}; + for (let row of propertyValues) { + leaves[row[0]] = [row[1], row[2]]; + } + return leaves; + } + + /** + * Template for {@link mapEntityGetter}. + * + * This implementation has a single additional parameter which is not + * defined by {@link mapEntityGetter}: + * + * @param {string[]} path - array of strings defining the path to the + * related entity + */ + this._generic_get_current_page_entities = async function ( + datamodel, north, south, west, east, path) { + var container = $(".caosdb-f-main-entities")[0]; + + if (typeof path !== "undefined" && path.length) { + var ids = [] + for (let rec of getEntities(container)) { + ids.push(getEntityID(rec)) + } + if (ids.length) { + const qs = caosdb_map._get_select_with_path(datamodel, path, ids); + let entities = await connection.get("Entity/?query=" + qs); + caosdb_map._set_subprops_at_top(entities, path.length - 1, datamodel); + let results = await transformation.transformEntities(entities); + container = $('<div>').append(results)[0]; + } else { + return []; + } + } + // it is possible, the the page contains entities which do not have + // lat/lng and there doesn't exist any related entity with lat/lng. return caosdb_map.get_map_entities(container, datamodel); } /** - * Implements {@link mapEntityGetter}. + * Returns a top level record entity from xml. + * + * @param {XMLDocument} entities - xml document containing the entities + * @param {number} rec_id - id of the record to be returned + * @returns {XMLDocument} the corresponding record + */ + this._get_toplvl_rec_with_id = function (entities, rec_id) { + let tmp = $(entities).find(`Response >[id='${rec_id}']`); + if (tmp.length != 1) { + throw new Error("There should be exactly one result record. Not " + + tmp.length) + } + return tmp[0]; + } + + /** + * Set the longitude/latitude from subproperties to the top level + * records in the xml and convert everything to html. + * + * @param {XMLDocument} entities - xml document containing the entities + * @param {number} depth - the depth of the path (full: including the first + * recordtype) + */ + this._set_subprops_at_top = function (entities, depth, datamodel) { + var latlong = caosdb_map._get_leaf_prop(entities, depth, datamodel); + + for (let rec_id in latlong) { + let tmp_rec = caosdb_map._get_toplvl_rec_with_id(entities, rec_id); + tmp_rec.append(str2xml(`<Property name="${datamodel.lat}">${latlong[rec_id][0]}</Property>`).firstElementChild); + tmp_rec.append(str2xml(`<Property name="${datamodel.lng}">${latlong[rec_id][1]}</Property>`).firstElementChild); + } + } + + /** + * Template for {@link mapEntityGetter}. + * + * This implementation has a single additional parameter which is not + * defined by {@link mapEntityGetter}: + * + * @param {string[]} path - array of strings defining the path to the + * related entity */ - this._query_all_entities = async function ( - datamodel, north, south, west, east) { - const results = await caosdb_map.query(`FIND ENTITY WITH ${datamodel.lat} AND ${datamodel.lng}`); + this._generic_query_all_entities = async function ( + datamodel, north, south, west, east, path) { + var results = undefined; + if (typeof path !== "undefined" && path.length) { + const qs = caosdb_map._get_select_with_path(datamodel, path); + let entities = await connection.get("Entity/?query=" + qs); + caosdb_map._set_subprops_at_top(entities, path.length - 1, datamodel); + results = await transformation.transformEntities(entities); + } else { + results = await caosdb_map.query( + `FIND ENTITY WITH ${datamodel.lat} AND ${datamodel.lng}`); + } const container = $('<div>').append(results)[0]; + + // As soon as the SELECT query can handle subtyping, the results don't + // have to filtered anymore. return caosdb_map.get_map_entities(container, datamodel); } @@ -410,7 +586,12 @@ var caosdb_map = new function () { const name = caosdb_map.make_entity_name_label(entity); const dms_lat = L.NumberFormatter.toDMS(lat); const dms_lng = L.NumberFormatter.toDMS(lng); - const loc = $(`<div class="small text-muted"> + let extra_loc_hint = ""; + let path = caosdb_map._get_current_path(); + if (path && path.length > 1) { + extra_loc_hint = `<div>Location of related ${path[path.length-1]}<div>`; + } + const loc = $(`<div class="small text-muted">${extra_loc_hint} Lat: ${dms_lat} Lng: ${dms_lng} </div>`); const ret = $('<div/>') @@ -421,6 +602,18 @@ var caosdb_map = new function () { return ret[0]; } + /** + * Returns the path from the config corresponding to the value stored in + * the session storage (i.e. the storage should be updated before calling + * this method if the value changes). + * + * @returns {string[]} path - array of strings defining the path to the + * related entity + */ + this._get_current_path = function () { + return caosdb_map.config.select.paths[sessionStorage["caosdb_map.display_path"]]; + } + /** * Default entities layers configuration with two layers: @@ -430,43 +623,52 @@ var caosdb_map = new function () { * * @type {EntityLayerConfig[]} */ - this._default_entity_layer_config = [{ - "id": "current_page_entities", - "name": "Entities on the current page.", - "description": "Show all entities on the current page.", - "icon": { - html: '<span style="color: #00F; font-size: 20px" class="glyphicon glyphicon-map-marker"></span>', - iconAnchor: [10, 19], - className: "", - }, - "zIndexOffset": 1000, - "datamodel": { - "lat": "latitude", - "lng": "longitude", - "role": "ENTITY", - "entity": "", - }, - "get_entities": this._get_current_page_entities, - "make_popup": this._make_map_popup, - }, { - "id": "all_map_entities", - "name": "All entities", - "description": "Show all entities with coordinates.", - "icon": { - html: '<span style="color: #F00; font-size: 20px" class="glyphicon glyphicon-map-marker"></span>', - iconAnchor: [10, 19], - className: "", + this._default_entity_layer_config = { + "current_page_entities": { + "name": "Entities on the current page.", + "description": "Show all entities on the current page.", + "icon": { + html: '<span style="color: #00F; font-size: 20px" class="glyphicon glyphicon-map-marker"></span>', + iconAnchor: [10, 19], + className: "", + }, + "zIndexOffset": 1000, + "datamodel": { + "lat": "latitude", + "lng": "longitude", + "role": "ENTITY", + "entity": "", + }, + "get_entities": (datamodel, north, south, west, east) => { + let path = caosdb_map._get_current_path() + return caosdb_map._generic_get_current_page_entities( + datamodel, north, south, west, east, path) + }, + "make_popup": this._make_map_popup, }, - "zIndexOffset": 0, - "datamodel": { - "lat": "latitude", - "lng": "longitude", - "role": "ENTITY", - "entity": "", + "all_map_entities": { + "name": "All entities", + "description": "Show all entities with coordinates.", + "icon": { + html: '<span style="color: #F00; font-size: 20px" class="glyphicon glyphicon-map-marker"></span>', + iconAnchor: [10, 19], + className: "", + }, + "zIndexOffset": 0, + "datamodel": { + "lat": "latitude", + "lng": "longitude", + "role": "ENTITY", + "entity": "", + }, + "get_entities": (datamodel, north, south, west, east) => { + let path = caosdb_map._get_current_path() + return caosdb_map._generic_query_all_entities( + datamodel, north, south, west, east, path) + }, + "make_popup": this._make_map_popup, }, - "get_entities": this._query_all_entities, - "make_popup": this._make_map_popup, - }, ]; + }; /** @@ -730,6 +932,22 @@ var caosdb_map = new function () { throw new Error("Could not find view " + id); } + /** + * Reload layers. + */ + this._reload_layers = function () { + caosdb_map._show_load_info() + const promises = [] + for (const layer of caosdb_map.layers) { + promises.push(caosdb_map._fill_layer(layer.layer_group, + caosdb_map._default_entity_layer_config[layer.id])); + } + Promise.all(promises).then((val) => { + caosdb_map._hide_load_info() + }) + } + + /** Initialize the caosdb_map module. * @@ -807,16 +1025,36 @@ var caosdb_map = new function () { view_config); // init entity layers - var layers = this.init_entity_layers(this._default_entity_layer_config); + this.layers = this.init_entity_layers(this._default_entity_layer_config); var layerControl = L.control.layers(); - for (const layer of layers) { + + const promises = [] + for (const layer of this.layers) { + + promises.push(caosdb_map._fill_layer(layer.layer_group, + this._default_entity_layer_config[layer.id])); layerControl.addOverlay(layer.layer_group, layer.chooser_html.outerHTML); layer.layer_group.addTo(this._map); } + Promise.all(promises).then((val) => { + caosdb_map._hide_load_info() + }) layerControl.addTo(this._map); // initialize handlers this.add_select_handler(this._map); + + + this.path_ddm = this._get_path_ddm( + (event) => { + sessionStorage["caosdb_map.display_path"] = event.target.value; + caosdb_map._reload_layers(); + }, + this.config.select.paths + ); + this._map.addControl(this.path_ddm); + + this.add_view_change_handler( this._map, config.views, @@ -858,12 +1096,33 @@ var caosdb_map = new function () { */ this.init_entity_layers = function (configs) { var ret = [] - for (const conf of configs) { - ret.push(this.init_entity_layer(conf)); + for (let name in configs) { + configs[name]["id"] = name; + ret.push(this._init_single_entity_layer(configs[name])); } return ret; } + /** + * Initialize an entity layer. + * + * @param {EntityLayerConfig} config + * @return {_EntityLayer} + */ + this._fill_layer = async function (layer_group, config) { + // in case load is called on a filled layer: clear first + layer_group.clearLayers(); + + var entities = await config.get_entities(config.datamodel); + layer_group.entities = entities; + var markers = caosdb_map.create_entity_markers( + entities, config.datamodel, config.make_popup, + config.zIndexOffset, config.icon); + + for (const marker of markers) { + layer_group.addLayer(marker); + } + }; /** * Initialize an entity layer. @@ -871,24 +1130,11 @@ var caosdb_map = new function () { * @param {EntityLayerConfig} config * @return {_EntityLayer} */ - this.init_entity_layer = function (config) { - logger.trace("enter init_entity_layer", config); + this._init_single_entity_layer = function (config) { + logger.trace("enter _init_single_entity_layer", config); var layer_group = L.layerGroup(); - // load all entities into layer group - var _load = async function (layer_group, config) { - var entities = await config.get_entities(config.datamodel); - var markers = caosdb_map.create_entitiy_markers( - entities, config.datamodel, config.make_popup, - config.zIndexOffset, config.icon); - - for (const marker of markers) { - layer_group.addLayer(marker); - } - }; - _load(layer_group, config); - var ret = { "id": config.id, "active": typeof config.active === "undefined" || config.active, @@ -975,7 +1221,6 @@ var caosdb_map = new function () { L.Handler.extend(this.select_handler); } - /** * Show the query panel if not visible, collapse the query shortcuts * if visible and fill the query string into the text input of the @@ -1048,9 +1293,18 @@ var caosdb_map = new function () { this.generate_query_from_bounds = function (north, south, west, east) { const role = this.config.select.query.role; - const entity = this.config.select.query.entity; + var entity = this.config.select.query.entity; const lat = this.config.datamodel.lat; const lng = this.config.datamodel.lng; + let path = caosdb_map._get_current_path(); + if (path && path.length > 0 && entity == "") { + entity = path[0]; + } + var additional_path = "" + if (path && path.length > 1) { + additional_path = caosdb_map._get_with_POV( + path.slice(1, path.length)) + } const query_filter = " ( " + lat + " < '" + north + "' AND " + lat + @@ -1058,7 +1312,7 @@ var caosdb_map = new function () { "' AND " + lng + " < '" + east + "' ) "; - const query = "FIND " + role + " " + entity + + const query = "FIND " + role + " " + entity + additional_path + " WITH " + query_filter; return query } @@ -1201,7 +1455,7 @@ var caosdb_map = new function () { const entity_on_page = $(`#${id}`).length > 0; const href = entity_on_page ? `#${id}` : connection.getBasePath() + `Entity/${id}` - const link_title = entity_on_page ? "Jump to this entitiy." : "Browse to this entity."; + const link_title = entity_on_page ? "Jump to this entity." : "Browse to this entity."; const link = $(`<a title="${link_title}" href="${href}"/>`) .addClass("pull-right") .append(`<span class="glyphicon glyphicon-share-alt"/></a>`); @@ -1239,8 +1493,8 @@ var caosdb_map = new function () { * @param {DivIcon_options} icon_options * @returns {L.Marker[]} an array of markers for the map. */ - this.create_entitiy_markers = function (entities, datamodel, make_popup, zIndexOffset, icon_options) { - logger.trace("enter create_entitiy_markers", entities, datamodel, zIndexOffset, icon_options); + this.create_entity_markers = function (entities, datamodel, make_popup, zIndexOffset, icon_options) { + logger.trace("enter create_entity_markers", entities, datamodel, zIndexOffset, icon_options); var ret = [] for (const map_entity of entities) { @@ -1440,6 +1694,83 @@ var caosdb_map = new function () { }, } + /** + * Shows the loading information div of the map + */ + this._show_load_info = function () { + $(".caosdb-f-map-loading").attr("style", "display:inherit"); + } + + /** + * Hides the loading information div of the map + */ + this._hide_load_info = function () { + $(".caosdb-f-map-loading").attr("style", "display:None"); + } + + /** + * Return a new leaflet control for setting paths to use for geo location + * + * @param {function} callback - a callback applies the effect of a + * changed path + * @returns {L.Control} the drop down menu button. + */ + this._get_path_ddm = function (callback, paths) { + + // TODO flatten the structure of the code and possibly merge it with the query_button code. + var path_ddm = L.Control.extend({ + options: { + position: "bottomright" + }, + + onAdd: function (m) { + return this.button; + }, + + button: function () { + // TODO refactor to make_map_control function + var button = L.DomUtil + .create("div", + "leaflet-bar leaflet-control leaflet-control-custom" + ); + button.title = `Show the location of related entities. +By default ('Same Entity') entities are shown that have +a geographic location. The other options allow to show +entities on the map using the location of a related +entity.`; + button.style.backgroundColor = "white"; + button.style.textAlign = "center"; + // Distance to zoom buttons: + button.style.marginTop = "10px"; + // TODO implement helper for pictures + let tmp_html = ('<div class="caosdb-f-map-loading" style="display:inherit">Loading Entities...</div><select><option value="same">Same Entity</option>'); + for (let pa in paths) { + tmp_html += `<option value="${pa}">${pa}</option>`; + } + tmp_html += '</select>'; + button.innerHTML = tmp_html; + const select = $(button).find('select'); + select.on("change", callback); + + const current_path = sessionStorage["caosdb_map.display_path"] || "same"; + sessionStorage["caosdb_map.display_path"] = current_path; + select[0].value = current_path + + $(button).on("mousedown", ( + event) => { + event + .stopPropagation(); + }); + $(button).on("mouseup", ( + event) => { + event + .stopPropagation(); + }); + return button; + }(), + }); + return new path_ddm(); + } /** * Plug-in for leaflet which lets the user select an area in the map diff --git a/src/core/js/ext_references.js b/src/core/js/ext_references.js index 7aecb16a9c8158683f84e364dc1362103f296c45..06bed856ff83f39b4f27ae54a4520c5fe4e6608b 100644 --- a/src/core/js/ext_references.js +++ b/src/core/js/ext_references.js @@ -460,9 +460,11 @@ var resolve_references = new function () { // Loop over all property values in the container element. Note: each property_value can be a single reference or a list of references. for (const property_value of property_values) { - const lists = $(property_value).find( - ".caosdb-value-list").has( - `.${_unresolved_class_name}`); + var lists = findElementByConditions( + property_value, + x => x.classList.contains("caosdb-value-list"), + x => x.classList.contains("caosdb-preview-container")) + lists = $(lists).has(`.${_unresolved_class_name}`); if (lists.length > 0) { logger.debug("processing list of references", lists); @@ -535,8 +537,10 @@ var resolve_references = new function () { // Load all remaining references. These are single reference values // and those references from lists which are left for lazy loading. - const rs = $(property_value).find( - `.${_unresolved_class_name}`); + const rs = findElementByConditions( + property_value, + x => x.classList.contains(`${_unresolved_class_name}`), + x => x.classList.contains("caosdb-preview-container")); for (var i = 0; i < rs.length; i++) { if (resolve_references.is_in_viewport_vertically( rs[i]) && diff --git a/src/core/js/ext_table_preview.js b/src/core/js/ext_table_preview.js new file mode 100644 index 0000000000000000000000000000000000000000..1d9da6fa9334a52de5eb39a66b585c7eb67cd120 --- /dev/null +++ b/src/core/js/ext_table_preview.js @@ -0,0 +1,97 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2020 Henrik tom Wörden <h.tomwoerden@indiscale.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ + +'use strict'; + +/** + * The ext_table_preview module provides a very basic preview for table files. + * + * The preview is generated using a server side script. + * + * @module ext_table_preview + * @version 0.1 + * + * @requires jQuery + * @requires log + * @requires getEntityPath + * @requires getEntityID + * @requires markdown + */ +var ext_table_preview = function ($, logger, connection, getEntityPath, getEntityID, markdown) { + + const get_preview = async function (entity) { + try { + const script_result = await connection.runScript("ext_table_preview/pandas_table_preview.py", + {"-p0": getEntityID(entity)} + ); + + const code = script_result.getElementsByTagName("script")[0].getAttribute("code"); + if (parseInt(code) > 1) { + return script_result.getElementsByTagName("stderr")[0] + } else if (parseInt(code) != 0) { + throw ("An error occurred during execution of the server-side " + + "script:\n" + + script_result.getElementsByTagName("stderr")[0]); + } else { + const tablecontent = script_result.getElementsByTagName("stdout")[0]; + const unformatted = markdown.textToHtml(tablecontent.textContent) + const formatted = $('<div class="table-responsive"/>').append(unformatted); + formatted.find("table").addClass("table table-bordered table-condensed").removeAttr("border"); + return formatted[0]; + } + } catch (err) { + if (err.message && err.message.indexOf && err.message.indexOf("HTTP status 403") > -1) { + throw new ext_bottom_line.BottomLineWarning("You are not allowed to generate the table preview. Please log in."); + } else { + throw err; + } + } + }; + + const is_table = function (entity) { + const path = getEntityPath(entity); + return path && (path.toLowerCase().endsWith('.xls') + || path.toLowerCase().endsWith('.xlsx') + || path.toLowerCase().endsWith('.csv') + || path.toLowerCase().endsWith('.tsv')); + }; + + const init = function () { + // only enable when init is being called + ext_table_preview.is_table = is_table; + }; + + return { + init: init, + get_preview: get_preview, + is_table: () => false, + }; + +}($, log.getLogger("ext_table_preview"), connection, getEntityPath, getEntityID, markdown); + +// this will be replaced by require.js in the future. +$(document).ready(function () { + if ("${BUILD_MODULE_EXT_BOTTOM_LINE_TABLE_PREVIEW}" == "ENABLED") { + caosdb_modules.register(ext_table_preview); + } +}); diff --git a/src/core/js/ext_xls_download.js b/src/core/js/ext_xls_download.js index 0f607a459b8e502e1d4c451b1636d99ceb01657c..d80bf4efa539f483811ca1d3b7f85b817489a572 100644 --- a/src/core/js/ext_xls_download.js +++ b/src/core/js/ext_xls_download.js @@ -24,7 +24,7 @@ /** * @module caosdb_table_export - * @version 0.1 + * @version 0.2 * * Convert Entities for TSV and XLS export. * @@ -35,8 +35,6 @@ var caosdb_table_export = new function () { var logger = log.getLogger("caosdb_table_export"); - var TAB = "%09"; - var NEWLINE = "%0A"; /** * Hide "Download XLS File" link if the user is not authenticted (i.e. @@ -44,11 +42,7 @@ var caosdb_table_export = new function () { */ this.init = function() { logger.info("init caosdb_table_export"); - // TODO with AMD, use userIsAnonymous() - if (Array.from( - document.getElementsByClassName("caosdb-user-role")).map( - el => el.innerText - ).filter(el => el == "anonymous").length > 0) { + if (userIsAnonymous()) { $(".caosdb-v-query-select-data-xsl").parent().hide(); } } @@ -62,19 +56,64 @@ var caosdb_table_export = new function () { * @return {string} */ this.get_tsv_string = function(raw) { + const columns = this._get_column_header(); + const entities = this._get_table_content(); + return this._create_tsv_string(entities, columns, raw); + } + + /** + * In order to create a valid tsv table, characters that have a special + * meaning (line break and tab) need to be removed. + * + * Both characters will be replaced by a single white space + * + * @param {string} raw - the cell content. + * @return {string} cleaned up content + */ + this._clean_cell = function(raw) { + return raw.replaceAll("\t"," ").replaceAll("\n"," ").replaceAll("\r"," ").replaceAll("\x1E"," ").replaceAll("\x15"," ") + } + + /** + * Finds the table in the DOM tree and returns the column names as array + * + * @return {string[]} + */ + this._get_column_header = function() { + const table = $('.caosdb-select-table'); + return table.find("th").toArray() + .map(e => caosdb_table_export._clean_cell(e.textContent)) + .filter(e => e.length > 0 && e.toLowerCase() != "id" && e.toLowerCase() != "version"); + } + + /** + * Finds the table in the DOM tree and returns the table content as array + * + * @return {HTMLElement[]} + */ + this._get_table_content = function() { const table = $('.caosdb-select-table'); - const columns = table.find("th").toArray() - .map(e => e.textContent) - .filter(e => e.length > 0); // TODO use entity-panel class in table as well (change in query.xsl // and then here) - const entities = table.find("tbody tr").toArray(); - const csv_string = this._get_tsv_string(entities, columns, raw); - return csv_string; + return table.find("tbody tr").toArray(); + } + + /** + * Encode tsv string + * + * @param {string} main - the tsv table string. + * @return {string} the same with prefix and URL encoded + */ + this._encode_tsv_string = function(main) { + const preamble = "data:text/csv;charset=utf-8,"; + main = encodeURIComponent(main); + return `${preamble}${main}`; } /** - * Convert all entities to a tsv string with the given columns. + * Convert all entities to an encoded tsv string with the given columns. + * + * TODO merge with caosdb_utils.create_tsv_table. * * @param {HTMLElement[]} entities - entities which are converted to rows * of the tsv string. @@ -83,36 +122,56 @@ var caosdb_table_export = new function () { * cells. Otherwise, the displayed data is used instead. * @return {string} */ - this._get_tsv_string = function (entities, columns, raw) { - logger.trace("enter get_tsv_string", entities, columns); - var preamble = "data:text/csv;charset=utf-8,"; - var header = "ID" + TAB + columns.join(TAB) + NEWLINE - var rows = caosdb_table_export._get_tsv_rows(entities, columns, raw).join(NEWLINE); - - const ret = `${preamble}${header}${rows}`; - logger.trace("leave get_tsv_string", ret); - return ret; + this._create_tsv_string = function (entities, columns, raw) { + logger.trace("enter _create_tsv_string ", entities, columns); + var header = "ID\tVersion\t" + columns.join("\t") + "\n" + var rows = []; + for (const table_row of entities) { + rows.push(caosdb_table_export._get_entity_row(table_row, columns, raw).join("\t")); + } + var rows = rows.join("\n"); + logger.trace("leave _create_tsv_string ", rows); + return `${header}${rows}`; } /** - * Return an array of rows with the given columns of the tsv table, one per - * entity. + * Return an array of cells, one per column, which contain a string + * representation of the value of the properties with the same name (as the + * column). * - * @param {HTMLElement[]} entities - entities which are converted to rows - * of the tsv string. + * @param {HTMLElement} entity - entity from which the cells are extracted. * @param {string[]} columns - array of property names. * @param {boolean} raw - if true, the raw entity ids are put into the * cells. Otherwise, the displayed data is used instead. * @return {string[]} */ - this._get_tsv_rows = function (entities, columns, raw) { - var rows = []; - for (const entity of entities) { - rows.push(this._get_entity_row(entity, columns, raw)); + this._get_entity_row = function (entity, columns, raw) { + var cells = [getEntityID(entity), getEntityVersion(entity)]; + var properties = getProperties(entity); + + for (const column of columns) { + var cell = ""; + for (const property of properties) { + if(property.name.toLowerCase() === column.toLowerCase()) { + var value = caosdb_table_export + ._get_property_value(property.html); + if (raw) { + cell = value.raw; + } else if (value.summary) { + cell = value.summary; + } else if (value.pretty) { + cell = value.pretty; + } else { + cell = value.raw; + } + } + } + cells.push(caosdb_table_export._clean_cell(cell)); } - return rows; - } + logger.trace("leave _get_entity_row", cells); + return cells; + } /** * Return different string representations of the property's value. @@ -187,73 +246,15 @@ var caosdb_table_export = new function () { } } - - /** - * Return an array of cells, one per column, which contain a string - * representation of the value of the properties with the same name (as the - * column). - * - * @param {HTMLElement} entity - entity from which the cells are extracted. - * @param {string[]} columns - array of property names. - * @param {boolean} raw - if true, the raw entity ids are put into the - * cells. Otherwise, the displayed data is used instead. - * @return {string[]} - */ - this._get_entity_row = function (entity, columns, raw) { - var cells = [getEntityID(entity)]; - var properties = getProperties(entity); - - for (const column of columns) { - var cell = ""; - for (const property of properties) { - if(property.name.toLowerCase() === column.toLowerCase()) { - var value = caosdb_table_export - ._get_property_value(property.html); - if (raw) { - cell = value.raw; - } else if (value.summary) { - cell = value.summary; - } else if (value.pretty) { - cell = value.pretty; - } else { - cell = value.raw; - } - } - } - cells.push(cell); - } - - logger.trace("leave _get_entity_row", cells); - return cells.join(TAB); - } - - /** * Open the resulting xls file by setting href to the location of the resulting * file in the server's `Shared` resource and imitate a click. */ - this._go_to_script_results = function (xls_link, filename) { - xls_link.setAttribute( - "href", - location.protocol + "//" +location.host + "/Shared/" + filename); - xls_link.click(); - } - - - this._get_csv_string = function (){ - const raw = $("input#caosdb-table-export-raw-flag-xls").is(":checked"); - const csv_string = caosdb_table_export.get_tsv_string(raw); - //const csv_string = document.getElementById("caosdb-f-query-select-data-tsv").getAttribute( - //"href"); - if (!csv_string) { - return undefined; - } - - return decodeURIComponent(csv_string.replace(/^data.*utf-8,/, "")); + this.go_to_script_results = function (filename) { + window.location.href = connection.getBasePath() + "Shared/" + filename; } } - /** * This function is called on click by the link button which says "Download TSV * File". @@ -264,12 +265,12 @@ var caosdb_table_export = new function () { */ function downloadTSV(tsv_link) { const raw = $("input#caosdb-table-export-raw-flag-tsv").is(":checked"); - const tsv_string = caosdb_table_export.get_tsv_string(raw); + var tsv_string = caosdb_table_export.get_tsv_string(raw); + tsv_string = caosdb_table_export._encode_tsv_string(tsv_string); $(tsv_link).attr("href", tsv_string); return true; } - /** * This function is called on click by the link button which says "Download XLS * File". @@ -279,15 +280,24 @@ function downloadTSV(tsv_link) { * resulting file. */ async function downloadXLS(xls_link) { - const csv_string = caosdb_table_export._get_csv_string(); - - // remove click event handler which called this function in the first place const onClickValue = xls_link.getAttribute("onClick"); - xls_link.removeAttribute("onClick"); - try { + // remove click event handler which called this function in the first place + xls_link.removeAttribute("onClick"); + + // add loading info. TODO make an animated one + $("#downloadModal").find(".caosdb-f-modal-footer-left").append( + createWaitingNotification("Exporting table...") + ); + + const raw = $("input#caosdb-table-export-raw-flag-xls").is(":checked"); + const tsv_string = caosdb_table_export.get_tsv_string(raw); + // TODO This existed previously. I do not know why. + if (!tsv_string) { + tsv_string = undefined; + } const xls_result = await connection.runScript("xls_from_csv.py", - {"-p0": {"filename": "selected.tsv", "blob": new Blob([csv_string], {type: "text/tab-separated-values;charset=utf-8"})}}); + {"-p0": {"filename": "selected.tsv", "blob": new Blob([tsv_string], {type: "text/tab-separated-values;charset=utf-8"})}}); const code = xls_result.getElementsByTagName("script")[0].getAttribute("code"); if (parseInt(code) > 0) { throw ("An error occurred during execution of the server-side script:\n" @@ -299,11 +309,14 @@ async function downloadXLS(xls_link) { + xls_result.getElementsByTagName("script")[0].outerHTML); } - // set the href in order to download the file and simulate a click. - caosdb_table_export._go_to_script_results(xls_link, filename); + // trigger download of generated file + caosdb_table_export.go_to_script_results(filename); + + } catch (e) { globalError(e); } finally { + removeAllWaitingNotifications($("#downloadModal")[0]); // restore the old click handler - hence a new file is generated with each click. xls_link.setAttribute("onClick", onClickValue); } @@ -314,4 +327,5 @@ async function downloadXLS(xls_link) { $(document).ready(function () { caosdb_modules.register(caosdb_table_export); + caosdb_modules.register(ext_file_download); }); diff --git a/src/core/js/footer.js b/src/core/js/footer.js index 79f3ed0ecb382534d7c997ce0bc230178792f677..c48c5cf19c405aea326b36aba2868008466a8329 100644 --- a/src/core/js/footer.js +++ b/src/core/js/footer.js @@ -26,7 +26,7 @@ * Call initially. * * TODO refactor to async function for better readability. - * @return + * @return something */ function footer_initOnDocumentReady() { diff --git a/src/core/js/form_elements.js b/src/core/js/form_elements.js index 0014f4db452a55839edaa860d7fa49f98529434e..d4cd4234a28953140fcc1f62104e2c43a3460cdb 100644 --- a/src/core/js/form_elements.js +++ b/src/core/js/form_elements.js @@ -9,8 +9,7 @@ * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * @@ -26,8 +25,6 @@ * form_elements module for reusable form elemenst which already have a basic * css styling. * - * @version 0.2 - * * IMPORTANCE CONCEPTS * * FIELD - an HTMLElement which wraps a LABEL element (the fields name) and the @@ -49,24 +46,53 @@ * SUBFORM - an HTMLElement which contains FIELDS and other SUBFORMS. SUBFORMS * can be used to nest FIELDS, which is not supported by HTML5 but allows only * for flat key-value pairs. - */ - -/** - * The configuration for double, integer, date input elements. - * - * @typedef {object} input_config - * @property {string} name - * @property {string} type - * @property {string} label - */ - -/** - * The configuration for reference_select input fields - * - * TODO * + * @version 0.2 + * @exports form_elements */ var form_elements = new function () { + /** + * Config for an alert + * + * @typedef {object} AlertConfig + * @property {string} [title] - an optional title for the alert. + * @property {string} [severity="danger"] - a bootstrap class suffix. Other + * examples: warning, info + * @property {string} message - informs the user what they are about to do. + * @property {function} proceed_callback - the function which is called + * then the user hits the "Proceed" button. + * @property {function} [cancel_callback] - a callback which is called then + * the cancel button is clicked. By default, only the alert is being + * closed an nothing happens. + * @property {string} [proceed_text="Proceed"] - the text on the proceed button. + * @property {string} [cancel_text="Cancel"] - the text on the cancel button. + * @property {string} [remember_my_decision_id] - if this parameter is + * present, a checkbox is appended to the alert ("Don't ask me + * again."). If the checkbox is checked the next time the make_alert + * function is called with the same remember_my_decision_id is created, + * the alert won't show up and the proceed_callback is called without + * any user interaction. + * @property {string} [remember_my_decision_text="Don't ask me again."] - + * label text for the checkbox. + * @property {HTMLElement} [proceed_button] - an optional custom proceed + * button. + * @property {HTMLElement} [cancel_button] - an optional custom cancel + * button. + */ + + + /** + * The configuration for double, integer, date input elements. + * + * There are specializations of this configuration object. See + * {@link ReferenceDropDownConfig} + * + * @typedef {object} FieldConfig + * @property {string} name + * @property {string} type + * @property {string} label + * @see {@link ReferenceDropDownConfig} + */ this.version = "0.1"; this.dependencies = ["log", "caosdb_utils", "markdown"]; @@ -164,37 +190,119 @@ var form_elements = new function () { } + this._get_alert_decision = function (key) { + return localStorage["form_elements.alert_decision." + key]; + } + + this._set_alert_decision = function (key, val) { + localStorage["form_elements.alert_decision." + key] = val; + } + /** - * (Re-)set this module's functions to standard implementation. + * Make an alert, that is a dialog which can intercept a function call and + * asks the user to proceed or cancel. + * + * @param {AlertConfig} config + * @return {HTMLElement} */ - this._init_functions = function () { + this.make_alert = function (config) { + caosdb_utils.assert_string(config.message, "config param `message`"); + caosdb_utils.assert_type(config.proceed_callback, "function", + "config param `proceed_callback`"); + + // define some defaults. + const title = config.title ? `<h4>${config.title}</h4>` : ""; + const proceed_text = config.proceed_text || "Proceed"; + const cancel_text = config.cancel_text || "Cancel"; + const severity = config.severity || "danger"; + const remember = !!config.remember_my_decision_id || false; + + // check if alert should be created at all + if (remember) { + var result = this._get_alert_decision(config.remember_my_decision_id); + if (result == "proceed") { + // call callback asyncronously and return + (async function(){ config.proceed_callback(); })(); + return undefined; + } + } - this.init = function () { - this.logger.trace("enter init"); + // create the alert + const _alert = $(`<div class="alert alert-${severity} + alert-dismissible fade in caosdb-f-form-elements-alert" role="alert">${title} + <p>${config.message}</p> + </div>`); + + // create the "Don't ask me again" checkbox + var checkbox = undefined; + if (remember) { + const remember_my_decision_text = config.remember_my_decision_text + || "Don't ask me again."; + checkbox = $(`<p class="checkbox"><label> + <input type="checkbox"/> ${remember_my_decision_text}</label></p>`); + _alert.append(checkbox); } - /** - * Return an OPTION element with entity reference. - * - * The OPTION element for a SELECT form input shows a short - * summary/description of an entity and has the entity's id as value. - * - * If the `desc` parameter is undefined, the entity_id is shown - * instead. - * - * @param {string} entity_id - the entity's id. - * @param {string} [desc] - the description for the entity. - * @returns {HTMLElement} OPTION element. - */ - this.make_reference_option = function (entity_id, desc) { - caosdb_utils.assert_string(entity_id, "param `entity_id`"); - if (typeof desc == "undefined") { - desc = entity_id; + + // create buttons ... + const cancel_button = config.cancel_button || $(`<button type="button" class="btn btn-default caosdb-f-btn-alert-cancel">${cancel_text}</button>`); + const proceed_button = config.proceed_button || $(`<button type="button" class="btn btn-${severity} caosdb-f-btn-alert-proceed">${proceed_text}</button>`); + _alert.append($("<p/>").append([proceed_button, cancel_button])); + + + // ... and bind callbacks to the buttons. + cancel_button.click(() => { + $(_alert).alert('close'); + if (typeof config.cancel_callback == "function") { + config.cancel_callback(); } - var opt_str = '<option value="' + entity_id + '">' + desc + - "</option>"; - return $(opt_str)[0]; + }); + proceed_button.click(() => { + if (remember && checkbox.find("input[type='checkbox']").is(":checked")) { + // store this decision. + form_elements._set_alert_decision(config.remember_my_decision_id, + "proceed"); + } + $(_alert).alert('close'); + config.proceed_callback(); + }); + + return _alert[0]; + } + + + this.init = function () { + this.logger.trace("enter init"); + } + + /** + * Return an OPTION element with entity reference. + * + * The OPTION element for a SELECT form input shows a short + * summary/description of an entity and has the entity's id as value. + * + * If the `desc` parameter is undefined, the entity_id is shown + * instead. + * + * @param {string} entity_id - the entity's id. + * @param {string} [desc] - the description for the entity. + * @returns {HTMLElement} OPTION element. + */ + this.make_reference_option = function (entity_id, desc) { + caosdb_utils.assert_string(entity_id, "param `entity_id`"); + if (typeof desc == "undefined") { + desc = entity_id; } + var opt_str = '<option value="' + entity_id + '">' + desc + + "</option>"; + return $(opt_str)[0]; + } + + + /** + * (Re-)set this module's functions to standard implementation. + */ + this._init_functions = function () { /** * Return SELECT form element with entity references. @@ -244,8 +352,6 @@ var form_elements = new function () { } /** - * @typedef {option} ReferenceDropDownConfig - * * Configuration object for a drop down menu for selecting references. * `make_reference_drop_down` generates such a drop down menu using a * SELECT input with the references as its OPTION elements. @@ -263,6 +369,10 @@ var form_elements = new function () { * defined by `label`. If the `label` property is undefined, the `name` * is shown instead. * + * The ReferenceDropDownConfig is a specialisation of a + * {@link FieldConfig}. + * + * @typedef {option} ReferenceDropDownConfig * @property {string} name - The name of the select input. * @property {string} query - Query for entities. * @property {function} [make_value] - Call-back for the generation of @@ -277,129 +387,9 @@ var form_elements = new function () { * undefined. This property is used by `make_form_field` to decide * which type of field is to be generated. * + * @see {@link FieldConfig} */ - /** - * Search and retrieve entities and create a SELECT from element. - * - * @param {ReferenceDropDownConfig} config - all necessary parameters - * for the configuration. - * @returns {HTMLElement} SELECT element. - */ - this.make_reference_drop_down = function (config) { - let ret = $(this._make_field_wrapper(config.name)); - let label = this._make_input_label_str(config); - let loading = $('<div class="caosdb-f-field-not-ready">loading...</div>'); - let input_col = $('<div class="col-sm-9"/>'); - - input_col.append(loading); - this._query(config.query).then(async function (entities) { - let select = $(await form_elements.make_reference_select( - entities, config.make_desc, config.make_value, config.multiple, - config.value)); - select.attr("name", config.name); - loading.remove(); - input_col.append(select); - form_elements.init_select_picker(ret[0], config.value); - ret[0].dispatchEvent(form_elements.field_ready_event); - select.change(function () { - ret[0].dispatchEvent(form_elements.field_changed_event); - }); - }).catch(err => { - form_elements.logger.error(err); - loading.remove(); - input_col.append(err); - ret[0].dispatchEvent(form_elements.field_error_event); - }); - - return ret.append(label, input_col)[0]; - } - - - this.init_select_picker = function (field, value) { - caosdb_utils.assert_html_element(field, "parameter `field`"); - const select = $(field).find("select")[0]; - const select_picker_options = {}; - if ($(select).prop("multiple")) { - select_picker_options["actionsBox"] = true; - } - if ($(select).find("option").length > 8) { - select_picker_options["liveSearch"] = true; - select_picker_options["liveSearchNormalize"] = true; - select_picker_options["liveSearchPlaceholder"] = "search..."; - } - $(select).selectpicker(select_picker_options); - $(select).selectpicker("val", value); - this.init_actions_box(field); - } - - - this.init_actions_box = function (field) { - this.logger.trace("enter init_actions_box", field); - caosdb_utils.assert_html_element(field, "parameter `field`"); - const select = $(field).find("select"); - var actions_box = select.siblings().find(".bs-actionsbox"); - if (actions_box.length === 0) { - actions_box = $(`<div class="bs-actionsbox"> - <div class="btn-group btn-group-sm btn-block"> - <button type="button" class="actions-btn btn-default bs-deselect-all btn btn-light">None</button> - </div> - </div>`) - .hide(); - - select - .siblings(".dropdown-menu") - .prepend(actions_box); - - field.addEventListener( - form_elements.field_changed_event.type, - (e) => { - if (form_elements.is_set(field)) { - actions_box.show(); - } else { - actions_box.hide(); - } - }, true); - - actions_box - .find(".bs-deselect-all") - .click((e) => { - select.val(null) - .selectpicker("render") - .parent().toggleClass("open", false); - select[0].dispatchEvent(form_elements.field_changed_event); - }); - } - } - - /** - * Return a promise which resolves with the field when the field is ready. - * - * This function is especially useful if the caller can not be sure if - * the field_ready_event has been dispatched already and the field is - * ready or if the fields creation is still pending. - * - * @param {HTMLElement} field - * @return {Promise} the field-ready promise - */ - this.field_ready = function (field) { - // TODO add support for field name (string) as field parameter - // TODO check type of param field (not an array!) - caosdb_utils.assert_html_element(field, "parameter `field`"); - return new Promise(function (resolve, reject) { - try { - if (!$(field).hasClass("caosdb-f-field-not-ready") && $(field).find(".caosdb-f-field-not-ready").length === 0) { - resolve(field); - } else { - field.addEventListener(form_elements.field_ready_event.type, - (e) => resolve(e.target), true); - } - } catch (err) { - reject(err); - } - }); - } - this._query = async function (q) { const result = await query(q); this.logger.debug("query returned", result); @@ -421,802 +411,978 @@ var form_elements = new function () { return this.parse_script_result(result); } - this.parse_script_result = function (result) { - console.log(result); - const scriptNode = result.evaluate("/Response/script", result, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; - const code = result.evaluate("@code", scriptNode, null, XPathResult.STRING_TYPE, null).stringValue; + } - const call = result.evaluate("call", scriptNode, null, XPathResult.STRING_TYPE, null).stringValue; - const stderr = result.evaluate("stderr", scriptNode, null, XPathResult.STRING_TYPE, null).stringValue; - const stdout = result.evaluate("stdout", scriptNode, null, XPathResult.STRING_TYPE, null).stringValue; + /** + * @typedef {object} ScriptingResult + * @property {string} code + * @property {string} call + * @property {string} stdout + * @property {string} stderr + */ - return { - "code": code, - "call": call, - "stdout": stdout, - "stderr": stderr - }; + /** + * Bla, TODO + * + * @param {XMLDocument} result + * @return {ScriptingResult} + */ + this.parse_script_result = function (result) { + console.log(result); + const scriptNode = result.evaluate("/Response/script", result, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; + const code = result.evaluate("@code", scriptNode, null, XPathResult.STRING_TYPE, null).stringValue; + + const call = result.evaluate("call", scriptNode, null, XPathResult.STRING_TYPE, null).stringValue; + const stderr = result.evaluate("stderr", scriptNode, null, XPathResult.STRING_TYPE, null).stringValue; + const stdout = result.evaluate("stdout", scriptNode, null, XPathResult.STRING_TYPE, null).stringValue; + + const ret = { + "code": code, + "call": call, + "stdout": stdout, + "stderr": stderr + }; + + return ret; + } + + /** + * Search and retrieve entities and create a SELECT from element. + * + * @param {ReferenceDropDownConfig} config - all necessary parameters + * for the configuration. + * @returns {HTMLElement} SELECT element. + */ + this.make_reference_drop_down = function (config) { + let ret = $(this._make_field_wrapper(config.name)); + let label = this._make_input_label_str(config); + let loading = $('<div class="caosdb-f-field-not-ready">loading...</div>'); + let input_col = $('<div class="col-sm-9"/>'); + + input_col.append(loading); + this._query(config.query).then(async function (entities) { + let select = $(await form_elements.make_reference_select( + entities, config.make_desc, config.make_value, config.multiple, + config.value)); + select.attr("name", config.name); + loading.remove(); + input_col.append(select); + form_elements.init_select_picker(ret[0], config.value); + ret[0].dispatchEvent(form_elements.field_ready_event); + select.change(function () { + ret[0].dispatchEvent(form_elements.field_changed_event); + }); + }).catch(err => { + form_elements.logger.error(err); + loading.remove(); + input_col.append(err); + ret[0].dispatchEvent(form_elements.field_error_event); + }); + + return ret.append(label, input_col)[0]; + } + + + /** + * Test 16 + */ + this.init_select_picker = function (field, value) { + caosdb_utils.assert_html_element(field, "parameter `field`"); + const select = $(field).find("select")[0]; + const select_picker_options = {}; + if ($(select).prop("multiple")) { + select_picker_options["actionsBox"] = true; } + if ($(select).find("option").length > 8) { + select_picker_options["liveSearch"] = true; + select_picker_options["liveSearchNormalize"] = true; + select_picker_options["liveSearchPlaceholder"] = "search..."; + } + $(select).selectpicker(select_picker_options); + $(select).selectpicker("val", value); + this.init_actions_box(field); + } - /** - * generate a java script object representation of a form - */ - this.form_to_object = function (form) { - this.logger.trace("entity form_to_json", form); - caosdb_utils.assert_html_element(form, "parameter `form`"); - const _to_json = (element, data) => { - this.logger.trace("enter element_to_json", element, data); + /** + * Test 17 + */ + this.init_actions_box = function (field) { + this.logger.trace("enter init_actions_box", field); + caosdb_utils.assert_html_element(field, "parameter `field`"); + const select = $(field).find("select"); + var actions_box = select.siblings().find(".bs-actionsbox"); + if (actions_box.length === 0) { + actions_box = $(`<div class="bs-actionsbox"> + <div class="btn-group btn-group-sm btn-block"> + <button type="button" class="actions-btn btn-default bs-deselect-all btn btn-light">None</button> + </div> + </div>`) + .hide(); + + select + .siblings(".dropdown-menu") + .prepend(actions_box); + + field.addEventListener( + form_elements.field_changed_event.type, + (e) => { + if (form_elements.is_set(field)) { + actions_box.show(); + } else { + actions_box.hide(); + } + }, true); + + actions_box + .find(".bs-deselect-all") + .click((e) => { + select.val(null) + .selectpicker("render") + .parent().toggleClass("open", false); + select[0].dispatchEvent(form_elements.field_changed_event); + }); + } + } + + /** + * Return a promise which resolves with the field when the field is ready. + * + * This function is especially useful if the caller can not be sure if + * the field_ready_event has been dispatched already and the field is + * ready or if the fields creation is still pending. + * + * @param {HTMLElement} field + * @return {Promise} the field-ready promise + */ + this.field_ready = function (field) { + // TODO add support for field name (string) as field parameter + // TODO check type of param field (not an array!) + caosdb_utils.assert_html_element(field, "parameter `field`"); + return new Promise(function (resolve, reject) { + try { + if (!$(field).hasClass("caosdb-f-field-not-ready") && $(field).find(".caosdb-f-field-not-ready").length === 0) { + resolve(field); + } else { + field.addEventListener(form_elements.field_ready_event.type, + (e) => resolve(e.target), true); + } + } catch (err) { + reject(err); + } + }); + } + + /** + * generate a java script object representation of a form + * + * @function + */ + this.form_to_object = function (form) { + this.logger.trace("entity form_to_json", form); + caosdb_utils.assert_html_element(form, "parameter `form`"); + + const _to_json = (element, data) => { + this.logger.trace("enter element_to_json", element, data); - for (const child of element.children) { - // ignore disabled fields and subforms - if ($(child).hasClass("caosdb-f-field-disabled")) { - continue; + for (const child of element.children) { + // ignore disabled fields and subforms + if ($(child).hasClass("caosdb-f-field-disabled")) { + continue; + } + const name = $(child).attr("name"); + const is_subform = $(child).hasClass("caosdb-f-form-elements-subform"); + if (is_subform) { + const subform = $(child).data("subform-name"); + // recursive + var subform_obj = _to_json(child, {}); + if (typeof data[subform] === "undefined") { + data[subform] = subform_obj; + } else if (Array.isArray(data[subform])) { + data[subform].push(subform_obj); + } else { + data[subform] = [data[subform], subform_obj] } - const name = $(child).attr("name"); - const is_subform = $(child).hasClass("caosdb-f-form-elements-subform"); - if (is_subform) { - const subform = $(child).data("subform-name"); - // recursive - var subform_obj = _to_json(child, {}); - if (typeof data[subform] === "undefined") { - data[subform] = subform_obj; - } else if (Array.isArray(data[subform])) { - data[subform].push(subform_obj); + } else if (name && name !== "") { + // input elements + const not_checkbox = !$(child).is(":checkbox"); + if (not_checkbox || $(child).is(":checked")) { + // checked or not a checkbox + var value = $(child).val(); + if (typeof data[name] === "undefined") { + data[name] = value; + } else if (Array.isArray(data[name])) { + data[name].push(value); } else { - data[subform] = [data[subform], subform_obj] + data[name] = [data[name], value] } - } else if (name && name !== "") { - // input elements - const not_checkbox = !$(child).is(":checkbox"); - if (not_checkbox || $(child).is(":checked")) { - // checked or not a checkbox - var value = $(child).val(); - if (typeof data[name] === "undefined") { - data[name] = value; - } else if (Array.isArray(data[name])) { - data[name].push(value); - } else { - data[name] = [data[name], value] - } - } else { - // TODO checkbox - } - } else if (child.children.length > 0) { - // recursive - _to_json(child, data); + } else { + // TODO checkbox } + } else if (child.children.length > 0) { + // recursive + _to_json(child, data); } + } - this.logger.trace("leave element_to_json", element, data); - return data; - }; - - const ret = _to_json(form, {}); - this.logger.trace("leave form_to_json", ret); - return ret; - } + this.logger.trace("leave element_to_json", element, data); + return data; + }; - this.make_submit_button = function () { - var ret = $('<button class="caosdb-f-form-elements-submit-button btn btn-primary" type="submit">Submit</button>'); - return ret[0]; - } + const ret = _to_json(form, {}); + this.logger.trace("leave form_to_json", ret); + return ret; + } - this.make_cancel_button = function (form) { - var ret = $('<button class="caosdb-f-form-elements-cancel-button btn btn-primary" type="button">Cancel</button>'); - ret.on("click", e => { - this.logger.debug("cancel form", e, form); - form.dispatchEvent(this.cancel_form_event); - }); - return ret[0]; - } + this.make_submit_button = function () { + var ret = $('<button class="caosdb-f-form-elements-submit-button btn btn-primary" type="submit">Submit</button>'); + return ret[0]; + } - /** - * TODO make syncronous - */ - this.make_form_field = async function (config) { - caosdb_utils.assert_type(config, "object", "param `config`"); - caosdb_utils.assert_string(config.type, "`config.type` of param `config`"); - - var field = undefined; - const type = config.type; - if (type === "date") { - field = this.make_date_input(config); - } else if (type === "checkbox") { - field = this.make_checkbox_input(config); - } else if (type === "text") { - field = this.make_text_input(config); - } else if (type === "double") { - field = this.make_double_input(config); - } else if (type === "integer") { - field = this.make_integer_input(config); - } else if (type === "range") { - field = await this.make_range_input(config); - } else if (type === "reference_drop_down") { - field = this.make_reference_drop_down(config); - } else if (type === "subform") { - // TODO handle cache and required for subforms - return await this.make_subform(config); - } else { - throw new TypeError("undefined field type `" + type + "`"); - } + this.make_cancel_button = function (form) { + var ret = $('<button class="caosdb-f-form-elements-cancel-button btn btn-primary" type="button">Cancel</button>'); + ret.on("click", e => { + this.logger.debug("cancel form", e, form); + form.dispatchEvent(this.cancel_form_event); + }); + return ret[0]; + } - if (config.required) { - this.set_required(field); - } - if (config.cached) { - this.set_cached(field); - } - if (config.help) { - this.add_help(field, config.help); - } + /** + * TODO make syncronous + * + * @return {HTMLElement} + */ + this.make_form_field = async function (config) { + caosdb_utils.assert_type(config, "object", "param `config`"); + caosdb_utils.assert_string(config.type, "`config.type` of param `config`"); + + var field = undefined; + const type = config.type; + if (type === "date") { + field = this.make_date_input(config); + } else if (type === "checkbox") { + field = this.make_checkbox_input(config); + } else if (type === "text") { + field = this.make_text_input(config); + } else if (type === "double") { + field = this.make_double_input(config); + } else if (type === "integer") { + field = this.make_integer_input(config); + } else if (type === "range") { + field = await this.make_range_input(config); + } else if (type === "reference_drop_down") { + field = this.make_reference_drop_down(config); + } else if (type === "subform") { + // TODO handle cache and required for subforms + return await this.make_subform(config); + } else { + throw new TypeError("undefined field type `" + type + "`"); + } - return field; + if (config.required) { + this.set_required(field); + } + if (config.cached) { + this.set_cached(field); + } + if (config.help) { + this.add_help(field, config.help); } + return field; + } - this.add_help = function (field, config) { - var help_button = $('<span data-trigger="click focus" data-toggle="popover" class="caosdb-f-form-help pull-right glyphicon glyphicon-info-sign"/>') - .css({ - "cursor": "pointer" - }); - if (typeof config === "string" || config instanceof String) { - help_button.attr("data-content", config); - help_button.popover(); - } else { - help_button.popover(config); - } + this.add_help = function (field, config) { + var help_button = $('<span data-trigger="click focus" data-toggle="popover" class="caosdb-f-form-help pull-right glyphicon glyphicon-info-sign"/>') + .css({ + "cursor": "pointer" + }); + if (typeof config === "string" || config instanceof String) { + help_button.attr("data-content", config); + help_button.popover(); + } else { + help_button.popover(config); + } - var label = $(field).children("label"); - if (label.length > 0) { - help_button.css({ - "margin-left": "4px" - }); - label.first().append(help_button); - } else { - $(field).append(help_button); - } + + var label = $(field).children("label"); + if (label.length > 0) { + help_button.css({ + "margin-left": "4px" + }); + label.first().append(help_button); + } else { + $(field).append(help_button); } + } - this.make_heading = function (config) { - if (typeof config.header === "undefined") { - return; - } else if (typeof config.header === "string" || config.header instanceof String) { - return $('<div class="caosdb-f-form-header h3">' + config.header + '</div>')[0]; - } - caosdb_utils.assert_html_element(config.header, "member `header` of parameter `config`"); - return config.header; + this.make_heading = function (config) { + if (typeof config.header === "undefined") { + return; + } else if (typeof config.header === "string" || config.header instanceof String) { + return $('<div class="caosdb-f-form-header h3">' + config.header + '</div>')[0]; } + caosdb_utils.assert_html_element(config.header, "member `header` of parameter `config`"); + return config.header; + } - this.make_form_wrapper = function (form, config) { - var wrapper = $('<div class="caosdb-f-form-wrapper"/>'); + this.make_form_wrapper = function (form, config) { + var wrapper = $('<div class="caosdb-f-form-wrapper"/>'); - var header = this.make_heading(config); - wrapper.append(header); + var header = this.make_heading(config); + wrapper.append(header); - var loading = $('<div>loading...</div>'); - var logger = this.logger; - var cancel = (e) => { - logger.trace("cancel form", e); - wrapper.remove(); - }; + var loading = $('<div>loading...</div>'); + var logger = this.logger; + var cancel = (e) => { + logger.trace("cancel form", e); + wrapper.remove(); + }; - wrapper.append(loading); + wrapper.append(loading); - Promise.resolve(form).then(form => { - // form ready - loading.remove(); - wrapper.append(form); - wrapper[0].dispatchEvent(this.form_ready_event); + Promise.resolve(form).then(form => { + // form ready + loading.remove(); + wrapper.append(form); + wrapper[0].dispatchEvent(this.form_ready_event); - }).catch(err => { - logger.error("form loading error", err); - loading.remove(); - wrapper.append(err); - }); + }).catch(err => { + logger.error("form loading error", err); + loading.remove(); + wrapper.append(err); + }); - wrapper[0].addEventListener(this.cancel_form_event.type, cancel, true); + wrapper[0].addEventListener(this.cancel_form_event.type, cancel, true); - return wrapper[0]; - } + return wrapper[0]; + } - this.make_form = function (config) { - var form = undefined; + /** + * Configuration objects which are passed to {@link make_form}. + * + * Note: either the `script` or the `name` property must be defined. If the former is defined, the latter will be overriden. + * + * @typedef {object} FormConfig + * + * @property {FieldConfig[]} fields - array of fields. The order is the + * order in which they appear in the resulting form. + * @property {string} [script] - if present the form will call a + * server-side script on submission. + * @property {string} [name] - The name of the form. This is being + * overridden by the `script` parameter if present. + * @property {function} [submit] - a callback which handles the submission + * of the form. This parameter is being overridden if the `script` + * parameter is present. + */ - if (config.script) { - form = this.make_script_form(config, config.script); - } else { - form = this.make_generic_form(config); - } - var wrapper = this.make_form_wrapper(form, config); - return wrapper; - } + /** + * Create a form. + * + * The returned element is a container which will eventually contain a HTML + * form element. The container emits a {@link form_ready_event} when the + * form is ready. + * + * @param {FormConfig} config + * @return {HTMLElement} + */ + this.make_form = function (config) { + var form = undefined; - /** - * TODO make syncronous - */ - this.make_subform = async function (config) { - this.logger.trace("enter make_subform"); - caosdb_utils.assert_type(config, "object", "param `config`"); - caosdb_utils.assert_string(config.name, "`config.name` of param `config`"); - caosdb_utils.assert_array(config.fields, "`config.fields` of param `config`"); + if (config.script) { + form = this.make_script_form(config, config.script); + } else { + form = this.make_generic_form(config); + } + var wrapper = this.make_form_wrapper(form, config); + return wrapper; + } - const name = config.name; - var form = $('<div data-subform-name="' + name + '" class="caosdb-f-form-elements-subform"/>'); + /** + * TODO make syncronous + */ + this.make_subform = async function (config) { + this.logger.trace("enter make_subform"); + caosdb_utils.assert_type(config, "object", "param `config`"); + caosdb_utils.assert_string(config.name, "`config.name` of param `config`"); + caosdb_utils.assert_array(config.fields, "`config.fields` of param `config`"); + + const name = config.name; + var form = $('<div data-subform-name="' + name + '" class="caosdb-f-form-elements-subform"/>'); + + for (let field of config.fields) { + this.logger.trace("add subform field", field); + let elem = await this.make_form_field(field); + form.append(elem); + } - for (let field of config.fields) { - this.logger.trace("add subform field", field); - let elem = await this.make_form_field(field); - form.append(elem); - } + this.logger.trace("leave make_subform", form[0]); + return form[0]; + } - this.logger.trace("leave make_subform", form[0]); - return form[0]; + this.dismiss_form = function (form) { + if (form.tagName === "FORM") { + form.dispatchEvent(this.cancel_form_event); } - - this.dismiss_form = function (form) { - if (form.tagName === "FORM") { - form.dispatchEvent(this.cancel_form_event); - } - var _form = $(form).find("form"); - if (_form.length > 0) { - _form[0].dispatchEvent(this.cancel_form_event); - } + var _form = $(form).find("form"); + if (_form.length > 0) { + _form[0].dispatchEvent(this.cancel_form_event); } + } - this.enable_group = function (form, group) { - this.enable_fields(this.get_group_fields(form, group)); - } + this.enable_group = function (form, group) { + this.enable_fields(this.get_group_fields(form, group)); + } - this.disable_group = function (form, group) { - this.disable_fields(this.get_group_fields(form, group)); - } + this.disable_group = function (form, group) { + this.disable_fields(this.get_group_fields(form, group)); + } - this.get_group_fields = function (form, group) { - return $(form).find(".caosdb-f-field[data-groups*='(" + group + ")']").toArray(); - } + this.get_group_fields = function (form, group) { + return $(form).find(".caosdb-f-field[data-groups*='(" + group + ")']").toArray(); + } - /** - * Return an array of field with name - * - * @param {string} name - the field name - * @return {HTMLElement[]} array of fields - */ - this.get_fields = function (form, name) { - caosdb_utils.assert_html_element(form, "parameter `form`"); - caosdb_utils.assert_string(name, "parameter `name`"); - return $(form).find(".caosdb-f-field[data-field-name='" + name + "']").toArray(); - } + /** + * Return an array of field with name + * + * @param {string} name - the field name + * @return {HTMLElement[]} array of fields + */ + this.get_fields = function (form, name) { + caosdb_utils.assert_html_element(form, "parameter `form`"); + caosdb_utils.assert_string(name, "parameter `name`"); + return $(form).find(".caosdb-f-field[data-field-name='" + name + "']").toArray(); + } - this.add_field_to_group = function (field, group) { - this.logger.trace("enter add_field_to_group", field, group); - var groups = ($(field).attr("data-groups") ? $(field).attr("data-groups") : "") + "(" + group + ")"; - $(field).attr("data-groups", groups); - } + this.add_field_to_group = function (field, group) { + this.logger.trace("enter add_field_to_group", field, group); + var groups = ($(field).attr("data-groups") ? $(field).attr("data-groups") : "") + "(" + group + ")"; + $(field).attr("data-groups", groups); + } - this.disable_fields = function (fields) { - $(fields).toggleClass("caosdb-f-field-disabled", true).hide(); - for (const field of $(fields)) { - field.dispatchEvent(this.field_disabled_event); - } + this.disable_fields = function (fields) { + $(fields).toggleClass("caosdb-f-field-disabled", true).hide(); + for (const field of $(fields)) { + field.dispatchEvent(this.field_disabled_event); } + } - this.enable_fields = function (fields) { - $(fields).toggleClass("caosdb-f-field-disabled", false).show(); - for (const field of $(fields)) { - field.dispatchEvent(this.field_enabled_event); - } + this.enable_fields = function (fields) { + $(fields).toggleClass("caosdb-f-field-disabled", false).show(); + for (const field of $(fields)) { + field.dispatchEvent(this.field_enabled_event); } + } - this.enable_name = function (form, name) { - this.enable_fields($(form).find(".caosdb-f-field[data-field-name='" + name + "']").toArray()); - } + this.enable_name = function (form, name) { + this.enable_fields($(form).find(".caosdb-f-field[data-field-name='" + name + "']").toArray()); + } - this.disable_name = function (form, name) { - this.disable_fields($(form).find(".caosdb-f-field[data-field-name='" + name + "']").toArray()); - } + this.disable_name = function (form, name) { + this.disable_fields($(form).find(".caosdb-f-field[data-field-name='" + name + "']").toArray()); + } - this.make_script_form = async function (config, script) { - this.logger.trace("enter make_script_form"); + this.make_script_form = async function (config, script) { + this.logger.trace("enter make_script_form"); - const submit_callback = async function (form) { - form = $(form); + const submit_callback = async function (form) { + form = $(form); - // actually submit the form - var response = await form_elements._run_script(script, form); - var result = []; + // actually submit the form + var response = await form_elements._run_script(script, form); + var result = []; - if (response.code === "0") { - // handle success - result.push(form_elements.make_success_message(response.stdout)); - return result; + if (response.code === "0") { + // handle success + result.push(form_elements.make_success_message(response.stdout)); + return result; - } else { - // handle scripting error - result.push(form_elements.make_error_message(response.call)); - result.push(form_elements.make_error_message(response.stderr)); - throw result; - } - }; + } else { + // handle scripting error + result.push(form_elements.make_error_message(response.call)); + result.push(form_elements.make_error_message(response.stderr)); + throw result; + } + }; + + this.logger.trace("leave make_script_form"); + const new_config = $.extend({}, { + name: script, + submit: submit_callback + }, config); + return await this.make_generic_form(new_config); + } - this.logger.trace("leave make_script_form"); - const new_config = $.extend({}, { - name: script, - submit: submit_callback - }, config); - return await this.make_generic_form(new_config); - } + /** + * Return a generic form, bind the config.submit to the submit event + * of the form. + * + * The `config.fields` array may contain `form_elements.field_config` + * objects or HTMLElements. + * + * TODO + */ + this.make_generic_form = async function (config) { + this.logger.trace("enter make_generic_form"); - /** - * Return a generic form, bind the config.submit to the submit event - * of the form. - * - * The `config.fields` array may contain `form_elements.field_config` - * objects or HTMLElements. - * - * TODO - */ - this.make_generic_form = async function (config) { - this.logger.trace("enter make_generic_form"); + caosdb_utils.assert_type(config, "object", "param `config`"); + caosdb_utils.assert_string(config.name, "`config.name` of param `config`", true); + caosdb_utils.assert_array(config.fields, "`config.fields` of param `config`"); - caosdb_utils.assert_type(config, "object", "param `config`"); - caosdb_utils.assert_string(config.name, "`config.name` of param `config`", true); - caosdb_utils.assert_array(config.fields, "`config.fields` of param `config`"); + const form = $('<form class="form-horizontal" action="#" method="post" />'); - const form = $('<form class="form-horizontal" action="#" method="post" />'); + // set name + if (config.name) { + form.attr("name", config.name); + } - // set name - if (config.name) { - form.attr("name", config.name); + // add fields + for (let field of config.fields) { + this.logger.trace("add field", field); + if (field instanceof HTMLElement) { + form.append(field); + } else { + let elem = await this.make_form_field(field); + form.append(elem); } + } - // add fields - for (let field of config.fields) { - this.logger.trace("add field", field); - if (field instanceof HTMLElement) { - form.append(field); + // set groups + if (config.groups) { + for (let group of config.groups) { + this.logger.trace("add group", group); + for (let fieldname of group.fields) { + let field = form.find(".caosdb-f-field[data-field-name='" + fieldname + "']"); + this.logger.trace("set group", field, group); + this.add_field_to_group(field, group.name) + + } + // disable if necessary + if (typeof group.enabled === "undefined" || group.enabled) { + this.enable_group(form, group.name); } else { - let elem = await this.make_form_field(field); - form.append(elem); + this.disable_group(form, group.name); } } + } - // set groups - if (config.groups) { - for (let group of config.groups) { - this.logger.trace("add group", group); - for (let fieldname of group.fields) { - let field = form.find(".caosdb-f-field[data-field-name='" + fieldname + "']"); - this.logger.trace("set group", field, group); - this.add_field_to_group(field, group.name) + const footer = this.make_footer(); + form.append(footer); - } - // disable if necessary - if (typeof group.enabled === "undefined" || group.enabled) { - this.enable_group(form, group.name); - } else { - this.disable_group(form, group.name); - } - } + if (!(typeof config.submit === 'boolean' && config.submit === false)) { + // add submit button unless config.submit is false + footer.append(this.make_submit_button()); + } + form[0].addEventListener("submit", (e) => { + e.preventDefault(); + e.stopPropagation(); + if (form.find(".caosdb-f-form-submitting").length > 0) { + // do not submit twice + return; } - const footer = this.make_footer(); - form.append(footer); + this.logger.debug("submit form", e); - if (!(typeof config.submit === 'boolean' && config.submit === false)) { - // add submit button unless config.submit is false - footer.append(this.make_submit_button()); - } - form[0].addEventListener("submit", (e) => { - e.preventDefault(); - e.stopPropagation(); - if (form.find(".caosdb-f-form-submitting").length > 0) { - // do not submit twice - return; - } - - this.logger.debug("submit form", e); + form[0].dispatchEvent(this.submit_form_event); - form[0].dispatchEvent(this.submit_form_event); + form.find(":input").prop("disabled", true); + var submitting = form_elements.make_submitting_info(); + form.find(".caosdb-f-form-elements-footer").before(submitting); - form.find(":input").prop("disabled", true); - var submitting = form_elements.make_submitting_info(); - form.find(".caosdb-f-form-elements-footer").before(submitting); + form[0].addEventListener(this.form_success_event.type, (e) => { + submitting.remove(); + }, true); + form[0].addEventListener(this.form_error_event.type, (e) => { + submitting.remove(); + }, true); - form[0].addEventListener(this.form_success_event.type, (e) => { - submitting.remove(); - }, true); - form[0].addEventListener(this.form_error_event.type, (e) => { - submitting.remove(); - }, true); + // remove old messages + const error_handler = config.error; + const success_handler = config.success; + const submit_callback = config.submit; + form.find(".caosdb-f-form-elements-message").remove(); + if (typeof config.submit === "function") { + // wrap callback in async function + const _wrap_callback = async function () { + try { + var results = await submit_callback(form[0]); - // remove old messages - const error_handler = config.error; - const success_handler = config.success; - const submit_callback = config.submit; - form.find(".caosdb-f-form-elements-message").remove(); - if (typeof config.submit === "function") { - // wrap callback in async function - const _wrap_callback = async function () { - try { - var results = await submit_callback(form[0]); - - // success_handler - if (typeof success_handler === "function") { - var processed = await success_handler(form[0], results); - if (typeof processed !== "undefined") { - form_elements.show_results(form[0], processed); - } - } else { - form_elements.show_results(form[0], results); + // success_handler + if (typeof success_handler === "function") { + var processed = await success_handler(form[0], results); + if (typeof processed !== "undefined") { + form_elements.show_results(form[0], processed); } + } else { + form_elements.show_results(form[0], results); + } - form[0].dispatchEvent(form_elements.form_success_event); - } catch (err) { - - // error_handler - if (typeof error_handler === "function") { - var processed = await error_handler(form[0], err); - if (typeof processed !== "undefined") { - form_elements.show_results(form[0], processed); - } - } else { - form_elements.show_errors(form[0], err); - } + form[0].dispatchEvent(form_elements.form_success_event); + } catch (err) { - form[0].dispatchEvent(form_elements.form_error_event); + // error_handler + if (typeof error_handler === "function") { + var processed = await error_handler(form[0], err); + if (typeof processed !== "undefined") { + form_elements.show_results(form[0], processed); + } + } else { + form_elements.show_errors(form[0], err); } - }(); - } - return false; + form[0].dispatchEvent(form_elements.form_error_event); + } + }(); + } + return false; - }, true); - form[0].addEventListener(this.form_success_event.type, function (e) { - // remove submit button, show ok button - form.find("button[type='submit']").remove(); - form.find("button:contains('Cancel')").text("Ok").prop("disabled", false); - }, true); - form[0].addEventListener(this.form_error_event.type, function (e) { - // reenable inputs - form.find(":input").prop("disabled", false); - }, true); + }, true); - // add cancel button - $(footer).append(this.make_cancel_button(form[0])); + form[0].addEventListener(this.form_success_event.type, function (e) { + // remove submit button, show ok button + form.find("button[type='submit']").remove(); + form.find("button:contains('Cancel')").text("Ok").prop("disabled", false); + }, true); + form[0].addEventListener(this.form_error_event.type, function (e) { + // reenable inputs + form.find(":input").prop("disabled", false); + }, true); - // init caching for this form - form_elements.init_form_caching(config, form[0]); + // add cancel button + $(footer).append(this.make_cancel_button(form[0])); - // init validation - form_elements.init_validator(form[0]); + // init caching for this form + form_elements.init_form_caching(config, form[0]); - this.logger.trace("leave make_generic_form"); - return form[0]; - } + // init validation + form_elements.init_validator(form[0]); - this.init_form_caching = function (config, form) { - var default_config = { - "cache_event": form_elements.submit_form_event.type, - "cache_storage": localStorage - }; - var lconfig = $.extend({}, default_config, config); + this.logger.trace("leave make_generic_form"); + return form[0]; + } - this.logger.trace("init_form_caching", lconfig, form); + this.init_form_caching = function (config, form) { + var default_config = { + "cache_event": form_elements.submit_form_event.type, + "cache_storage": localStorage + }; + var lconfig = $.extend({}, default_config, config); - form.addEventListener(lconfig.cache_event, (e) => { - form_elements.cache_form(lconfig.cache_storage, form); - }, true); - form_elements.load_cached(lconfig.cache_storage, form); - } + this.logger.trace("init_form_caching", lconfig, form); - this.show_results = function (form, results) { - $(form).append(results); - } + form.addEventListener(lconfig.cache_event, (e) => { + form_elements.cache_form(lconfig.cache_storage, form); + }, true); + form_elements.load_cached(lconfig.cache_storage, form); + } - this.show_errors = function (form, errors) { - $(form).append(errors); - } + this.show_results = function (form, results) { + $(form).append(results); + } - this.make_footer = function () { - return $('<div class="text-right caosdb-f-form-elements-footer"/>') - .css({ - "margin": "20px", - }).append(this.make_required_marker()) - .append('<span style="margin-right: 4px; font-size: 11px">required field</span>')[0]; - } + this.show_errors = function (form, errors) { + $(form).append(errors); + } - this.make_error_message = function (message) { - return this.make_message(message, "error"); - } + this.make_footer = function () { + return $('<div class="text-right caosdb-f-form-elements-footer"/>') + .css({ + "margin": "20px", + }).append(this.make_required_marker()) + .append('<span style="margin-right: 4px; font-size: 11px">required field</span>')[0]; + } - this.make_success_message = function (message) { - return this.make_message(message, "success"); - } + this.make_error_message = function (message) { + return this.make_message(message, "error"); + } - this.make_submitting_info = function () { - // TODO styling - return $(this.make_message("Submitting... please wait. This might take some time.", "info")) - .toggleClass("h3", true) - .toggleClass("caosdb-f-form-submitting", true) - .toggleClass("text-right", true)[0]; - } + this.make_success_message = function (message) { + return this.make_message(message, "success"); + } - this.make_message = function (message, type) { - var ret = $('<div class="caosdb-f-form-elements-message"/>'); - if (type) { - ret.addClass("caosdb-f-form-elements-message-" + type); - } - return ret.append(markdown.textToHtml(message))[0]; + this.make_submitting_info = function () { + // TODO styling + return $(this.make_message("Submitting... please wait. This might take some time.", "info")) + .toggleClass("h3", true) + .toggleClass("caosdb-f-form-submitting", true) + .toggleClass("text-right", true)[0]; + } + + this.make_message = function (message, type) { + var ret = $('<div class="caosdb-f-form-elements-message"/>'); + if (type) { + ret.addClass("caosdb-f-form-elements-message-" + type); } + return ret.append(markdown.textToHtml(message))[0]; + } - /** - * TODO make syncronous - */ - this.make_range_input = async function (config) { - - // TODO - // 1. wrapp both inputs to separate it from the label into a container - // 2. make two rows for each input - // 3. make inline-block for all included elements - const from_config = $.extend({}, { - cached: config.cached, - required: config.required, - type: "double" - }, config.from); - const to_config = $.extend({}, { - cached: config.cached, - required: config.required, - type: "double" - }, config.to); - - const from_input = await this.make_form_field(from_config); - const to_input = await this.make_form_field(to_config); - - const ret = $(this._make_field_wrapper(config.name)); - if (config.label) { - ret.append(this._make_input_label_str(config)); - } + /** + * TODO make syncronous + */ + this.make_range_input = async function (config) { + + // TODO + // 1. wrapp both inputs to separate it from the label into a container + // 2. make two rows for each input + // 3. make inline-block for all included elements + const from_config = $.extend({}, { + cached: config.cached, + required: config.required, + type: "double" + }, config.from); + const to_config = $.extend({}, { + cached: config.cached, + required: config.required, + type: "double" + }, config.to); + + const from_input = await this.make_form_field(from_config); + const to_input = await this.make_form_field(to_config); + + const ret = $(this._make_field_wrapper(config.name)); + if (config.label) { + ret.append(this._make_input_label_str(config)); + } - ret.append(from_input); - ret.append(to_input); + ret.append(from_input); + ret.append(to_input); - // styling - $(from_input).toggleClass("form-group", false); - $(from_input).find(".col-sm-3").toggleClass("col-sm-3", false).toggleClass("col-sm-1"); - $(from_input).find(".col-sm-9").toggleClass("col-sm-9", false).toggleClass("col-sm-3"); - $(to_input).toggleClass("form-group", false); - $(to_input).find(".col-sm-3").toggleClass("col-sm-3", false).toggleClass("col-sm-1").toggleClass("col-sm-offset-1"); - $(to_input).find(".col-sm-9").toggleClass("col-sm-9", false).toggleClass("col-sm-3"); + // styling + $(from_input).toggleClass("form-group", false); + $(from_input).find(".col-sm-3").toggleClass("col-sm-3", false).toggleClass("col-sm-1"); + $(from_input).find(".col-sm-9").toggleClass("col-sm-9", false).toggleClass("col-sm-3"); + $(to_input).toggleClass("form-group", false); + $(to_input).find(".col-sm-3").toggleClass("col-sm-3", false).toggleClass("col-sm-1").toggleClass("col-sm-offset-1"); + $(to_input).find(".col-sm-9").toggleClass("col-sm-9", false).toggleClass("col-sm-3"); - return ret[0]; - } + return ret[0]; + } - /** - * Return a DIV with class `caosdb-f-field` and a data attribute - * `data-field-name` which contains the name. - * - * The DIV is used to wrap LABEL and INPUT elements of a form together. - * - * @param {string} name - the name of the field. - * @returns {HTMLElement} a DIV. - */ - this._make_field_wrapper = function (name) { - caosdb_utils.assert_string(name, "param `name`"); - return $('<div class="form-group caosdb-f-field" data-field-name="' + name + '" />') - .css({"padding": "0"})[0]; - } + /** + * Return a DIV with class `caosdb-f-field` and a data attribute + * `data-field-name` which contains the name. + * + * The DIV is used to wrap LABEL and INPUT elements of a form together. + * + * @param {string} name - the name of the field. + * @returns {HTMLElement} a DIV. + */ + this._make_field_wrapper = function (name) { + caosdb_utils.assert_string(name, "param `name`"); + return $('<div class="form-group caosdb-f-field" data-field-name="' + name + '" />') + .css({"padding": "0"})[0]; + } - this.make_date_input = function (config) { - return this._make_input(config); - } + this.make_date_input = function (config) { + return this._make_input(config); + } - this.make_text_input = function (config) { - return this._make_input(config); - } + this.make_text_input = function (config) { + return this._make_input(config); + } - /** - * Return an input field which accepts double values. - * - * `config.type` is set to "number" and overrides any other type. - * - * @param {form_elements.input_config} config. - * @returns {HTMLElement} a double form field. - */ - this.make_double_input = function (config) { - var clone = $.extend({}, config, { - type: "number" - }); - var ret = $(this._make_input(clone)) - ret.find("input").attr("step", "any"); - return ret[0]; - } + /** + * Return an input field which accepts double values. + * + * `config.type` is set to "number" and overrides any other type. + * + * @param {form_elements.input_config} config. + * @returns {HTMLElement} a double form field. + */ + this.make_double_input = function (config) { + var clone = $.extend({}, config, { + type: "number" + }); + var ret = $(this._make_input(clone)) + ret.find("input").attr("step", "any"); + return ret[0]; + } - /** - * Return an input field which accepts integers. - * - * `config.type` is set to "number" and overrides any other type. - * - * @param {form_elements.input_config} config. - * @returns {HTMLElement} an integer form field. - */ - this.make_integer_input = function (config) { - var ret = $(this.make_double_input(config)); - ret.find("input").attr("step", "1"); - return ret[0]; - } + /** + * Return an input field which accepts integers. + * + * `config.type` is set to "number" and overrides any other type. + * + * @param {form_elements.input_config} config. + * @returns {HTMLElement} an integer form field. + */ + this.make_integer_input = function (config) { + var ret = $(this.make_double_input(config)); + ret.find("input").attr("step", "1"); + return ret[0]; + } - /** - * Return a checkbox input field. - * - * @param {form_elements.checkbox_config} config. - * @returns {HTMLElement} a checkbox form field. - */ - this.make_checkbox_input = function (config) { - var clone = $.extend({}, config, { - type: "checkbox" - }); - var ret = $(this._make_input(clone)); - ret.find("input:checkbox").prop("checked", false); - ret.find("input:checkbox").toggleClass("form-control", false); - if (config.checked) { - ret.find("input:checkbox").prop("checked", true); - ret.find("input:checkbox").attr("checked", "checked"); - } - if (config.value) { - ret.find("input:checkbox").attr("value", config.value); - } - return ret[0]; + /** + * Return a checkbox input field. + * + * @param {form_elements.checkbox_config} config. + * @returns {HTMLElement} a checkbox form field. + */ + this.make_checkbox_input = function (config) { + var clone = $.extend({}, config, { + type: "checkbox" + }); + var ret = $(this._make_input(clone)); + ret.find("input:checkbox").prop("checked", false); + ret.find("input:checkbox").toggleClass("form-control", false); + if (config.checked) { + ret.find("input:checkbox").prop("checked", true); + ret.find("input:checkbox").attr("checked", "checked"); + } + if (config.value) { + ret.find("input:checkbox").attr("value", config.value); } + return ret[0]; + } - /** - * Add `caosdb-f-form-field-required` class to form field. - * - * @param {HTMLElement} field - the required form field. - */ - this.set_required = function (field) { - $(field).toggleClass("caosdb-f-form-field-required", true); - $(field).find(":input").prop("required", true); - $(field).find("label").prepend(this.make_required_marker()); - } + /** + * Add `caosdb-f-form-field-required` class to form field. + * + * @param {HTMLElement} field - the required form field. + */ + this.set_required = function (field) { + $(field).toggleClass("caosdb-f-form-field-required", true); + $(field).find(":input").prop("required", true); + $(field).find("label").prepend(this.make_required_marker()); + } - /** - * Return a span which is to be inserted before a field's label text - * and which marks that field as required. - * - * @returns {HTMLElement} span element. - */ - this.make_required_marker = function () { - // TODO create class and move to css file - return $('<span>*</span>') - .css({ - "font-size": "10px", - "color": "red", - "margin-right": "4px", - "font-weight": "100", - })[0]; - } + /** + * Return a span which is to be inserted before a field's label text + * and which marks that field as required. + * + * @returns {HTMLElement} span element. + */ + this.make_required_marker = function () { + // TODO create class and move to css file + return $('<span>*</span>') + .css({ + "font-size": "10px", + "color": "red", + "margin-right": "4px", + "font-weight": "100", + })[0]; + } - this.get_enabled_required_fields = function (form) { - return $(this.get_enabled_fields(form)) - .filter(".caosdb-f-form-field-required") - .toArray(); - } + this.get_enabled_required_fields = function (form) { + return $(this.get_enabled_fields(form)) + .filter(".caosdb-f-form-field-required") + .toArray(); + } - this.get_enabled_fields = function (form) { - return $(form) - .find(".caosdb-f-field") - .filter(function (idx) { - // remove disabled fields from results - return !$(this).hasClass("caosdb-f-field-disabled"); - }) - .toArray(); - } + this.get_enabled_fields = function (form) { + return $(form) + .find(".caosdb-f-field") + .filter(function (idx) { + // remove disabled fields from results + return !$(this).hasClass("caosdb-f-field-disabled"); + }) + .toArray(); + } - this.all_required_fields_set = function (form) { - const req = form_elements.get_enabled_required_fields(form); - for (const field of req) { - if (!form_elements.is_set(field)) { - return false; - } + this.all_required_fields_set = function (form) { + const req = form_elements.get_enabled_required_fields(form); + for (const field of req) { + if (!form_elements.is_set(field)) { + return false; } - return true; } + return true; + } - /** - * @param {HTMLElement} form - the form be validated. - */ - this.is_valid = function (form) { - return form_elements.all_required_fields_set(form); - } + /** + * @param {HTMLElement} form - the form be validated. + */ + this.is_valid = function (form) { + return form_elements.all_required_fields_set(form); + } - this.toggle_submit_button_form_valid = function (form, submit) { - // TODO do not change the submit button directly. change the - // `submittable` state of the form and handle the case where a form - // is submitting when this function is called. - if (form_elements.is_valid(form)) { - $(submit).prop("disabled", false); - } else { - $(submit).prop("disabled", true); - } + this.toggle_submit_button_form_valid = function (form, submit) { + // TODO do not change the submit button directly. change the + // `submittable` state of the form and handle the case where a form + // is submitting when this function is called. + if (form_elements.is_valid(form)) { + $(submit).prop("disabled", false); + } else { + $(submit).prop("disabled", true); } + } - this.init_validator = function (form) { - const submit = $(form).find(":input[type='submit']")[0]; - if (submit) { - form.addEventListener("caosdb.field.changed", (e) => form_elements.toggle_submit_button_form_valid(form, submit), true); - form.addEventListener("caosdb.field.enabled", (e) => form_elements.toggle_submit_button_form_valid(form, submit), true); - form.addEventListener("input", (e) => form_elements.toggle_submit_button_form_valid(form, submit), true); - } + this.init_validator = function (form) { + const submit = $(form).find(":input[type='submit']")[0]; + if (submit) { + form.addEventListener("caosdb.field.changed", (e) => form_elements.toggle_submit_button_form_valid(form, submit), true); + form.addEventListener("caosdb.field.enabled", (e) => form_elements.toggle_submit_button_form_valid(form, submit), true); + form.addEventListener("input", (e) => form_elements.toggle_submit_button_form_valid(form, submit), true); } + } - /** - * Return an input and a label, wrapped in a div with class - * `caosdb-f-field`. - * - * @param {object} config - config object with `name`, `type` and - * optional `label` - * @returns {HTMLElement} a form field. - */ - this._make_input = function (config) { - caosdb_utils.assert_string(config.name, "the name of a form field"); - let ret = $(this._make_field_wrapper(config.name)); - let name = config.name; - let label = this._make_input_label_str(config); - let type = config.type || "text"; - let value = config.value; - let input = $('<input class="form-control caosdb-f-property-single-raw-value" type="' + type + - '" name="' + name + - '" />'); - input.change(function () { - ret[0].dispatchEvent(form_elements.field_changed_event); - }); - let input_col = $('<div class="caosdb-f-property-value col-sm-9"/>'); - input_col.append(input); - if (value) { - input.val(value); - } - return ret.append(label, input_col)[0]; - } - - /** - * Return a string representation of a LABEL element, ready for parsing. - * - * This function is used by other functions to generate a LABEL element. - * - * The config's `name` goes to the `for` attribute, the `label` is the - * text node of the resulting LABEL element. - * - * @param {object} config - a config object with `name` and `label`. - * @returns {string} a html string for a LABEL element. - */ - this._make_input_label_str = function (config) { - let name = config.name; - let label = config.label; - return label ? '<label for="' + name + - '" data-property-name="' + name + - '" class="control-label col-sm-3">' + label + - '</label>' : ""; + /** + * Return an input and a label, wrapped in a div with class + * `caosdb-f-field`. + * + * @param {object} config - config object with `name`, `type` and + * optional `label` + * @returns {HTMLElement} a form field. + */ + this._make_input = function (config) { + caosdb_utils.assert_string(config.name, "the name of a form field"); + let ret = $(this._make_field_wrapper(config.name)); + let name = config.name; + let label = this._make_input_label_str(config); + let type = config.type || "text"; + let value = config.value; + let input = $('<input class="form-control caosdb-f-property-single-raw-value" type="' + type + + '" name="' + name + + '" />'); + input.change(function () { + ret[0].dispatchEvent(form_elements.field_changed_event); + }); + let input_col = $('<div class="caosdb-f-property-value col-sm-9"/>'); + input_col.append(input); + if (value) { + input.val(value); } + return ret.append(label, input_col)[0]; + } + /** + * Return a string representation of a LABEL element, ready for parsing. + * + * This function is used by other functions to generate a LABEL element. + * + * The config's `name` goes to the `for` attribute, the `label` is the + * text node of the resulting LABEL element. + * + * @param {object} config - a config object with `name` and `label`. + * @returns {string} a html string for a LABEL element. + */ + this._make_input_label_str = function (config) { + let name = config.name; + let label = config.label; + return label ? '<label for="' + name + + '" data-property-name="' + name + + '" class="control-label col-sm-3">' + label + + '</label>' : ""; } + this._init_functions(); } diff --git a/src/core/js/preview.js b/src/core/js/preview.js index f74fd13ffd2fe373543c025866e91cfe6b6fd9eb..b85f7b56dc13ff94aa2c3de90eb1b464d7f1e9d6 100644 --- a/src/core/js/preview.js +++ b/src/core/js/preview.js @@ -211,8 +211,8 @@ var preview = new function() { /** * Transform the raw xml response of the server into an array of entities for preview. * - * @param {Promise for XMLDocument} xml - A Promise for the servers xml response. - * @return {Promise for HTMLElement[]} A Promise for an array of entities. + * @param {Promise | XMLDocument} xml - A Promise for the servers xml response. + * @return {Promise | HTMLElement[]} A Promise for an array of entities. */ this.processPreviewResponse = function(xml) { let xsl = preview.getEntityXsl(); @@ -222,7 +222,7 @@ var preview = new function() { /** * Retrieve the XSL script for entities from the server. * - * @return {Promise for XMLDocument} A Promise for the XSL script. + * @return {Promise | XMLDocument} A Promise for the XSL script. */ this.getEntityXsl = async function _getEntityXsl() { return transformation.retrieveEntityXsl(); @@ -679,7 +679,7 @@ var preview = new function() { * Retrieve a list of entities from the server. * * @param {String[]} entityIds - The ids of the entities which are to be retrieved. - * @return {Promise for HTMLElement[]} A Promise for an array of entities. + * @return {Promise | HTMLElement[]} A Promise for an array of entities. */ this.retrievePreviewEntities = async function _rPE(entityIds) { try { @@ -711,9 +711,9 @@ var preview = new function() { /** * Transform the xml to an array of entities. * - * @param {Promise XMLDocument} xml - The server response. - * @param {Promise XMLDocument} xsl - The xsl script. - * @return {Promise HTMLElement[]} A promise for an Array of HTMLElements. + * @param {Promise | XMLDocument} xml - The server response. + * @param {Promise | XMLDocument} xsl - The xsl script. + * @return {Promise | HTMLElement[]} A promise for an Array of HTMLElements. */ this.transformXmlToPreviews = async function _tXTP(xml, xsl) { let html = await asyncXslt(xml, xsl); diff --git a/src/core/js/tour.js b/src/core/js/tour.js index 7b47dd37239c29b5c5e7edb67f1d16fe43bf8ad6..cfe65519eef0f34f708461bca3658e76f5530eff 100644 --- a/src/core/js/tour.js +++ b/src/core/js/tour.js @@ -630,6 +630,7 @@ var tour = new function() { content: markdown_content, placement: placement, html: true, + sanitize: false, trigger: 'manual', template: popover_template, }); @@ -934,8 +935,6 @@ var tour = new function() { tour_overview.append(next); } - panel.hover(undefined, ()=>{panel.collapse('hide');}); - panel.append(tour_overview); this.leave_tour_button.on("click", () => {this.deactivate();}); @@ -981,6 +980,9 @@ var tour = new function() { tour._instance.set_tour_button_text("Tour"); } $('#caosdb-query-panel').before(tour._instance.panel); + // hide, when the mouse leaves the navbar + $('nav.navbar').hover(undefined, ()=>{$(tour._instance.panel).collapse('hide');}); + } diff --git a/src/core/js/webcaosdb.js b/src/core/js/webcaosdb.js index df93703d83fafde15908efb40c3ae578824980d9..55337a36a97d6a7ad42aadfe673e429d6d942a0a 100644 --- a/src/core/js/webcaosdb.js +++ b/src/core/js/webcaosdb.js @@ -146,6 +146,64 @@ this.navbar = new function () { .on("hidden.bs.collapse", function (e) { logger.trace("navbar shrinks", e); }); + this.init_login_show_button(); + } + + + /** + * Initialize the hiding/showing of the input form. + * + * If the viewport is xs (width <= 768px) the login form is hidden in the + * a menu anyways. + * + * If the viewport is greater then the folloging applies: + * + * When loading the page, the login form is hidden behind a "Login" button. + * When the user clicks the Login button the form appears and the other + * button disappears. After a timeout of 10 seconds of inactivity the form + * hides again. When the user starts typing, the timeout is canceled. + * + * If the user leaves the input fields ("blur" event) the timeout is + * reestablished and the form hides after 10 seconds. + */ + this.init_login_show_button = function () { + const form = $("#caosdb-f-login-form"); + const show_button = $("#caosdb-f-login-show-button"); + var timeout = undefined; + + // show form and hide the show_button + const _in = () => { + // xs means viewport <= 768px + form.removeClass("visible-xs-inline-block"); + show_button.addClass("hidden"); + } + // hide form and show the show_button + const _out = () => { + // xs means viewport <= 768px + form.addClass("visible-xs-inline-block"); + show_button.removeClass("hidden"); + } + show_button.on("click", () => { + // show form... + _in(); + + // and hide it after ten seconds if nothing happens + timeout = setTimeout(_out,10000) + }); + form.find("input,button").on("blur", () => { + if (timeout) { + // cancel existing timeout (e.g. from showing) + clearTimeout(timeout); + } + // hide after 10 seconds if nothing happens + timeout = setTimeout(_out,10000) + }); + form.find("input,button").on("change", () => { + // something happens! + if (timeout) { + clearTimeout(timeout); + } + }); } @@ -269,6 +327,30 @@ this.caosdb_utils = new function () { } throw new TypeError(name + " is expected to be an array, was " + typeof obj); } + + /** + * Create a tsv table as string. + * + * The data must be appropriately encoded (e.g. urlencoded). + * + * With `tab=","` it is also possible to create csv tables. + * + * @param {string[][]} data - An array of rows which contain arrays of + * cells. + * @param {string} [preamble="data:text/csv;charset=utf-8,"] - a prefix for + * the the resulting string. The default is suitable for creating + * downloadable href attributes of links. + * @param {string} [tab="%09"] - the cell separator. + * @param {string} [newline="%0A"] - the row separator. + * @return {string} a tsv table as a string. + */ + this.create_tsv_table = function(data, preamble, tab, newline) { + preamble = ((typeof preamble == 'undefined') ? "data:text/csv;charset=utf-8,": preamble); + tab = tab || "%09"; + newline = newline || "%0A"; + const rows = data.map(x => x.join(tab)) + return `${preamble}${rows.join(newline)}`; + } } /** @@ -592,7 +674,8 @@ this.transaction = new function () { * @return {Element[]} array of xml elements. */ this.retrieveEntitiesById = async function _rEBIs(entityIds) { - return $(await connection.get(this.generateEntitiesUri(entityIds))).find('Response [id]').toArray(); + const response = await connection.get(this.generateEntitiesUri(entityIds)); + return $(response).find('Response > [id]').toArray(); } /** Sends a PUT request with an xml representation of entities and @@ -905,6 +988,117 @@ this.transaction = new function () { } } +/** + * This module provides the functionality to load the full version history (for + * privileged users) and export it to tsv. + */ +var version_history = new function () { + + this._get = connection.get; + /** + * Retrieve the version history of an entity and return a table with the + * history. + * + * @param {string} entity - the entity id with or without version id. + * @return {HTMLElement} A table with the version history. + */ + this.retrieve_history = async function(entity) { + const xml = this._get(transaction + .generateEntitiesUri([entity]) + "?H"); + const html = (await transformation.transformEntities(xml))[0]; + const history_table = $(html).find(".caosdb-f-entity-version-history"); + return history_table[0]; + } + + /** + * Initalize the buttons for loading the version history. + * + * The buttons are visible when the entity has only the normal version info + * attached and the current user has the permissions to retrieve the + * version history. + * + * The buttons trigger the retrieval of the version history and append the + * version history to the version info modal. + */ + this.init_load_history_buttons = function () { + for (let entity of $(".caosdb-entity-panel")) { + const is_permitted = hasEntityPermission(entity, "RETRIEVE:HISTORY"); + if (!is_permitted) { + continue; + } + const entity_id_version = getEntityIdVersion(entity); + const version_info = $(entity) + .find(".caosdb-f-entity-version-info"); + const button = $(version_info) + .find(".caosdb-f-entity-version-load-history-btn"); + button.show(); + button + .click(async () => { + button.prop("disabled", true); + const wait = createWaitingNotification("Retrieving full history. Please wait."); + const sparse = $(version_info) + .find(".caosdb-f-entity-version-history"); + sparse.find(".modal-body *").replaceWith(wait); + + const history_table = await version_history + .retrieve_history(entity_id_version); + sparse.replaceWith(history_table); + version_history.init_export_history_buttons(entity); + }); + } + } + + /** + * Transform the HTML table with the version history to tsv. + * + * @param {HTMLElement} history_table - the HTML representation of the + * version history. + * @return {string} the version history as downloadable tsv string, + * suitable for the href attribute of a link or window.location. + */ + this.get_history_tsv = function (history_table) { + const rows = []; + for (let row of $(history_table).find("tr")) { + const cells = $(row).find(".export-data").toArray().map(x => x.textContent); + rows.push(cells); + } + return caosdb_utils.create_tsv_table(rows); + } + + /** + * Initialize the export buttons of `entity`. + * + * The buttons are only visible when the version history is visible and + * trigger a download of a tsv file which contains the version history. + * + * The buttons trigger the download of a tsv file with the version history. + * + * @param {HTMLElement} [entity] - if undefined, the export buttons of all + * page entities are initialized. + */ + this.init_export_history_buttons = function (entity) { + entity = entity || $(".caosdb-entity-panel"); + for (let version_info of $(entity) + .find(".caosdb-f-entity-version-info")) { + $(version_info).find(".caosdb-f-entity-version-export-history-btn") + .click(async () => { + const html_table = $(version_info).find("table")[0]; + const history_tsv = this.get_history_tsv(html_table); + version_history._download_tsv(history_tsv); + }); + } + } + + this._download_tsv = function(tsv_link) { + window.location.href = tsv_link; + } + + + this.init = function () { + this.init_load_history_buttons(); + this.init_export_history_buttons(); + } +} var paging = new function () { @@ -979,6 +1173,9 @@ var paging = new function () { return null; } + // remove fragment + uri_old = uri_old.split("#")[0]; + var pattern = /(\?(.*&)?)P=([^&]*)/; if (pattern.test(uri_old)) { // replace existing P=... @@ -1003,7 +1200,7 @@ var paging = new function () { if (uri == null) { throw new Error("uri was null."); } - var pattern = /\?(.*&)?P=([^&]*)/; + var pattern = /\?(.*&)?P=([^&#]*)/; var ret = pattern.exec(uri); return (ret != null ? ret[2] : null); } @@ -1121,7 +1318,6 @@ var queryForm = new function () { See https://developer.mozilla.org/en-US/docs/Web/Events/submit why this is necessary. */ var submithandler = function () { - // store current query var queryField = form.query; var value = queryField.value.toUpperCase(); @@ -1141,6 +1337,13 @@ var queryForm = new function () { queryForm.redirect(queryField.value, paging); }; + $("#caosdb-query-textarea").on("keydown", (e) => { + // prevent submit on enter + if(e.originalEvent.which == 13) { + e.originalEvent.preventDefault(); + } + }) + // handler for the form form.onsubmit = function (e) { @@ -1503,13 +1706,13 @@ function insertParam(xsl, name, value = null) { /** * When the page is scrolled down 100 pixels, the scroll-back button appears. * - * @return + * @return FIXME */ /** * Every initial function calling is done here. * - * @return + * @return TODO */ function initOnDocumentReady() { hintMessages.init(); @@ -1534,6 +1737,7 @@ function initOnDocumentReady() { } caosdb_modules.init(); navbar.init(); + version_history.init(); } diff --git a/src/core/xsl/entity.xsl b/src/core/xsl/entity.xsl index 299b1dac37a1029e5bc7c48008efa3735d61037b..cac9e87ea40bf6a687a19a1942b5534276c8faf7 100644 --- a/src/core/xsl/entity.xsl +++ b/src/core/xsl/entity.xsl @@ -103,6 +103,7 @@ <xsl:attribute name="data-entity-id"> <xsl:value-of select="@id"/> </xsl:attribute> + <xsl:apply-templates mode="entity-permissions" select="Permissions"/> <!-- A page-unique ID for this entity --> <xsl:variable name="entityid" select="concat('entity_',generate-id())"/> <div class="panel-heading caosdb-entity-panel-heading"> @@ -156,6 +157,16 @@ <span class="label caosdb-id caosdb-id-button hidden"> <xsl:value-of select="@id"/> </span> + <button class="btn btn-link caosdb-v-bookmark-button"> + <xsl:attribute name="data-bmval"> + <xsl:value-of select="@id"/> + <xsl:if test="Version/Successor"> + <!-- this is not the head --> + <xsl:value-of select="concat('@', Version/@id)"/> + </xsl:if> + </xsl:attribute> + <span class="glyphicon glyphicon-bookmark"/> + </button> <xsl:apply-templates mode="entity-heading-attributes-version" select="Version"> <xsl:with-param name="entityId" select="@id"/> </xsl:apply-templates> @@ -392,8 +403,8 @@ <xsl:otherwise> <xsl:choose> <!-- the referenced entities have been returned. --> - <xsl:when test="*[@id]"> - <xsl:for-each select="*[@id]"> + <xsl:when test="Record|RecordType|Property|File"> + <xsl:for-each select="Record|RecordType|Property|File"> <xsl:call-template name="single-value"> <xsl:with-param name="reference"> <xsl:value-of select="'true'"/> @@ -518,78 +529,197 @@ </xsl:attribute> <span class="glyphicon glyphicon-time"/> </button> + <!-- the following div.modal is the window that pops up when the user clicks on the clock button --> <div class="caosdb-f-entity-version-info modal fade" tabindex="-1" role="dialog"> <xsl:attribute name="id"><xsl:value-of select="$versionModalId"/></xsl:attribute> + <xsl:attribute name="data-entity-versioned-id"><xsl:value-of select="concat($entityId, '@', @id)"/></xsl:attribute> <div class="modal-dialog modal-lg" role="document"> <div class="modal-content text-left"> + <!-- modal-header start --> <div> <xsl:attribute name="class"> modal-header - <xsl:if test="Successor"> + <xsl:if test="not(@head='true')"> <!-- indicate old version by color --> <xsl:value-of select="' bg-danger'"/> </xsl:if> </xsl:attribute> <button type="button" class="close" data-dismiss="modal" aria-label="Close" title="Close"><span aria-hidden="true">×</span></button> <h4 class="modal-title">Version Info</h4> - <p class="caosdb-entity-heading-attr"> + <p class="caosdb-entity-heading-attr"> <em class="caosdb-entity-heading-attr-name"> This is - <xsl:if test="Successor"><b>not</b></xsl:if> + <xsl:if test="not(@head='true')"><b>not</b></xsl:if> the latest version of this entity. + <xsl:apply-templates mode="entity-version-modal-head" select="Successor"> + <xsl:with-param name="entityId" select="$entityId"/> + </xsl:apply-templates> </em> </p> </div> - <div class="modal-body"> - <xsl:apply-templates mode="entity-version-modal-head" select="Successor"> + <!-- modal-header end --> + <div class="caosdb-f-entity-version-history"> + <!-- modal-body and modal-footer are added by this template --> + <xsl:apply-templates select="." mode="entity-version-history-table"> <xsl:with-param name="entityId" select="$entityId"/> </xsl:apply-templates> - <xsl:apply-templates mode="entity-version-modal-successor" select="Successor"> + </div> + </div> + </div> + </div> + </xsl:template> + + <xsl:template match="Version[@completeHistory='true']" mode="entity-version-history-table"> + <!-- contains the table of the full version history --> + <xsl:param name="entityId"/> + <div class="modal-body"> + <table class="table table-hover"> + <thead> + <tr><div class="export-data">Entity ID</div><th/> + <th class="export-data">Version ID</th> + <th class="export-data">Date</th> + <th class="export-data">User</th> + <div class="export-data">URI</div> + </tr></thead> + <tbody> + <xsl:apply-templates mode="entity-version-modal-successor" select="Successor"> + <xsl:with-param name="entityId" select="$entityId"/> + </xsl:apply-templates> + <tr> + <div class="export-data"><xsl:value-of select="$entityId"/></div> + <td class="caosdb-v-entity-version-hint caosdb-v-entity-version-hint-cur">This Version</td> + <td><xsl:apply-templates select="@id" mode="entity-version-id"/> + </td><td> + <xsl:apply-templates select="@date" mode="entity-version-date"/> + </td><td class="export-data"> + <xsl:value-of select="@username"/>@<xsl:value-of select="@realm"/> + </td> + <div class="export-data"><xsl:value-of select="concat($entitypath, $entityId, '@', @id)"/></div> + </tr> + <xsl:apply-templates mode="entity-version-modal-predecessor" select="Predecessor"> + <xsl:with-param name="entityId" select="$entityId"/> + </xsl:apply-templates> + </tbody> + </table> + </div> + <div class="modal-footer"> + <button type="button" class="caosdb-f-entity-version-export-history-btn btn btn-default">Export history</button> + </div> + </xsl:template> + + <xsl:template match="Version[not(@completeHistory='true')]" mode="entity-version-history-table"> + <!-- contains the table of the simple version info (not the full history)--> + <xsl:param name="entityId"/> + <div class="modal-body"> + <table class="table"> + <thead><tr><th>Previous Version</th><th>This Version</th><th>Next Version</th></tr></thead> + <tbody> + <tr> + <td> + <xsl:if test="not(Predecessor)"> + <div class="caosdb-v-entity-version-no-related">No predecessor</div> + </xsl:if> + <xsl:apply-templates select="Predecessor/@id" mode="entity-version-link-to-other-version"> <xsl:with-param name="entityId" select="$entityId"/> </xsl:apply-templates> - <p class="caosdb-entity-heading-attr"> - <em class="caosdb-entity-heading-attr-name">This version:</em> - <xsl:value-of select="@id"/> (<xsl:value-of select="@date"/>) - </p> - <xsl:apply-templates mode="entity-version-modal-predecessor" select="Predecessor"> + </td> + <td> + <xsl:apply-templates select="@id" mode="entity-version-id"/> + </td> + <td> + <xsl:if test="not(Successor)"> + <div class="caosdb-v-entity-version-no-related">No successor</div> + </xsl:if> + <xsl:apply-templates select="Successor/@id" mode="entity-version-link-to-other-version"> <xsl:with-param name="entityId" select="$entityId"/> </xsl:apply-templates> - </div> - </div> - </div> + </td> + </tr> + </tbody> + </table> + </div> + <div class="modal-footer"> + <button type="button" style="display: none" class="caosdb-f-entity-version-load-history-btn btn btn-default">Load full history</button> </div> </xsl:template> + + <xsl:template match="@id" mode="entity-version-id"> + <!-- a versions'id (abbreviated) --> + <xsl:attribute name="title">Full Version ID: <xsl:value-of select="."/></xsl:attribute> + <xsl:value-of select="substring(.,1,8)"/> + <div class="export-data"><xsl:value-of select="."/></div> + </xsl:template> + + <xsl:template match="@date" mode="entity-version-date"> + <!-- a version's date (abbreviated)--> + <xsl:attribute name="title"><xsl:value-of select="."/></xsl:attribute> + <xsl:value-of select="substring(.,0,11)"/> + <xsl:value-of select="' '"/> + <xsl:value-of select="substring(.,12,8)"/> + <div class="export-data"><xsl:value-of select="."/></div> + </xsl:template> + + <xsl:template match="Predecessor|Successor" mode="entity-version-modal-single-history-item"> + <!-- a single row of the version history table --> + <xsl:param name="entityId"/> + <xsl:param name="hint"/> + <tr> + <div class="export-data"><xsl:value-of select="$entityId"/></div> + <td class="caosdb-v-entity-version-hint"><xsl:value-of select="$hint"/></td> + <td> + <xsl:apply-templates select="@id" mode="entity-version-link-to-other-version"> + <xsl:with-param name="entityId" select="$entityId"/> + </xsl:apply-templates> + </td><td> + <xsl:apply-templates select="@date" mode="entity-version-date"/> + </td><td class="export-data"> + <xsl:value-of select="@username"/>@<xsl:value-of select="@realm"/> + </td> + <div class="export-data"><xsl:value-of select="concat($entitypath, $entityId, '@', @id)"/></div> + </tr> + </xsl:template> + + <xsl:template match="@id" mode="entity-version-link-to-other-version"> + <!-- link to other version (used by both version tables)--> + <xsl:param name="entityId"/> + <a><xsl:attribute name="href"><xsl:value-of select="$entityId"/>@<xsl:value-of select="."/></xsl:attribute> + <xsl:apply-templates select="." mode="entity-version-id"/></a> + </xsl:template> + <xsl:template match="Predecessor" mode="entity-version-modal-predecessor"> - <!-- content of the versioning window --> + <!-- content of the versioning info (not the full history) --> <xsl:param name="entityId"/> - <p class="caosdb-entity-heading-attr"> - <em class="caosdb-entity-heading-attr-name">Previous version:</em> - <a><xsl:attribute name="href"><xsl:value-of select="$entityId"/>@<xsl:value-of select="@id"/></xsl:attribute> - <xsl:value-of select="@id"/> (<xsl:value-of select="@date"/>) - </a> - </p> + <xsl:apply-templates mode="entity-version-modal-single-history-item" select="."> + <xsl:with-param name="entityId" select="$entityId"/> + <xsl:with-param name="hint" select="'Older Version'"/> + </xsl:apply-templates> + <xsl:apply-templates mode="entity-version-modal-predecessor" select="Predecessor"> + <xsl:with-param name="entityId" select="$entityId"/> + </xsl:apply-templates> </xsl:template> + <xsl:template match="Successor" mode="entity-version-modal-head"> - <!-- content of the versioning window --> + <!-- content of the versioning modal's header (if a newer version exists) --> <xsl:param name="entityId"/> - <p class="caosdb-entity-heading-attr"> - <em class="caosdb-entity-heading-attr-name">Newest version:</em> - <a><xsl:attribute name="href"><xsl:value-of select="$entityId"/>@HEAD</xsl:attribute> - <xsl:value-of select="$entityId"/>@HEAD - </a> - </p> + View the newest version here: + <a><xsl:attribute name="href"><xsl:value-of select="$entityId"/>@HEAD</xsl:attribute> + <xsl:value-of select="$entityId"/>@HEAD + </a> </xsl:template> + <xsl:template match="Successor" mode="entity-version-modal-successor"> - <!-- content of the versioning window --> + <!-- content of the versioning info (not the full history) --> <xsl:param name="entityId"/> - <p class="caosdb-entity-heading-attr"> - <em class="caosdb-entity-heading-attr-name">Next version:</em> - <a><xsl:attribute name="href"><xsl:value-of select="$entityId"/>@<xsl:value-of select="@id"/></xsl:attribute> - <xsl:value-of select="@id"/> (<xsl:value-of select="@date"/>) - </a> - </p> + <xsl:apply-templates mode="entity-version-modal-successor" select="Successor"> + <xsl:with-param name="entityId" select="$entityId"/> + </xsl:apply-templates> + <xsl:apply-templates mode="entity-version-modal-single-history-item" select="."> + <xsl:with-param name="entityId" select="$entityId"/> + <xsl:with-param name="hint" select="'Newer Version'"/> + </xsl:apply-templates> </xsl:template> + <xsl:template match="Version/Successor" mode="entity-action-panel-version"> <!-- clickable warning message in the entity actions panel when there exists a newer version --> <xsl:param name="entityId"/> @@ -598,13 +728,15 @@ <strong>Warning</strong> A newer version exists! </a> </xsl:template> + <xsl:template match="Version" mode="entity-version-marker"> <!-- content of the data-version-id attribute --> <xsl:attribute name="data-version-id"> - <xsl:value-of select="@id"/> + <xsl:value-of select="@id"/> </xsl:attribute> <xsl:apply-templates select="Successor" mode="entity-version-marker"/> </xsl:template> + <xsl:template match="Successor" mode="entity-version-marker"> <!-- content of the data-version-successor attribute This data-attribute marks entities which have a newer version. @@ -613,4 +745,17 @@ <xsl:value-of select="@id"/> </xsl:attribute> </xsl:template> + + <!-- PERMISSIONS --> + <xsl:template match="Permissions" mode="entity-permissions"> + <div style="display: none"> + <xsl:apply-templates select="Permission" mode="entity-permissions"/> + </div> + </xsl:template> + + <xsl:template match="Permission" mode="entity-permissions"> + <div> + <xsl:attribute name="data-permission"><xsl:value-of select="@name"/></xsl:attribute> + </div> + </xsl:template> </xsl:stylesheet> diff --git a/src/core/xsl/main.xsl b/src/core/xsl/main.xsl index 10a4887aed065a5c8ffc74e463fc7660f81d743b..f81d8792c94d8da38d058f96d026b757af402b6c 100644 --- a/src/core/xsl/main.xsl +++ b/src/core/xsl/main.xsl @@ -142,6 +142,16 @@ <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/webcaosdb.js')"/> </xsl:attribute> </xsl:element> + <xsl:element name="script"> + <xsl:attribute name="src"> + <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/pako.js')"/> + </xsl:attribute> + </xsl:element> + <xsl:element name="script"> + <xsl:attribute name="src"> + <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/utif.js')"/> + </xsl:attribute> + </xsl:element> <script> $(document).ready(() => paging.initPaging(window.location.href, <xsl:value-of select="/Response/@count"/> )); </script> @@ -157,7 +167,7 @@ </xsl:element> <xsl:element name="script"> <xsl:attribute name="src"> - <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/autocomplete.js')"/> + <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_autocomplete.js')"/> </xsl:attribute> </xsl:element> <xsl:element name="script"> @@ -170,6 +180,11 @@ <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_references.js')"/> </xsl:attribute> </xsl:element> + <xsl:element name="script"> + <xsl:attribute name="src"> + <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_table_preview.js')"/> + </xsl:attribute> + </xsl:element> <xsl:element name="script"> <xsl:attribute name="src"> <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_xls_download.js')"/> @@ -195,6 +210,11 @@ <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/edit_mode.js')"/> </xsl:attribute> </xsl:element> + <xsl:element name="script"> + <xsl:attribute name="src"> + <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_file_download.js')"/> + </xsl:attribute> + </xsl:element> <xsl:element name="script"> <xsl:attribute name="src"> <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/leaflet.js')"/> @@ -265,6 +285,11 @@ <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_entity_state.js')"/> </xsl:attribute> </xsl:element> + <xsl:element name="script"> + <xsl:attribute name="src"> + <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_bookmarks.js')"/> + </xsl:attribute> + </xsl:element> <!--JS_EXTENSIONS--> </xsl:template> <xsl:template name="caosdb-data-container"> diff --git a/src/core/xsl/navbar.xsl b/src/core/xsl/navbar.xsl index 907f27e512250cf5fde0ff2fe93fe85f800428a6..6ce69e638efda10dbb95a066e8b59508854a4435 100644 --- a/src/core/xsl/navbar.xsl +++ b/src/core/xsl/navbar.xsl @@ -132,6 +132,23 @@ </ul> </xsl:if> <ul class="nav navbar-nav navbar-right"> + <li class="dropdown"> + <a class="dropdown-toggle" data-toggle="dropdown" href="#"> + <span id="caosdb-f-bookmarks-collection-counter" class="badge">0</span> + Bookmarks + <span class="caret"></span></a> + <ul class="dropdown-menu"> + <li class="disabled" id="caosdb-f-bookmarks-collection-link" + title="Show all bookmarked entities."> + <a>Show all</a></li> + <li class="disabled" id="caosdb-f-bookmarks-export-link" + title="Export all bookmarks to a file. The exported file is a spread sheet with columns for the id, the version, the complete URI of the bookmarked entities and the path, if the entity is a file."> + <a>Export to file</a></li> + <li class="disabled" id="caosdb-f-bookmarks-clear" + title="Empty the list of bookmarks."> + <a>Clear</a></li> + </ul> + </li> <xsl:call-template name="caosdb-user-menu"/> </ul> </div> @@ -191,17 +208,15 @@ </xsl:when> <xsl:otherwise> <li id="user-menu"> - <form class="navbar-form" method="POST"> + <form id="caosdb-f-login-form" class="navbar-form visible-xs-inline-block" method="POST"> <xsl:attribute name="action"> <xsl:value-of select="concat($basepath, 'login')"/> </xsl:attribute> <input class="form-control" id="username" name="username" placeholder="username" type="text"/> <input class="form-control" id="password" name="password" placeholder="password" type="password"/> - <button class="btn btn-default" type="submit"> - <span class="glyphicon glyphicon-log-in"></span> - Login - </button> + <button class="btn btn-primary" type="submit">Login</button> </form> + <button style="margin-right: 15px" class="btn btn-default navbar-btn hidden-xs" id="caosdb-f-login-show-button" type="button">Login</button> </li> </xsl:otherwise> </xsl:choose> diff --git a/src/core/xsl/query.xsl b/src/core/xsl/query.xsl index be49ed7d889e920cfee87e723c4c5c3b8efb27b2..44d1c1bd6cff8f4b22138c1287af15713069ed79 100644 --- a/src/core/xsl/query.xsl +++ b/src/core/xsl/query.xsl @@ -89,7 +89,7 @@ </div> <div class="col-xs-6 text-right"> <!-- Trigger the modal with a button --> - <button class="btn btn-info btn-sm" data-target="#downloadModal" data-toggle="modal" type="button">Download this table</button> + <button class="btn btn-info btn-sm caosdb-v-btn-select" data-target="#downloadModal" data-toggle="modal" type="button">Export</button> <!-- Modal --> <div class="modal fade text-left" id="downloadModal" role="dialog"> <div class="modal-dialog"> @@ -102,16 +102,21 @@ <div class="modal-body"> <p> <a id="caosdb-f-query-select-data-tsv" onclick="downloadTSV(this)" href="#selected_data.tsv" download="selected_data.tsv"> - Download TSV File + Download table as TSV File </a> <span class="checkbox" style="margin-top: 0; display: inline; position: absolute; right: 10px"><label><input type="checkbox" name="raw" id="caosdb-table-export-raw-flag-tsv" title="Export raw entity ids instead of the visible page content."/>raw</label></span> </p> <p> <a class="caosdb-v-query-select-data-xsl" onclick="downloadXLS(this)" href="#selected_data.xsl" download=""> - Download XLS File + Download table as XLS File </a> <span class="checkbox" style="margin-top: 0; display: inline; position: absolute; right: 10px"><label><input type="checkbox" name="raw" id="caosdb-table-export-raw-flag-xls" title="Export raw entity ids instead of the visible page content."/>raw</label></span> </p> + <p> + <a id="caosdb-f-query-select-files" onclick="ext_file_download.download_files(this)" href="#selected_data.tsv" download="files.zip" title="Collects file entities listed in the table in a zip file. If the entity belonging to a row is a file entity, it will be included."> + Download files referenced in the table + </a> + </p> <hr/> <p> <small>Download this dataset in Python with:</small> @@ -123,7 +128,13 @@ </p> </div> <div class="modal-footer"> - <button class="btn btn-default" data-dismiss="modal" type="button">Close</button> + <div class="row" style="margin:0px"> + <div class="col-xs-6 caosdb-f-modal-footer-left"> + </div> + <div class="col-xs-6"> + <button class="btn btn-default" data-dismiss="modal" type="button">Close</button> + </div> + </div> </div> </div> </div> @@ -138,7 +149,7 @@ <thead> <tr> <th></th> - <xsl:for-each select="Selector[@name!='id']"> + <xsl:for-each select="Selector"> <th> <xsl:value-of select="@name"/> </th> @@ -149,6 +160,8 @@ <xsl:for-each select="/Response/*[@id]"> <xsl:call-template name="select-table-row"> <xsl:with-param name="entity-id" select="@id"/> + <xsl:with-param name="version-id" select="Version/@id"/> + <xsl:with-param name="ishead" select="Version/@head"/> </xsl:call-template> </xsl:for-each> </tbody> @@ -159,9 +172,14 @@ </xsl:template> <xsl:template name="entity-link"> <xsl:param name="entity-id"/> + <xsl:param name="version-id"/> + <xsl:param name="ishead"/> <a class="btn btn-default btn-sm caosdb-select-id"> <xsl:attribute name="href"> <xsl:value-of select="concat($entitypath, $entity-id)"/> + <xsl:if test="$version-id and not($ishead)"> + <xsl:value-of select="concat('@', $version-id)"/> + </xsl:if> </xsl:attribute> <!-- <xsl:value-of select="$entity-id" /> --> <span class="caosdb-select-id-target"> @@ -172,18 +190,26 @@ <xsl:template name="select-table-row"> <xsl:param name="entity-id"/> + <xsl:param name="version-id"/> + <xsl:param name="ishead"/> <tr> <xsl:attribute name="data-entity-id"> <xsl:value-of select="$entity-id"/> </xsl:attribute> + <xsl:attribute name="data-version-id"> + <xsl:value-of select="$version-id"/> + </xsl:attribute> <td> <xsl:call-template name="entity-link"> <xsl:with-param name="entity-id" select="$entity-id"/> + <xsl:with-param name="version-id" select="$version-id"/> + <xsl:with-param name="ishead" select="$ishead"/> </xsl:call-template> </td> <xsl:for-each select="/Response/Query/Selection/Selector"> <xsl:call-template name="select-table-cell"> <xsl:with-param name="entity-id" select="$entity-id"/> + <xsl:with-param name="version-id" select="$version-id"/> <xsl:with-param name="field-name" select="translate(@name, $uppercase, $lowercase)"/> </xsl:call-template> </xsl:for-each> @@ -192,13 +218,15 @@ <xsl:template name="select-table-cell"> <xsl:param name="entity-id"/> + <xsl:param name="version-id"/> <xsl:param name="field-name"/> <td class="caosdb-f-entity-property"> <xsl:attribute name="data-property-name"> <xsl:value-of select="$field-name"/> </xsl:attribute> <div class="caosdb-f-property-value caosdb-v-property-value"> - <xsl:apply-templates select="/Response/*[@id=$entity-id]" mode="walk-select-segments"> + <xsl:apply-templates select="/Response/*[@id=$entity-id and Version/@id=$version-id]" mode="walk-select-segments"> + <!--<xsl:apply-templates select="/Response/*[@id=$entity-id]" mode="walk-select-segments">--> <xsl:with-param name="first-segment"> <xsl:value-of select="substring-before(concat($field-name, '.'), '.')"/> </xsl:with-param> @@ -212,9 +240,28 @@ <xsl:template match="Property" mode="walk-select-segments"> <!-- handle properties --> + <xsl:param name="first-segment"/> <xsl:param name="next-segments"/> <xsl:choose> + <xsl:when test="@*[translate($first-segment, $uppercase, $lowercase)=name()]"> + <!--handle attributes--> + <xsl:call-template name="single-value"> + <xsl:with-param name="value"> + <xsl:value-of select="@*[translate(name(), $uppercase, $lowercase)=$first-segment]"/> + </xsl:with-param> + </xsl:call-template> + </xsl:when> + + <xsl:when test="translate($first-segment, $uppercase, $lowercase)='version'"> + <!--handle version--> + <xsl:call-template name="single-value"> + <xsl:with-param name="value"> + <xsl:value-of select="Version/@id"/> + </xsl:with-param> + </xsl:call-template> + </xsl:when> + <xsl:when test="$next-segments='value'"> <!--handle value--> <xsl:apply-templates mode="property-value" select="."/> @@ -263,6 +310,15 @@ </xsl:call-template> </xsl:when> + <xsl:when test="translate($first-segment, $uppercase, $lowercase)='version'"> + <!--handle version--> + <xsl:call-template name="single-value"> + <xsl:with-param name="value"> + <xsl:value-of select="Version/@id"/> + </xsl:with-param> + </xsl:call-template> + </xsl:when> + <xsl:when test="$next-segments"> <!-- when there is a next-segmenst --> <xsl:apply-templates select="Property[translate(@name, $uppercase, $lowercase)=$first-segment]" mode="walk-select-segments"> diff --git a/src/doc/Makefile b/src/doc/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..f3519f277badaf083c7f3512c64b18911ddf1f11 --- /dev/null +++ b/src/doc/Makefile @@ -0,0 +1,51 @@ +# ** header v3.0 +# This file is a part of the CaosDB Project. +# +# Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> +# Copyright (C) 2020 Daniel Hornung <d.hornung@indiscale.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# +# ** end header + +# This Makefile is a wrapper for sphinx scripts. +# +# It is based upon the autocreated makefile for Sphinx documentation. + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= -a +SPHINXBUILD ?= sphinx-build +# SPHINXAPIDOC ?= javasphinx-apidoc +SOURCEDIR = . +BUILDDIR = ../../build/doc + +# npm is not always in the global PATH +NPM_PATH = $(shell npm bin) +NPM_PREFIX = $(shell npm prefix) + +.PHONY: doc-help Makefile api + +# Put it first so that "make" without argument is like "make help". +doc-help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile api + PATH=$(NPM_PATH):$$PATH $(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) +# sphinx-build -M html . ../../build/doc + +api: + PATH=$(NPM_PATH):$$PATH jsdoc -t $(NPM_PREFIX)/node_modules/jsdoc-sphinx/template -d $@ -r "../../src/core" diff --git a/src/doc/concepts.rst b/src/doc/concepts.rst new file mode 100644 index 0000000000000000000000000000000000000000..23e5fc4f6ddb666757fb9c79e192e07ffed8fb44 --- /dev/null +++ b/src/doc/concepts.rst @@ -0,0 +1,6 @@ +======================== +The concepts of pycaosdb +======================== + +Some text... + diff --git a/src/doc/conf.py b/src/doc/conf.py new file mode 100644 index 0000000000000000000000000000000000000000..8e627dd0c26f9d760c549abfab4cbc824baa9912 --- /dev/null +++ b/src/doc/conf.py @@ -0,0 +1,212 @@ +# -*- coding: utf-8 -*- +# +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('../caosdb')) + + +# -- Project information ----------------------------------------------------- + +import sphinx_rtd_theme + +project = 'caosdb-webui' +copyright = '2020, IndiScale GmbH' +author = 'Daniel Hornung' + +# The short X.Y version +version = '0.X.Y' +# The full version, including alpha/beta/rc tags +release = '0.x.y-beta-rc2' + + +# -- General configuration --------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ +# 'sphinx_js', + 'sphinx.ext.todo', + "sphinx.ext.autodoc", +# 'autoapi.extension', + "recommonmark", # For markdown files. + "sphinx_rtd_theme", + 'sphinx.ext.intersphinx', + 'sphinx.ext.mathjax', + 'sphinx.ext.ifconfig', + # 'sphinx.ext.napoleon', # For Google style docstrings +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = None + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = "sphinx_rtd_theme" + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +# html_static_path = ['_static'] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# +# html_sidebars = {} + + +# -- Options for HTMLHelp output --------------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = 'caosdb-webuidoc' + + +# -- Options for LaTeX output ------------------------------------------------ + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'caosdb-webui.tex', 'caosdb-webui Documentation', + 'IndiScale GmbH', 'manual'), +] + + +# -- Options for manual page output ------------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'caosdb-webui', 'caosdb-webui Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ---------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'caosdb-webui', 'caosdb-webui Documentation', + author, 'caosdb-webui', 'One line description of project.', + 'Miscellaneous'), +] + + +# -- Options for Epub output ------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = project + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +# +# epub_identifier = '' + +# A unique identification for the text. +# +# epub_uid = '' + +# A list of files that should not be packed into the epub file. +epub_exclude_files = ['search.html'] + + +# -- Extension configuration ------------------------------------------------- + +# -- Options for sphinx-js --------------------------------------------------- +# See also https://pypi.org/project/sphinx-js/ + +js_source_path = '../core/js/' +primary_domain = 'js' # Not strictly necessary? + +# -- Options for intersphinx extension --------------------------------------- + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {'https://docs.python.org/': None} + +# TODO Which options do we want? +autodoc_default_options = { + 'members': None, + 'undoc-members': None, +} + +# -- Options for sphinx-autoapi ---------------------------------------------- +# See also https://pypi.org/project/sphinx-js/ + +autoapi_type = 'javascript' +autoapi_dirs = ['../core/js/'] +autoapi_add_toctree_entry = False diff --git a/src/doc/extension.rst b/src/doc/extension.rst new file mode 100644 index 0000000000000000000000000000000000000000..18fd0f25a8ce75cd19edfa845b06f5c45bd3fd20 --- /dev/null +++ b/src/doc/extension.rst @@ -0,0 +1,13 @@ + +Extending the CaosDB Web Interface +================================== + +Here we collect information on how to extend the web interface as a developer. + +.. toctree:: + :maxdepth: 1 + :glob: + + extension/* + + diff --git a/src/doc/extension/forms.rst b/src/doc/extension/forms.rst new file mode 100644 index 0000000000000000000000000000000000000000..1bced612b5f9517c5ec149871cbf53321b4671d4 --- /dev/null +++ b/src/doc/extension/forms.rst @@ -0,0 +1,80 @@ + +Creating forms for the CaosDB Web Interface +=========================================== + +The ``form_elements`` module provides a library for generating forms from simple config objects. The forms are styled for the seamless integration into the CaosDB web interface and are especially useful for calling server side scripts. + +See also the :doc:`API documentation <../api/module-form_elements>` + +Examples +-------- + +Generating a generic form +^^^^^^^^^^^^^^^^^^^^^^^^^ + +The following code snippet adds a form to the body of the HTML document. + +.. code-block:: javascript + + function my_special_submit_handler (form) { + // handle form submision + }; + const config = { + name: "my_form", + fields: [ + { type: "reference_drop_down", name: "experiment_id", label: "Experiment", query: "FIND Record Experiment", required: true }, + { type: "integer", name: "number", label: "A Number", required: true }, + { type: "date", name: "date", label: "A Date", required: false }, + { type: "text", name: "comment", label: "A Comment", required: false }, + ], + submit: my_special_submit_handler + }; + const form = form_elements.make_form(config); + $("body").append(form); + +The form has four fields: + + 1. A drop-down menu which contains all Records of type "Experiment" as options, + 2. an integer field, labeled "A Number", + 3. a date field, labeled "A Date", and + 4. a text field, labeled "A Comment". + +The first two fields are required and the form cannot be submitted without it. The latter are optional. + +On submission, the function ``my_special_submit_handler`` is being called with the form element as only parameter. + +As the generated form is a plain HTML form, the javascript form API can be used. However, there are special methods in the ``form_elements`` module e.g. :doc:`get_fields <../api/module-form_elements>` which are especially designed to interact with the forms generated by the ``make_form`` factory. + +Calling a server-side script +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If you intend to call a server-side script, the config has to be changed a litte bit and the script calling is done by the ``form_elements`` module. There is no need to define the submit_hander anymore. Instead, just name the script which is to be called. + +.. code-block:: javascript + + const config = { + script: "process.py", + fields: [ + { type: "reference_drop_down", name: "experiment_id", label: "Experiment", query: "FIND Record Experiment", required: true }, + { type: "integer", name: "number", label: "A Number", required: true }, + { type: "date", name: "date", label: "A Date", required: false }, + { type: "text", name: "comment", label: "A Comment", required: false }, + ], + }; + const form = form_elements.make_form(config); + $("body").append(form); + +On submission, the form data will be send as a json file to the script and passed as the first parameter. The call would look like ``./process.py form.json`` and the file would contain, for example, + +.. code-block:: json + + { + "experiment_id": "234234", + "number": "400", + "date": "2020-12-24", + "comment": "This is a comment", + } + +For more and advanced options for the form see the :doc:`API documentation <../api/module-form_elements>` + + diff --git a/src/doc/genindex.rst b/src/doc/genindex.rst new file mode 100644 index 0000000000000000000000000000000000000000..48ab71fd283bb48564ac30e4c69e62bbd463cd77 --- /dev/null +++ b/src/doc/genindex.rst @@ -0,0 +1,4 @@ +.. This file is a placeholder and will be replaced. + +Index +===== diff --git a/src/doc/getting_started.md b/src/doc/getting_started.md new file mode 120000 index 0000000000000000000000000000000000000000..88332e357f5e06f3de522768ccdcd9e513c15f62 --- /dev/null +++ b/src/doc/getting_started.md @@ -0,0 +1 @@ +../../README_SETUP.md \ No newline at end of file diff --git a/src/doc/index.rst b/src/doc/index.rst new file mode 100644 index 0000000000000000000000000000000000000000..107c9052fd6cdafecd201eb17118d8e56f3da440 --- /dev/null +++ b/src/doc/index.rst @@ -0,0 +1,25 @@ + +Welcome to the documentation of CaosDB's web UI! +================================================ + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + :hidden: + + Getting started <getting_started> + Tutorials <tutorials/index> + Concepts <concepts> + Extending the UI <extension> + API <api/index> + + +This documentation helps you to :doc:`get started<getting_started>`, explains the most important +:doc:`concepts<concepts>` and offers a range of :doc:`tutorials<tutorials>`. + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`search` diff --git a/src/doc/tutorials/first_steps.rst b/src/doc/tutorials/first_steps.rst new file mode 100644 index 0000000000000000000000000000000000000000..48126b2aab6175da557919133493585eda59bbfa --- /dev/null +++ b/src/doc/tutorials/first_steps.rst @@ -0,0 +1,87 @@ +First Steps +=========== + +Before using or even manipulating data stored in CaosDB, it is important to +understand the way data is structured. Here, we will briefly look at this +structure. You can find more details here_. In CaosDB data is stored in objects called +`Records`. A `Record` can have multiple `Properties`, like numbers, text or references +to other `Records`. `RecordTypes` are kind of blue prints for `Records` and +provide a structure to the data. Let's look at an example: + +.. image:: model.svg + +.. The image is not good yet. Children should have properties of parents. + +This illustrates a simple data model used in the `demo instance`_ provided by `IndiScale`_. +It shows that the `RecordType` Analysis has among others the `Properties` +`quality_factor`, a number, and `date`, you guessed it... a date. The `Property` +`MusicalInstrumet` illustrates that a `Record` that has `Analysis` as a parent +`RecordType` should reference a `Record` that has the `MusicalInstrumet` `RecordType` as a parent. + +We recommend that you connect to the demo instance in order to try out the following +examples (see :doc:`Getting Started secton</getting_started>`.). However, you +can also translate the examples to the data model that you have at hand. + + + +Main Menu (WIP) +--------------- + + +.. note:: + By default only 10 Entities are shown on one page. You can get to + other pages with the “Next Page” and “Previous Page” buttons. + +:math:`\Rightarrow` What are the differences between the options of the +“Entities” menu? + +Entities, Records, Properties…What? + + +- semantic data modeling + +- entries in LinkAhead are like Objects + +- RecordType: blue print for data + +- Record: actual data + + +See also the +`wiki <https://gitlab.com/caosdb/caosdb/wikis/Concepts/Data%20Model>`__ +or the `paper <https://www.mdpi.com/2306-5729/4/2/83>`__ + +|image| + +References in two directions + +- | References in LinkAhead are directed: + | A Record A references another Record B + +- The referencing Record A has a corresponding Property. + +- The referenced Record B does not. + +- In order to get referencing Records in the Web Interface, click on the following button + (or “Backref” on older systems). + +|image1| + +File System +----------- + +- Clicking on “File System” in the main menu allows you to browse files + that LinkAhead knows about. + +- Typically, most files will be mounted from some file server. + +.. note:: You will not find any Records in this view (that are not Files). + + + +.. _here: https://gitlabio.something +.. _`demo instance`: https://demo.indiscale.com +.. _`IndiScale`: https://indiscale.com +.. |image| image:: model.svg +.. |image1| image:: References_button.png + :width: 4em diff --git a/src/doc/tutorials/index.rst b/src/doc/tutorials/index.rst new file mode 100644 index 0000000000000000000000000000000000000000..dbfeafed2a09d587ebd538a62ed9002943b52aa0 --- /dev/null +++ b/src/doc/tutorials/index.rst @@ -0,0 +1,11 @@ + +CaosDB Web Interface Tutorials +============================== + +This chapter contains the following tutorials: + +.. toctree:: + :maxdepth: 2 + :glob: + + * diff --git a/src/doc/tutorials/model.svg b/src/doc/tutorials/model.svg new file mode 100644 index 0000000000000000000000000000000000000000..2602cb43f15976305d48e6f2d5efeb3821e1d669 --- /dev/null +++ b/src/doc/tutorials/model.svg @@ -0,0 +1,632 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + contentScriptType="application/ecmascript" + contentStyleType="text/css" + height="502" + preserveAspectRatio="none" + version="1.1" + viewBox="0 0 407 502" + width="407" + zoomAndPan="magnify" + id="svg233" + sodipodi:docname="model.svg" + inkscape:version="0.92.4 5da689c313, 2019-01-14"> + <metadata + id="metadata237"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1920" + inkscape:window-height="1043" + id="namedview235" + showgrid="false" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:zoom="1.1817368" + inkscape:cx="112.55875" + inkscape:cy="257" + inkscape:window-x="1920" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="svg233" /> + <defs + id="defs11"> + <filter + height="3" + id="f64vrt8w3qxjw" + width="3" + x="-1" + y="-1"> + <feGaussianBlur + result="blurOut" + stdDeviation="2.0" + id="feGaussianBlur2" /> + <feColorMatrix + in="blurOut" + result="blurOut2" + type="matrix" + values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .4 0" + id="feColorMatrix4" /> + <feOffset + dx="4.0" + dy="4.0" + in="blurOut2" + result="blurOut3" + id="feOffset6" /> + <feBlend + in="SourceGraphic" + in2="blurOut3" + mode="normal" + id="feBlend8" /> + </filter> + </defs> + <rect + style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.13385832;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="rect4867" + width="407" + height="502" + x="0" + y="0" /> + <polygon + id="polygon13" + style="fill:#dddddd;stroke:#000000;stroke-width:1.5;filter:url(#f64vrt8w3qxjw)" + points="533.5,526 126.5,526 126.5,24 236.5,24 243.5,46.2969 533.5,46.2969 " + transform="translate(-126.5,-24)" /> + <line + id="line15" + y2="22.296902" + y1="22.296902" + x2="117" + x1="0" + style="stroke:#000000;stroke-width:1.5" /> + <text + style="font-weight:bold;font-size:14px;font-family:sans-serif;fill:#000000" + id="text17" + y="38.995098" + x="130.5" + textLength="104" + lengthAdjust="spacingAndGlyphs" + font-weight="bold" + font-size="14" + transform="translate(-126.5,-24)">RecordTypes</text> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text42" + y="144.7104" + x="461" + textLength="0" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)" /> + <rect + y="411" + x="16" + width="116" + style="fill:#fefece;stroke:#a80036;stroke-width:1.5;filter:url(#f64vrt8w3qxjw)" + id="Manufacturer" + height="60.804699" /> + <circle + r="11" + id="ellipse47" + style="fill:#ff1111;stroke:#a80036;stroke-width:1" + cy="427" + cx="31" /> + <path + inkscape:connector-curvature="0" + id="path49" + d="m 33.9688,432.6406 q -0.5782,0.2969 -1.2188,0.4375 -0.6406,0.1563 -1.3437,0.1563 -2.5,0 -3.8282,-1.6406 -1.3125,-1.6563 -1.3125,-4.7813 0,-3.125 1.3125,-4.7812 1.3282,-1.6563 3.8282,-1.6563 0.7031,0 1.3437,0.1563 0.6563,0.1562 1.2188,0.4531 v 2.7187 q -0.625,-0.5781 -1.2188,-0.8437 -0.5937,-0.2813 -1.2187,-0.2813 -1.3438,0 -2.0313,1.0782 -0.6875,1.0625 -0.6875,3.1562 0,2.0938 0.6875,3.1719 0.6875,1.0625 2.0313,1.0625 0.625,0 1.2187,-0.2656 0.5938,-0.2813 1.2188,-0.8594 z" /> + <text + style="font-size:12px;font-family:sans-serif;fill:#000000" + id="text51" + y="455.1543" + x="171.5" + textLength="84" + lengthAdjust="spacingAndGlyphs" + font-size="12" + transform="translate(-126.5,-24)">Manufacturer</text> + <line + id="line53" + y2="443" + y1="443" + x2="131" + x1="17" + style="stroke:#a80036;stroke-width:1.5" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text55" + y="481.21039" + x="152.5" + textLength="0" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)" /> + <line + id="line57" + y2="463.80469" + y1="463.80469" + x2="131" + x1="17" + style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" /> + <rect + y="235" + x="16" + width="174" + style="fill:#fefece;stroke:#a80036;stroke-width:1.5;filter:url(#f64vrt8w3qxjw)" + id="MusicalInstrument" + height="101.6211" /> + <circle + r="11" + id="ellipse60" + style="fill:#ff1111;stroke:#a80036;stroke-width:1" + cy="251" + cx="43.600006" /> + <path + inkscape:connector-curvature="0" + id="path62" + d="m 46.5688,256.6406 q -0.5782,0.2969 -1.2188,0.4375 -0.6406,0.1563 -1.3437,0.1563 -2.5,0 -3.8282,-1.6406 -1.3125,-1.6563 -1.3125,-4.7813 0,-3.125 1.3125,-4.7812 1.3282,-1.6563 3.8282,-1.6563 0.7031,0 1.3437,0.1563 0.6563,0.1562 1.2188,0.4531 v 2.7187 q -0.625,-0.5781 -1.2188,-0.8437 -0.5937,-0.2813 -1.2187,-0.2813 -1.3438,0 -2.0313,1.0782 -0.6875,1.0625 -0.6875,3.1562 0,2.0938 0.6875,3.1719 0.6875,1.0625 2.0313,1.0625 0.625,0 1.2187,-0.2656 0.5938,-0.2813 1.2188,-0.8594 z" /> + <text + style="font-size:12px;font-family:sans-serif;fill:#000000" + id="text64" + y="279.1543" + x="186.89999" + textLength="114" + lengthAdjust="spacingAndGlyphs" + font-size="12" + transform="translate(-126.5,-24)">MusicalInstrument</text> + <line + id="line66" + y2="267" + y1="267" + x2="189" + x1="17" + style="stroke:#a80036;stroke-width:1.5" /> + <line + id="line68" + y2="281.40231" + y1="281.40231" + x2="73.5" + x1="17" + style="stroke:#a80036;stroke-width:1.5" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text70" + y="308.71039" + x="200" + textLength="59" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">Properties</text> + <line + id="line72" + y2="281.40231" + y1="281.40231" + x2="189" + x1="132.5" + style="stroke:#a80036;stroke-width:1.5" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text74" + y="341.2222" + x="148.5" + textLength="86" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">price (DOUBLE)</text> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text76" + y="354.02689" + x="148.5" + textLength="162" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">Manufacturer (Manufacturer)</text> + <line + id="line78" + y2="300.60941" + y1="300.60941" + x2="62" + x1="17" + style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text80" + y="327.91751" + x="188.5" + textLength="82" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">recommended</text> + <line + id="line82" + y2="300.60941" + y1="300.60941" + x2="189" + x1="144" + style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" /> + <rect + y="411" + x="167.5" + width="65" + style="fill:#fefece;stroke:#a80036;stroke-width:1.5;filter:url(#f64vrt8w3qxjw)" + id="Violin" + height="60.804699" /> + <circle + r="11" + id="ellipse85" + style="fill:#ff1111;stroke:#a80036;stroke-width:1" + cy="427" + cx="182.5" /> + <path + inkscape:connector-curvature="0" + id="path87" + d="m 185.4688,432.6406 q -0.5782,0.2969 -1.2188,0.4375 -0.6406,0.1563 -1.3437,0.1563 -2.5,0 -3.8282,-1.6406 -1.3125,-1.6563 -1.3125,-4.7813 0,-3.125 1.3125,-4.7812 1.3282,-1.6563 3.8282,-1.6563 0.7031,0 1.3437,0.1563 0.6563,0.1562 1.2188,0.4531 v 2.7187 q -0.625,-0.5781 -1.2188,-0.8437 -0.5937,-0.2813 -1.2187,-0.2813 -1.3438,0 -2.0313,1.0782 -0.6875,1.0625 -0.6875,3.1562 0,2.0938 0.6875,3.1719 0.6875,1.0625 2.0313,1.0625 0.625,0 1.2187,-0.2656 0.5938,-0.2813 1.2188,-0.8594 z" /> + <text + style="font-size:12px;font-family:sans-serif;fill:#000000" + id="text89" + y="455.1543" + x="323" + textLength="33" + lengthAdjust="spacingAndGlyphs" + font-size="12" + transform="translate(-126.5,-24)">Violin</text> + <line + id="line91" + y2="443" + y1="443" + x2="231.5" + x1="168.5" + style="stroke:#a80036;stroke-width:1.5" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text93" + y="481.21039" + x="304" + textLength="0" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)" /> + <line + id="line95" + y2="463.80469" + y1="463.80469" + x2="231.5" + x1="168.5" + style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" /> + <rect + y="397" + x="267.5" + width="119" + style="fill:#fefece;stroke:#a80036;stroke-width:1.5;filter:url(#f64vrt8w3qxjw)" + id="Guitar" + height="88.816399" /> + <circle + r="11" + id="ellipse98" + style="fill:#ff1111;stroke:#a80036;stroke-width:1" + cy="413" + cx="304.54999" /> + <path + inkscape:connector-curvature="0" + id="path100" + d="m 307.5188,418.6406 q -0.5782,0.2969 -1.2188,0.4375 -0.6406,0.1563 -1.3437,0.1563 -2.5,0 -3.8282,-1.6406 -1.3125,-1.6563 -1.3125,-4.7813 0,-3.125 1.3125,-4.7812 1.3282,-1.6563 3.8282,-1.6563 0.7031,0 1.3437,0.1563 0.6563,0.1562 1.2188,0.4531 v 2.7187 q -0.625,-0.5781 -1.2188,-0.8437 -0.5937,-0.2813 -1.2187,-0.2813 -1.3438,0 -2.0313,1.0782 -0.6875,1.0625 -0.6875,3.1562 0,2.0938 0.6875,3.1719 0.6875,1.0625 2.0313,1.0625 0.625,0 1.2187,-0.2656 0.5938,-0.2813 1.2188,-0.8594 z" /> + <text + style="font-size:12px;font-family:sans-serif;fill:#000000" + id="text102" + y="441.1543" + x="449.95001" + textLength="38" + lengthAdjust="spacingAndGlyphs" + font-size="12" + transform="translate(-126.5,-24)">Guitar</text> + <line + id="line104" + y2="429" + y1="429" + x2="385.5" + x1="268.5" + style="stroke:#a80036;stroke-width:1.5" /> + <line + id="line106" + y2="443.40231" + y1="443.40231" + x2="297.5" + x1="268.5" + style="stroke:#a80036;stroke-width:1.5" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text108" + y="470.71039" + x="424" + textLength="59" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">Properties</text> + <line + id="line110" + y2="443.40231" + y1="443.40231" + x2="385.5" + x1="356.5" + style="stroke:#a80036;stroke-width:1.5" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text112" + y="503.2222" + x="400" + textLength="107" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">electric (BOOLEAN)</text> + <line + id="line114" + y2="462.60941" + y1="462.60941" + x2="286" + x1="268.5" + style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text116" + y="489.91751" + x="412.5" + textLength="82" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">recommended</text> + <line + id="line118" + y2="462.60941" + y1="462.60941" + x2="385.5" + x1="368" + style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" /> + <rect + y="255.5" + x="225.5" + width="165" + style="fill:#fefece;stroke:#a80036;stroke-width:1.5;filter:url(#f64vrt8w3qxjw)" + id="SoundQualityAnalyzer" + height="60.804699" /> + <circle + r="11" + id="ellipse121" + style="fill:#ff1111;stroke:#a80036;stroke-width:1" + cy="271.5" + cx="240.5" /> + <path + inkscape:connector-curvature="0" + id="path123" + d="m 243.4688,277.1406 q -0.5782,0.2969 -1.2188,0.4375 -0.6406,0.1563 -1.3437,0.1563 -2.5,0 -3.8282,-1.6406 -1.3125,-1.6563 -1.3125,-4.7813 0,-3.125 1.3125,-4.7812 1.3282,-1.6563 3.8282,-1.6563 0.7031,0 1.3437,0.1563 0.6563,0.1562 1.2188,0.4531 v 2.7187 q -0.625,-0.5781 -1.2188,-0.8437 -0.5937,-0.2813 -1.2187,-0.2813 -1.3438,0 -2.0313,1.0782 -0.6875,1.0625 -0.6875,3.1562 0,2.0938 0.6875,3.1719 0.6875,1.0625 2.0313,1.0625 0.625,0 1.2187,-0.2656 0.5938,-0.2813 1.2188,-0.8594 z" /> + <text + style="font-size:12px;font-family:sans-serif;fill:#000000" + id="text125" + y="299.6543" + x="381" + textLength="133" + lengthAdjust="spacingAndGlyphs" + font-size="12" + transform="translate(-126.5,-24)">SoundQualityAnalyzer</text> + <line + id="line127" + y2="287.5" + y1="287.5" + x2="389.5" + x1="226.5" + style="stroke:#a80036;stroke-width:1.5" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text129" + y="325.71039" + x="362" + textLength="0" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)" /> + <line + id="line131" + y2="308.30469" + y1="308.30469" + x2="389.5" + x1="226.5" + style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" /> + <rect + y="35" + x="20" + width="268" + style="fill:#fefece;stroke:#a80036;stroke-width:1.5;filter:url(#f64vrt8w3qxjw)" + id="Analysis" + height="140.0352" /> + <circle + r="11" + id="ellipse134" + style="fill:#ff1111;stroke:#a80036;stroke-width:1" + cy="51" + cx="124.75" /> + <path + inkscape:connector-curvature="0" + id="path136" + d="m 127.7188,56.6406 q -0.5782,0.2969 -1.2188,0.4375 -0.6406,0.1563 -1.3437,0.1563 -2.5,0 -3.8282,-1.6406 -1.3125,-1.6563 -1.3125,-4.7813 0,-3.125 1.3125,-4.7812 1.3282,-1.6563 3.8282,-1.6563 0.7031,0 1.3437,0.1563 0.6563,0.1562 1.2188,0.4531 v 2.7187 q -0.625,-0.5781 -1.2188,-0.8437 -0.5937,-0.2813 -1.2187,-0.2813 -1.3438,0 -2.0313,1.0782 -0.6875,1.0625 -0.6875,3.1562 0,2.0938 0.6875,3.1719 0.6875,1.0625 2.0313,1.0625 0.625,0 1.2187,-0.2656 0.5938,-0.2813 1.2188,-0.8594 z" /> + <text + style="font-size:12px;font-family:sans-serif;fill:#000000" + id="text138" + y="79.154297" + x="271.75" + textLength="50" + lengthAdjust="spacingAndGlyphs" + font-size="12" + transform="translate(-126.5,-24)">Analysis</text> + <line + id="line140" + y2="67" + y1="67" + x2="287" + x1="21" + style="stroke:#a80036;stroke-width:1.5" /> + <line + id="line142" + y2="81.402298" + y1="81.402298" + x2="124.5" + x1="21" + style="stroke:#a80036;stroke-width:1.5" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text144" + y="108.7104" + x="251" + textLength="59" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">Properties</text> + <line + id="line146" + y2="81.402298" + y1="81.402298" + x2="287" + x1="183.5" + style="stroke:#a80036;stroke-width:1.5" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text148" + y="141.2222" + x="152.5" + textLength="134" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">quality_factor (DOUBLE)</text> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text150" + y="154.0269" + x="152.5" + textLength="92" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">date (DATETIME)</text> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text152" + y="166.8315" + x="152.5" + textLength="111" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">report (REFERENCE)</text> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text154" + y="179.6362" + x="152.5" + textLength="256" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">SoundQualityAnalyzer (SoundQualityAnalyzer)</text> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text156" + y="192.4409" + x="152.5" + textLength="220" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">MusicalInstrument (MusicalInstrument)</text> + <line + id="line158" + y2="100.6094" + y1="100.6094" + x2="113" + x1="21" + style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" /> + <text + style="font-size:11px;font-family:sans-serif;fill:#000000" + id="text160" + y="127.9175" + x="239.5" + textLength="82" + lengthAdjust="spacingAndGlyphs" + font-size="11" + transform="translate(-126.5,-24)">recommended</text> + <line + id="line162" + y2="100.6094" + y1="100.6094" + x2="287" + x1="195" + style="stroke:#a80036;stroke-width:1;stroke-dasharray:1, 2" /> + <path + inkscape:connector-curvature="0" + style="fill:none;stroke:#a80036;stroke-width:1" + id="MusicalInstrument-Violin" + d="m 145.51,354.27 c 12.48,19.76 25.51,40.37 35.69,56.48" /> + <polygon + id="polygon211" + style="fill:none;stroke:#a80036;stroke-width:1" + points="261.26,361.26 277.86,374.43 266.03,381.91 " + transform="translate(-126.5,-24)" /> + <path + inkscape:connector-curvature="0" + style="fill:none;stroke:#a80036;stroke-width:1" + id="MusicalInstrument-Guitar" + d="m 192.64,348.42 c 25.04,17.17 51.64,35.39 74.51,51.06" /> + <polygon + id="polygon214" + style="fill:none;stroke:#a80036;stroke-width:1" + points="302.54,361.05 322.99,366.58 315.08,378.13 " + transform="translate(-126.5,-24)" /> + <path + inkscape:connector-curvature="0" + style="fill:none;stroke:#a80036;stroke-width:1" + id="MusicalInstrument-Manufacturer" + d="m 91.08,350.09 c -3.97,21 -8.2,43.41 -11.46,60.66" /> + <polygon + id="polygon217" + style="fill:#a80036;stroke:#a80036;stroke-width:1" + points="220,361.26 214.9551,366.4126 217.771,373.0512 222.8159,367.8986 " + transform="translate(-126.5,-24)" /> + <path + inkscape:connector-curvature="0" + style="fill:none;stroke:#a80036;stroke-width:1" + id="Analysis-SoundQualityAnalyzer" + d="m 222.3,185.39 c 21.35,24.82 43.61,50.69 60.09,69.84" /> + <polygon + id="polygon220" + style="fill:#a80036;stroke:#a80036;stroke-width:1" + points="340.04,199.21 340.9231,206.3668 347.869,208.3043 346.9859,201.1475 " + transform="translate(-126.5,-24)" /> + <path + inkscape:connector-curvature="0" + style="fill:none;stroke:#a80036;stroke-width:1" + id="Analysis-MusicalInstrument" + d="m 130.66,187.93 c -4.56,16 -9.21,32.33 -13.37,46.92" /> + <polygon + id="polygon223" + style="fill:#a80036;stroke:#a80036;stroke-width:1" + points="260.78,199.21 255.287,203.8819 257.4868,210.7493 262.9798,206.0774 " + transform="translate(-126.5,-24)" /> +</svg> diff --git a/src/doc/tutorials/query.rst b/src/doc/tutorials/query.rst new file mode 100644 index 0000000000000000000000000000000000000000..29d998cc1dbce5c5e17ae4cf9f60176b9f88861a --- /dev/null +++ b/src/doc/tutorials/query.rst @@ -0,0 +1,143 @@ + +Querying CaosDB +=============== + +You should have the web interface of a CaosDB instance at hand. If you do not +have one, you can visit https://demo.indiscale.com + +Introduction +------------ + +The semantic data model of CaosDB allows efficient data access. The +CaosDB Query Language (CQL) is used to search data. Queries can be entered in +the webinterface under the respective menu entry. + +Let's start with a simple one:: + + FIND RECORD MusicalInstrument + +Most queries simply start with the ``FIND`` keyword and describe what we are +looking for behind that. The ``RECORD`` keyword denotes that we are only looking +for Records (and not Files, Properties or RecordTypes). Finally, we provided +a RecordType name: MusicalInstrument. This means that we will get all Records +that have this RecordType as parent. Try it out! + +Let's look at:: + + FIND Guitar + +When we leave out the ``RECORD`` keyword, we will get every entity that is a +Guitar. When you submit this query you should find also a RecordType Guitar +in the results. Using ``FIND RecordType Guitar`` would restrict the result to +only that RecordType. + +Note, that you cannot only provide RecordType names after the ``FIND``, but names +in general: ``FIND RECORD Nice Guitar``. This will give you a Record with the +name "Nice Guitar" (if one exists... and there should be one in the demo instance). + +While it does not matter whether you use capital letters or not, the names have to +be exact. There are two features that make it easy to use names for querying +in spite of this: +- You can use "*" to match any string. E.g. ``FIND RECORD Nice*`` +- After typing three letters, names that start with those three are +suggested by the auto completion. + +.. note:: + + Train yourself by trying to guess what the result will be before + actually executing the query. + + +Searching Data Using Properties +-------------------------------- + +Looking for entities with certain names or such that have certain parents is +nice. However, the queries become really useful if we can impose further conditions +on the results. Let's start with an example again:: + + FIND Guitar with price > 10000 + +This should list expensive guitars where are in the demo instance. Thus, +we are using a property (the price) of the Guitar Records to restrict the +result set. In general this looks like:: + + FIND <Name> <Property Filter> + +Typically, the filter has the form ``<Property> <Operator> <Value>``, +for example ``length >= 0.7mm``. +There are many filters available. You can check the specification for a comprehensive description of +those. Here, we will only look at the most common examples. + + +If you only want to assure that Records have a certain Property, without imposing +constrains on the value, you can use:: + + FIND RECORD MusicalInstrument WITH Manufacturer + + +Similarly, to what we saw above when using incomplete names, you can use a "*" +to match parts of text properties:: + + FIND RECORD WITH serialNumber like KN* + +There is large number of operators that can be used together with dates or +timestamps. One of the most useful is probably:: + + FIND RECORD WITH date in 2019 + +A lot of valuable information is often stored in the relations among data, i.e. in +the references of entities. So how can we use those?:: + + FIND RECORD WHICH REFERENCES A Guitar + +This should be pretty self explanatory. And it is also possible to check for +references in the other direction:: + + FIND RECORD WHICH IS REFERENCED BY A Analysis + +You can also simply provide the ID of the entity:: + + FIND RECORD WHICH IS REFERENCED BY 123`` + + +Using Multiple Filters +---------------------- + +Often, one condition is not sufficient. Thus multiple filters/conditions can be combined. +This can for example be done using the following structure:: + + FIND <Name> <Property Filter> (AND|OR) <Property Filter> + +An example would be:: + + FIND Guitar WITH price>48 AND electric=TRUE + +Furthermore, reference conditions can be nested:: + + FIND <Name> WHICH REFERENCES <Name> WHICH REFERENCES <Name> + + +For example:: + + FIND Manufacturer WHICH IS REFERENCED BY Guitar WHICH IS REFERENCED BY Analysis + + +Restricting Result Information +------------------------------ + +Using ``COUNT`` instead of ``FIND`` will only return the number of +entities in the result set. + +.. note:: This is often useful when experimenting with queries. + +Using ``SELECT ... FROM`` instead of ``FIND`` returns specific +information in a table. A comma separated list of Property names can be provided behind the +``SELECT`` keyword:: + + SELECT price, electric FROM Guitar + +Or:: + + SELECT quality_factor, report, date FROM Analysis WHICH REFERENCES A Guitar WITH electric=TRUE + + diff --git a/src/ext/js/fileupload.js b/src/ext/js/fileupload.js index e21ccf7f6035a8170bd1a0f4c7f5868d56c83b37..1c069f2a8fdded69a2b7cc93f27601226e1d149c 100644 --- a/src/ext/js/fileupload.js +++ b/src/ext/js/fileupload.js @@ -235,8 +235,6 @@ var fileupload = new function() { } this.init = function() { - fileupload.debug("init"); - // add global listener for start_edit event document.body.addEventListener(edit_mode.start_edit.type, function(e) { $(e.target).find(".caosdb-properties .caosdb-f-entity-property").each(function(idx) { diff --git a/src/server_side_scripting/ext_file_download/zip_files.py b/src/server_side_scripting/ext_file_download/zip_files.py new file mode 100755 index 0000000000000000000000000000000000000000..65f27c9d901a6790456b8addb636bd55b7fe0c7e --- /dev/null +++ b/src/server_side_scripting/ext_file_download/zip_files.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +# ** header v3.0 +# This file is a part of the CaosDB Project. +# +# Copyright (C) 2019 IndiScale GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# +# ** end header + +"""Creates a zip file from multiple file entities. """ + +import argparse +import datetime +import io +import logging +import os +import sys +from tempfile import NamedTemporaryFile +from zipfile import ZipFile + +import caosdb as db +import pandas as pd +from caosadvancedtools.serverside import helper +from caosdb import CaosDBException, ConsistencyError, EntityDoesNotExistError + + +def _parse_arguments(): + """Parses the command line arguments. + + Takes into account defaults from the environment (where known). + """ + parser = argparse.ArgumentParser(description='__doc__') + parser.add_argument('-a', '--auth-token', required=False, + help=("An authentication token. If not provided caosdb" + " pylib will search for other methods of " + "authentication if necessary.")) + parser.add_argument('ids', help="list of entity ids.") + parser.add_argument('table', help="tsv table to be saved (as string).") + + return parser.parse_args() + + +def collect_files_in_zip(ids, table): + # File output + now = datetime.datetime.now() + zip_name = "files.{time}.zip".format( + time=now.strftime("%Y-%m-%dT%H_%M_%S")) + zip_display_path, zip_internal_path = helper.get_shared_filename(zip_name) + with ZipFile(zip_internal_path, 'w') as zf: + nc = helper.NameCollector() + + # add the table which has been genereated by the webui table exporter + with NamedTemporaryFile(delete=False) as table_file: + # the file has been transmitted as string and has to be written to + # a file first. + table_file.write(table.encode()) + zf.write(table_file.name, "selected_table.tsv") + + # download and add all files + for file_id in ids: + try: + tmp = db.execute_query("FIND {a:} WITH ID={a:}".format( + a=file_id), + unique=True) + except EntityDoesNotExistError as e: + # TODO + # Current behavior: script terminates with error if just one + # file cannot be retrieved. + # Desired behavior: The script should go on with the other + # ids, but the user should be informed about the missing files. + # How should we do this? + logger = logging.getLogger("caosadvancedtools") + logger.error("Did not find Entity with ID={}.".format( + file_id)) + + raise e + savename = nc.get_unique_savename(os.path.basename(tmp.path)) + val_file = helper.get_file_via_download( + tmp, logger=logging.getLogger("caosadvancedtools")) + + zf.write(val_file, savename) + + return zip_display_path + + +def main(): + args = _parse_arguments() + + if hasattr(args, "auth_token") and args.auth_token: + db.configure_connection(auth_token=args.auth_token) + + id_list = [int(el) for el in args.ids.split(",")] + + zip_file = collect_files_in_zip(id_list, args.table) + + print(zip_file) + + +if __name__ == "__main__": + main() diff --git a/src/server_side_scripting/ext_table_preview/pandas_table_preview.py b/src/server_side_scripting/ext_table_preview/pandas_table_preview.py new file mode 100755 index 0000000000000000000000000000000000000000..c0659d9b1839c43e0629a878d792c414577ea344 --- /dev/null +++ b/src/server_side_scripting/ext_table_preview/pandas_table_preview.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +# encoding: utf-8 +# +# ** header v3.0 +# This file is a part of the CaosDB Project. +# +# Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> +# Copyright (C) 2020 Henrik tom Wörden <h.tomwoerden@indiscale.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# +# ** end header +# + +""" +This script tries to read typical table data files (.csv etc.) with pandas and +creates a html (partial) representation of the table. +""" + +import logging +import os +import sys +from datetime import datetime + +import caosdb as db +import pandas as pd +from caosadvancedtools.serverside.helper import get_argument_parser +from caosadvancedtools.serverside.logging import configure_server_side_logging + +MAXIMUMFILESIZE = 1e8 +VALID_ENDINGS = [".csv", ".tsv", ".xls", ".xlsx"] + + +def get_file(eid): + """ retrieves the file entity from caosdb """ + try: + fi = db.File(id=eid) + fi.retrieve() + except db.exceptions.EntityDoesNotExistError: + print("Cannot create preview for Entity with ID={}, because it seems" + "not to exist.".format(eid), file=sys.stderr) + sys.exit(1) + + return fi + + +def size_is_ok(fi): + """ show previews only for files that are not too large """ + + return fi.size <= MAXIMUMFILESIZE + + +def get_ending(fipath): + """ return which of the valid endings (tsv etc.) is the one present""" + + for end in VALID_ENDINGS: + if fipath.lower().endswith(end): + return end + + return None + + +def ending_is_valid(fipath): + """ return whether the ending indicates a file type that can be treated""" + + return get_ending(fipath) is not None + + +def read_file(fipath, ftype): + """ tries to read the provided file """ + + try: + if ftype in [".xls", ".xlsx"]: + df = pd.read_excel(fipath) + elif ftype == ".tsv": + df = pd.read_csv(fipath, sep="\t", comment="#") + elif ftype == ".csv": + df = pd.read_csv(fipath, comment="#") + else: + print("File type unknown: {}".format(ftype)) + raise RuntimeError("") + except Exception: + raise ValueError() + + return df + + +def create_table_preview(fi): + if not ending_is_valid(fi.path): + print("Cannot create preview for Entity with ID={}, because download" + "failed.".format(entity_id), file=sys.stderr) + sys.exit(5) + + ending = get_ending(fi.path) + + if not size_is_ok(fi): + print("Skipped creating a preview for Entity with ID={}, because the" + "file is large!".format(entity_id), file=sys.stderr) + sys.exit(2) + + try: + tmpfile = fi.download() + except Exception: + print("Cannot create preview for Entity with ID={}, because download" + "failed.".format(entity_id), file=sys.stderr) + + sys.exit(3) + + try: + df = read_file(tmpfile, ending) + except ValueError: + print("Cannot read File Entity with ID={}.".format(entity_id), + file=sys.stderr) + sys.exit(4) + + print(df.to_html(max_cols=10, max_rows=10)) + + +if __name__ == "__main__": + conlogger = logging.getLogger("connection") + conlogger.setLevel(level=logging.ERROR) + + parser = get_argument_parser() + args = parser.parse_args() + + debug_file = configure_server_side_logging() + logger = logging.getLogger("caosadvancedtools") + + db.configure_connection(auth_token=args.auth_token) + entity_id = args.filename + + fi = get_file(entity_id) + + create_table_preview(fi) diff --git a/test/core/index.html b/test/core/index.html index e4d6d81634aa73c2d71385545fb917392ba5df1f..5786583c3df7bf281bc3a151f67b378dd22ca5ff 100644 --- a/test/core/index.html +++ b/test/core/index.html @@ -37,6 +37,8 @@ <script src="js/bootstrap.js"></script> <script src="js/bootstrap-select.js"></script> <script src="js/bootstrap-autocomplete.min.js"></script> + <script src="js/utif.js"></script> + <script src="js/pako.js"></script> <script src="js/webcaosdb.js"></script> <script src="js/plotly.js"></script> <script> @@ -54,6 +56,7 @@ <script src="js/edit_mode.js"></script> <script src="js/query_shortcuts.js"></script> <script src="js/ext_references.js"></script> + <script src="js/ext_file_download.js"></script> <script src="js/ext_xls_download.js"></script> <script src="js/form_elements.js"></script> <script src="js/tour.js"></script> @@ -65,11 +68,13 @@ <script src="js/proj4leaflet.js"></script> <script src="js/ext_map.js"></script> <script src="js/ext_applicable.js"></script> + <script src="js/ext_table_preview.js"></script> <script src="js/ext_bottom_line.js"></script> <script src="js/ext_revisions.js"></script> - <script src="js/autocomplete.js"></script> + <script src="js/ext_autocomplete.js"></script> <script src="js/ext_sss_markdown.js"></script> <script src="js/ext_trigger_crawler_form.js"></script> + <script src="js/ext_bookmarks.js"></script> <!--EXTENSIONS--> <script src="js/modules/webcaosdb.js.js"></script> <script src="js/modules/caosdb.js.js"></script> @@ -81,6 +86,7 @@ <script src="js/modules/navbar.xsl.js"></script> <script src="js/modules/edit_mode.js.js"></script> <script src="js/modules/ext_xls_download.js.js"></script> + <script src="js/modules/ext_file_download.js.js"></script> <script src="js/modules/query_shortcuts.js.js"></script> <script src="js/modules/form_elements.js.js"></script> <script src="js/modules/ext_references.js.js"></script> @@ -88,8 +94,9 @@ <script src="js/modules/ext_applicable.js.js"></script> <script src="js/modules/ext_bottom_line.js.js"></script> <script src="js/modules/ext_revisions.js.js"></script> - <script src="js/modules/autocomplete.js.js"></script> + <script src="js/modules/ext_autocomplete.js.js"></script> <script src="js/modules/ext_sss_markdown.js.js"></script> <script src="js/modules/ext_trigger_crawler_form.js.js"></script> + <script src="js/modules/ext_bookmarks.js.js"></script> </body> </html> diff --git a/test/core/js/modules/autocomplete.js.js b/test/core/js/modules/autocomplete.js.js deleted file mode 100644 index b75acdcad8b08e548423513ccdf78d7ca0ea7359..0000000000000000000000000000000000000000 --- a/test/core/js/modules/autocomplete.js.js +++ /dev/null @@ -1,71 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2019 IndiScale GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ - -'use strict'; - -QUnit.module("autocomplete.js", { - before: function (assert){ - autocomplete.retrieve_names = async function () { - return ['IceCore', 'Bag', 'IceSample', 'IceCream', 'Palette']; - } - } -}); - -QUnit.test("availability", function(assert) { - //assert.ok(bootstrap..init, "init available"); - assert.equal(autocomplete.version, "0.1", "test version"); - assert.ok(autocomplete.init, "init available"); -}); - - - -QUnit.test("filter", function(assert) { - assert.equal(autocomplete.filter('IceCore','Ice'), true, 'test filter') - assert.equal(autocomplete.filter('IceCore','iCe'), true, 'test filter') - assert.equal(autocomplete.filter('IceCore','Core'), false, 'test filter') - assert.equal(autocomplete.filter('Bag','Ice'), false, 'test filter') -}); - -QUnit.test("search", async function(assert) { - - var done = assert.async(2); - var gcallback = function(expresults){ - return function (results) { - assert.propEqual( - results, - expresults, - "test list filter"); - done(); - }; - }; - - await autocomplete.search("Ice", gcallback( - ['IceCore', 'IceSample', 'IceCream'] - )); - - await autocomplete.search("Core", gcallback([])); -}); - -QUnit.test("class", function(assert) { - assert.ok(autocomplete.toggle_completion , "toggle available"); - assert.ok(autocomplete.toggle_completion() , "toggle runs"); -}); diff --git a/test/core/js/modules/caosdb.js.js b/test/core/js/modules/caosdb.js.js index 76141117ffc845917c0e5ff50a165e7718663a31..20dc4b4eee0116fecf57b0a178f622c4385f08b2 100644 --- a/test/core/js/modules/caosdb.js.js +++ b/test/core/js/modules/caosdb.js.js @@ -17,7 +17,7 @@ QUnit.module("caosdb.js", { }, err => {console.log(err);}); }, - + before: function(assert) { var done = assert.async(3); this.setTestDocument("x", done, ` @@ -474,3 +474,110 @@ QUnit.test("unset_entity_references", function(assert) { assert.equal(getProperties(r)[0].reference, true); } }); + + +QUnit.test("_constructXpaths", function (assert) { + assert.propEqual( + _constructXpaths([["id"], ["longitude"], ["latitude"]]), + ["@id", "Property[@name='longitude']", "Property[@name='latitude']"] + ); + assert.propEqual( + _constructXpaths([["Geo Location", "longitude"], ["latitude"]]), + ["Property[@name='Geo Location']//Property[@name='longitude']", "Property[@name='latitude']"] + ); + assert.propEqual( + _constructXpaths([["", "longitude"], ["latitude"]]), + ["Property//Property[@name='longitude']", "Property[@name='latitude']"] + ); + assert.propEqual( + _constructXpaths([["", "Geo Location", "", "longitude"]]), + ["Property//Property[@name='Geo Location']//Property//Property[@name='longitude']"] + ); +}); + + +QUnit.test("getPropertyValues", function (assert) { + const test_response = str2xml(` +<Response srid="851d063d-121b-4b67-98d4-6e5ad4d31e72" timestamp="1606214042274" baseuri="https://localhost:10443" count="8"> + <Query string="select Campaign.responsible.firstname from icecore" results="8"> + <ParseTree>(cq select (prop_sel (prop_subsel (selector_txt C a m p a i g n) . (prop_subsel (selector_txt r e s p o n s i b l e) . (prop_subsel (selector_txt f i r s t n a m e ))))) from (entity icecore) <EOF>)</ParseTree> + <Role/> + <Entity>icecore</Entity> + <Selection> + <Selector name="Campaign.responsible.firstname"/> + </Selection> + </Query> + <Record id="6525" name="Test_IceCore_1"> + <Permissions/> + <Property datatype="Campaign" id="6430" name="Campaign"> + <Record id="6516" name="Test-2020_Camp1"> + <Permissions/> + <Property datatype="REFERENCE" id="168" name="responsible"> + <Record id="6515" name="Test_Scientist"> + <Permissions/> + <Property datatype="DOUBLE" id="151" name="latitude" importance="FIX"> + 1.34 + <Permissions/> + </Property> + <Property datatype="DOUBLE" id="151" name="longitude" importance="FIX"> + 2 + <Permissions/> + </Property> + </Record> + <Permissions/> + </Property> + </Record> + <Permissions/> + </Property> + </Record> + <Record id="6526" name="Test_IceCore_2"> + <Permissions/> + <Property datatype="Campaign" id="6430" name="Campaign"> + <Record id="6516" name="Test-2020_Camp1"> + <Permissions/> + <Property datatype="REFERENCE" id="168" name="responsible"> + <Record id="6515" name="Test_Scientist"> + <Permissions/> + <Property datatype="DOUBLE" id="151" name="latitude" importance="FIX"> + 3 + <Permissions/> + </Property> + <Property datatype="DOUBLE" id="151" name="longitude" importance="FIX"> + 4.8345 + <Permissions/> + </Property> + </Record> + <Permissions/> + </Property> + </Record> + <Permissions/> + </Property> + </Record> +</Response>`); + + assert.propEqual( + getPropertyValues(test_response, [["id"], ["", "latitude"],["", "longitude"]]), + [["6525" ,"1.34", "2"], ["6526", "3", "4.8345"]]); +}); + +// Test for bug 103 +// If role is File when creating XML for entities, checksum, path and size must be given. +QUnit.test("unset_file_attributes", function(assert) { + // This should run: + var res1 = createEntityXML("Record", "test", 103, {}, {}); + assert.equal(xml2str(res1), "<Record id=\"103\" name=\"test\"/>"); + // This must throw an exception: + assert.throws(function () { + createEntityXML("File", "test", 103, {}, {}); + }); + // This should produce a valid XML. + var res2 = createEntityXML("File", "test", 103, {}, {}, + false, undefined, undefined, undefined, + "testfile.txt", "blablabla", 0); + assert.equal(xml2str(res2), "<File id=\"103\" name=\"test\" path=\"testfile.txt\" checksum=\"blablabla\" size=\"0\"/>"); + + var res3 = createFileXML("test", 103, {}, + "testfile.txt", "blablabla", 0, + undefined); + assert.equal(xml2str(res3), "<File id=\"103\" name=\"test\" path=\"testfile.txt\" checksum=\"blablabla\" size=\"0\"/>"); +}); diff --git a/test/core/js/modules/entity.xsl.js b/test/core/js/modules/entity.xsl.js index c607ee28bf7888f94e3086abf8e033e1532d0d09..59a2f8a6f9a03cf4990fe11bfd751d437a3ffc3e 100644 --- a/test/core/js/modules/entity.xsl.js +++ b/test/core/js/modules/entity.xsl.js @@ -264,6 +264,67 @@ QUnit.test("data-version-successor attribute", function(assert) { assert.equal($(html).find("div.caosdb-entity-panel[data-version-successor]").length, 0, "data-version-successor attribute not present"); }); +QUnit.test("version full history", function (assert) { + var xmlstr = ` + <Response username="user1" realm="Realm1" srid="31ce8ea1-6c9b-4a82-82ec-9f6f3edd2622" timestamp="1606225647516" baseuri="https://localhost:10443" count="1"> + <Record id="3373" name="TestRecord1-10thVersion" description="This is the 10th version."> + <Permissions> + <Permission name="RETRIEVE:HISTORY" /> + </Permissions> + <Version id="vid6" username="user1" realm="Realm1" date="date6" completeHistory="true"> + <Predecessor id="vid5" username="user1" realm="Realm1" date="date5"> + <Predecessor id="vid4" username="user1" realm="Realm1" date="date4"> + <Predecessor id="vid3" username="user1" realm="Realm1" date="date3"> + <Predecessor id="vid2" username="user1" realm="Realm1" date="date2"> + <Predecessor id="vid1" username="user1" realm="Realm1" date="date1" /> + </Predecessor> + </Predecessor> + </Predecessor> + </Predecessor> + <Successor id="vid7" username="user1" realm="Realm1" date="date7"> + <Successor id="vid8" username="user1" realm="Realm1" date="date8"> + <Successor id="vid9" username="user1" realm="Realm1" date="date9"> + <Successor id="vid10" username="user1" realm="Realm1" date="date10" /> + </Successor> + </Successor> + </Successor> + </Version> + <Parent id="3372" name="TestRT" /> + </Record> +</Response> +`; + var xml = str2xml(xmlstr); + var html = applyTemplates(xml, this.entityXSL, "entities", "*"); + var version_info = $(html).find(".caosdb-f-entity-version-info"); + var table_elem = $(version_info).find("table"); + + var TAB = "%09", NEWL = "%0A", usr = "user1@Realm1", + path = "/entitypath/3373"; + var export_table = `data:text/csv;charset=utf-8,Entity ID${TAB}Version ID${TAB}Date${TAB}User${TAB}URI${NEWL}3373${TAB}vid10${TAB}date10${TAB}${usr}${TAB}${path}@vid10${NEWL}3373${TAB}vid9${TAB}date9${TAB}${usr}${TAB}${path}@vid9${NEWL}3373${TAB}vid8${TAB}date8${TAB}${usr}${TAB}${path}@vid8${NEWL}3373${TAB}vid7${TAB}date7${TAB}${usr}${TAB}${path}@vid7${NEWL}3373${TAB}vid6${TAB}date6${TAB}${usr}${TAB}${path}@vid6${NEWL}3373${TAB}vid5${TAB}date5${TAB}${usr}${TAB}${path}@vid5${NEWL}3373${TAB}vid4${TAB}date4${TAB}${usr}${TAB}${path}@vid4${NEWL}3373${TAB}vid3${TAB}date3${TAB}${usr}${TAB}${path}@vid3${NEWL}3373${TAB}vid2${TAB}date2${TAB}${usr}${TAB}${path}@vid2${NEWL}3373${TAB}vid1${TAB}date1${TAB}${usr}${TAB}${path}@vid1` + assert.equal(version_info.length, 1); + assert.equal(table_elem.length, 1); + assert.equal(version_history.get_history_tsv(table_elem[0]), export_table); +}); + +QUnit.test("Transforming abstract properties", function (assert) { + var xmlstr = `<Property id="3842" name="reftotestrt" datatype="TestRT"> + <Version id="04ad505da057603a9177a1fcf6c9efd5f3690fe4" date="2020-11-23T10:38:02.936+0100" /> + </Property>`; + var xml = str2xml(xmlstr); + var html = applyTemplates(xml, this.entityXSL, "entity-body", "Property"); + var prop = getPropertyFromElement(html.firstElementChild); + assert.propEqual(prop, { + "datatype": "TestRT", + "html": {}, + "id": "3842", + "list": false, + "name": "reftotestrt", + "reference": true, + "unit": undefined, + "value": ""} + ); +}); + /* MISC FUNCTIONS */ function applyTemplates(xml, xsl, mode, select = "*") { let entryRule = '<xsl:template priority="9" match="/"><xsl:apply-templates select="' + select + '" mode="' + mode + '"/></xsl:template>'; diff --git a/test/core/js/modules/ext_autocomplete.js.js b/test/core/js/modules/ext_autocomplete.js.js new file mode 100644 index 0000000000000000000000000000000000000000..e8776f945b7bb46a0d431eb2d0ac0f7fe21419fc --- /dev/null +++ b/test/core/js/modules/ext_autocomplete.js.js @@ -0,0 +1,72 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2019 IndiScale GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ + +'use strict'; + +QUnit.module("ext_autocomplete.js", { + before: function (assert){ + ext_autocomplete.retrieve_names = async function () { + return ['IceCore', 'Bag', 'IceSample', 'IceCream', 'Palette']; + } + ext_autocomplete.init(); + + } +}); + +QUnit.test("availability", function(assert) { + //assert.ok(bootstrap..init, "init available"); + assert.equal(ext_autocomplete.version, "0.1", "test version"); + assert.ok(ext_autocomplete.init, "init available"); +}); + + + +QUnit.test("starts_with_filter", function(assert) { + assert.equal(ext_autocomplete.starts_with_filter('IceCore','Ice'), true, 'test filter') + assert.equal(ext_autocomplete.starts_with_filter('IceCore','iCe'), true, 'test filter') + assert.equal(ext_autocomplete.starts_with_filter('IceCore','Core'), false, 'test filter') + assert.equal(ext_autocomplete.starts_with_filter('Bag','Ice'), false, 'test filter') +}); + +QUnit.test("search", async function(assert) { + + var done = assert.async(2); + var gcallback = function(expresults){ + return function (results) { + assert.propEqual( + results, + expresults, + "test list filter"); + done(); + }; + }; + await ext_autocomplete.search("Ice", + gcallback( ['IceCore', 'IceSample', 'IceCream']) + ); + + await ext_autocomplete.search("Core", gcallback([])); +}); + +QUnit.test("class", function(assert) { + assert.ok(ext_autocomplete.switch_on_completion , "toggle available"); + assert.ok(ext_autocomplete.switch_on_completion() , "toggle runs"); +}); diff --git a/test/core/js/modules/ext_bookmarks.js.js b/test/core/js/modules/ext_bookmarks.js.js new file mode 100644 index 0000000000000000000000000000000000000000..831df74231e479d5b4550524f6bc0da617c98fb3 --- /dev/null +++ b/test/core/js/modules/ext_bookmarks.js.js @@ -0,0 +1,181 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ + +'use strict'; + +QUnit.module("ext_bookmarks.js", { + before: function (assert) { + // setup before module + ext_bookmarks.set_collection_id("test"); + // otherwise tests would collide with actual bookmarks + }, + beforeEach: function (assert) { + // setup before each test + }, + afterEach: function (assert) { + // teardown after each test + ext_bookmarks.clear_bookmark_storage(); + connection._init(); + }, + after: function (assert) { + // teardown after module + } +}); + +QUnit.test("parse_uri", function(assert) { + assert.equal(typeof ext_bookmarks.parse_uri(""), "undefined"); + assert.equal(typeof ext_bookmarks.parse_uri("asdf"), "undefined"); + assert.equal(typeof ext_bookmarks.parse_uri("https://localhost:1234/Entity/sada?sadfasd#sdfgdsf"), "undefined"); + + assert.propEqual(ext_bookmarks.parse_uri("safgsa/123&456&789?sadfasdf#_bm_1"), + {bookmarks: ["123", "456", "789"], collection_id: "1"}); +}); + +QUnit.test("get_bookmarks, clear_bookmark_storage", function(assert) { + assert.propEqual(ext_bookmarks.get_bookmarks(), []); + + ext_bookmarks.bookmark_storage[ext_bookmarks.get_key("sdfg")] = "3456" + assert.propEqual(ext_bookmarks.get_bookmarks(), ["3456"]); + + ext_bookmarks.clear_bookmark_storage(); + assert.propEqual(ext_bookmarks.get_bookmarks(), []); +}); + +QUnit.test("get_export_table", async function (assert) { + connection.get = (id) => `<root><Response><File id="${id}" path="testpath_${id.split("/")[1]}"><Version id="abcHead"/></File></Response></root>`; + const TAB = "%09"; + const NEWL = "%0A"; + const context_root = connection.getBasePath() + "Entity/"; + var table = await ext_bookmarks.get_export_table( + ["123@ver1", "456@ver2", "789@ver3", "101112", "@131415"]); + assert.equal(table, + `data:text/csv;charset=utf-8,ID${TAB}Version${TAB}URI${TAB}Path${TAB}Name${TAB}RecordType${NEWL}123${TAB}ver1${TAB}${context_root}123@ver1${TAB}testpath_123@ver1${TAB}${TAB}${NEWL}456${TAB}ver2${TAB}${context_root}456@ver2${TAB}testpath_456@ver2${TAB}${TAB}${NEWL}789${TAB}ver3${TAB}${context_root}789@ver3${TAB}testpath_789@ver3${TAB}${TAB}${NEWL}101112${TAB}abcHead${TAB}${context_root}101112@abcHead${TAB}testpath_101112${TAB}${TAB}${NEWL}${TAB}131415${TAB}${context_root}@131415${TAB}testpath_@131415${TAB}${TAB}`); + +}); + +QUnit.test("update_clear_button", function (assert) { + const clear_button = $(`<div id="caosdb-f-bookmarks-clear"/>`); + $("body").append(clear_button); + + assert.notOk(clear_button.is(".disabled")); + ext_bookmarks.update_clear_button([]); + assert.ok(clear_button.is(".disabled")); + + ext_bookmarks.update_clear_button(["asdf"]); + assert.notOk(clear_button.is(".disabled")); + + ext_bookmarks.update_clear_button(["asdf"]); + assert.notOk(clear_button.is(".disabled")); + + ext_bookmarks.update_clear_button([]); + assert.ok(clear_button.is(".disabled")); + + clear_button.remove(); +}); + +QUnit.test("update_export_link", function (assert) { + const export_link = $(`<div id="caosdb-f-bookmarks-export-link"/>`); + $("body").append(export_link); + + assert.notOk(export_link.is(".disabled")); + ext_bookmarks.update_export_link([]); + assert.ok(export_link.is(".disabled")); + + ext_bookmarks.update_export_link(["asdf"]); + assert.notOk(export_link.is(".disabled")); + + ext_bookmarks.update_export_link(["asdf"]); + assert.notOk(export_link.is(".disabled")); + + ext_bookmarks.update_export_link([]); + assert.ok(export_link.is(".disabled")); + + export_link.remove(); +}); + +QUnit.test("update_collection_link", function (assert) { + const collection_link = $( + `<div id="caosdb-f-bookmarks-collection-link"><a/></div>`); + const a = collection_link.find("a")[0]; + $("body").append(collection_link); + + assert.notOk(collection_link.is(".disabled")); + assert.notOk(a.href); + + ext_bookmarks.update_collection_link([]); + assert.ok(collection_link.is(".disabled")); + assert.notOk(a.href); + + ext_bookmarks.update_collection_link(["asdf"]); + assert.notOk(collection_link.is(".disabled")); + assert.equal(a.href, ext_bookmarks.get_collection_link(["asdf"])); + + ext_bookmarks.update_collection_link(["asdf", "sdfg"]); + assert.notOk(collection_link.is(".disabled")); + assert.equal(a.href, ext_bookmarks.get_collection_link(["asdf", "sdfg"])); + + ext_bookmarks.update_collection_link([]); + assert.ok(collection_link.is(".disabled")); + assert.notOk(a.href); + + collection_link.remove(); +}); + +QUnit.test("bookmark buttons", function (assert) { + const inactive_button = $(`<div data-bmval="id1"/>`); + const active_button = $(`<div class="active" data-bmval="id2"/>`); + const broken_button = $(`<div data-bmval=""/>`); + const non_button = $(`<div data-bla="sadf"/>)`); + const outside_button = $(`<div data-bmval="id3"/>`); + const inside_buttons = $("<div/>").append([inactive_button, active_button, + broken_button, non_button]); + + // get_bookmark_buttons + assert.equal(ext_bookmarks.get_bookmark_buttons("body").length, 0); + + $("body").append([outside_button, inside_buttons]); + + assert.equal(ext_bookmarks.get_bookmark_buttons("body").length, 4, "all but no_button"); + assert.equal(ext_bookmarks.get_bookmark_buttons(inside_buttons).length, 3, "all but non_button and outside_button"); + + // get_value + assert.equal(ext_bookmarks.get_value(inactive_button), "id1"); + assert.notOk(ext_bookmarks.get_value(non_button)); + assert.notOk(ext_bookmarks.get_value(broken_button)); + + // init_button + assert.ok(active_button.is(".active")); + ext_bookmarks.init_button(active_button); + assert.notOk(active_button.is(".active")); + + ext_bookmarks.bookmark_storage[ext_bookmarks.get_key("id1")] = "id1" + assert.notOk(inactive_button.is(".active")); + ext_bookmarks.init_button(inactive_button); + assert.ok(inactive_button.is(".active")); + + ext_bookmarks.clear_bookmark_storage(); + assert.notOk(inactive_button.is(".active"), "clear_bookmark_storage removes active class"); + + inside_buttons.remove(); + outside_button.remove(); +}); diff --git a/test/core/js/modules/ext_bottom_line.js.js b/test/core/js/modules/ext_bottom_line.js.js index 067aade473ba9918f9c1d44eee2b9bb60e8cc863..d4add1a8997b86dbf9f39566aa5f14b0b6721df1 100644 --- a/test/core/js/modules/ext_bottom_line.js.js +++ b/test/core/js/modules/ext_bottom_line.js.js @@ -45,7 +45,7 @@ var ext_bottom_line_test_suite = function ($, ext_bottom_line, QUnit) { }, { "id": "test.success-2", "is_applicable": "(entity) => getParents(entity).map(par => par.name).includes('TestPreviewRecordType') && getEntityName(entity) != 'TestPreviewRecord-fall-back'", - "create": "(entity) => { return plotly_preview.create_plot([{x: [1,2,3,4,5], y: [1,2,4,8,16]}], { 'xaxis': {'title': 'time [samples]'}}); }" + "create": "(entity) => { return plotly_preview.create_plot([{x: [1,2,3,4,5], y: [1,2,4,8,16]}], { 'xaxis': {'title': 'time [samples]'}}, {displaylogo: false}); }" } ] }; @@ -66,8 +66,8 @@ var ext_bottom_line_test_suite = function ($, ext_bottom_line, QUnit) { } }); - QUnit.test("app.creators", function (assert) { - assert.equal(ext_bottom_line.app.creators.length, 7, "seven creators"); + QUnit.test("_creators", function (assert) { + assert.equal(ext_bottom_line._creators.length, 9, "nine creators, 5 default ones, 4 from these tests."); }); QUnit.test("get_container - creation", function(assert) { @@ -120,4 +120,12 @@ var ext_bottom_line_test_suite = function ($, ext_bottom_line, QUnit) { }); + QUnit.test("tiff converter", async function(assert) { + let entity_xml = `<Response><File path="../pics/saturn.tif"/></Response>`; + const entity = (await transformation.transformEntities(str2xml(entity_xml)))[0]; + const tiff_preview = await ext_bottom_line._creators.filter((c) => c.id == "_default_creators.tiff_images")[0].create(entity); + + assert.equal($(tiff_preview).find("img").attr("src").slice(0,21), "data:image/png;base64", "decoded tiff to png"); + }); + }($, ext_bottom_line, QUnit); diff --git a/test/core/js/modules/ext_file_download.js.js b/test/core/js/modules/ext_file_download.js.js new file mode 100644 index 0000000000000000000000000000000000000000..1103f61304c56259cc1a54a94c0c6b6b201d2aa5 --- /dev/null +++ b/test/core/js/modules/ext_file_download.js.js @@ -0,0 +1,65 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2020 Henrik tom Wörden <h.tomwoerden@indiscale.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ + +'use strict'; + +QUnit.module("ext_file_download.js", { + before: function (assert) { + // setup before module + }, + beforeEach: function (assert) { + // setup before each test + }, + afterEach: function (assert) { + // teardown after each test + }, + after: function (assert) { + // teardown after module + } +}); + +QUnit.test("chunk_list ", function(assert) { + const li = [1,2,3,4,5,6,7]; + const res = ext_file_download.chunk_list(li, 3); + assert.equal(res.length, 3, "number of parts"); + assert.propEqual(res[2], [7], "number of parts"); +}); + +QUnit.test("collect_ids ", function (assert) { + const line = id => $(`<tr data-entity-id="${id}"/>`); + const prop_val = x => $(`<div class="caosdb-f-property-value"/>`); + const single_val =x => $(`<div class="caosdb-f-property-single-raw-value caosdb-id">${x}</div>`); + + + const line1 = line("34"); + line1.append([prop_val().append(single_val("5")),prop_val().append(single_val("6"))]) + $("body").append([line1]); + + const res = ext_file_download.collect_ids() + assert.ok(res.indexOf("5") > -1, "missing id"); + assert.ok(res.indexOf("6") > -1, "missing id"); + assert.ok(res.indexOf("34") > -1, "missing id"); + + line1.remove(); + +}); diff --git a/test/core/js/modules/ext_map.js.js b/test/core/js/modules/ext_map.js.js index 9d068ad1c688c7a8643c18846103ea33a10c8874..5afe434e375b40e3f141141b120ed5497939211f 100644 --- a/test/core/js/modules/ext_map.js.js +++ b/test/core/js/modules/ext_map.js.js @@ -23,10 +23,13 @@ 'use strict'; QUnit.module("ext_map.js", { - before: function(assert) { + before: function (assert) { var lat = "latitude"; var lng = "longitude"; - this.datamodel = { lat: lat, lng: lng }; + this.datamodel = { + lat: lat, + lng: lng + }; this.test_map_entity = ` <div class="caosdb-entity-panel caosdb-properties"> <div class="caosdb-id">1234</div> @@ -42,22 +45,22 @@ QUnit.module("ext_map.js", { </div> </div>`; }, - beforeEach: function(assert) { + beforeEach: function (assert) { sessionStorage.removeItem("caosdb_map.view"); } }); -QUnit.test("availability", function(assert) { - assert.equal(caosdb_map.version, "0.3", "test version"); +QUnit.test("availability", function (assert) { + assert.equal(caosdb_map.version, "0.4", "test version"); assert.ok(caosdb_map.init, "init available"); }); -QUnit.test("default config", function(assert) { +QUnit.test("default config", function (assert) { assert.ok(caosdb_map._default_config); assert.equal(caosdb_map._default_config.version, caosdb_map.version, "version"); }); -QUnit.test("load_config", async function(assert) { +QUnit.test("load_config", async function (assert) { assert.ok(caosdb_map.load_config, "available"); var config = await caosdb_map.load_config("non_existing.json"); assert.ok(config, "returns something"); @@ -65,39 +68,43 @@ QUnit.test("load_config", async function(assert) { assert.equal(config.views[0].id, "UNCONFIGURED", "view has id 'UNCONFIGURED'."); }); -QUnit.test("check_config", function(assert) { +QUnit.test("check_config", function (assert) { assert.ok(caosdb_map.check_config(caosdb_map._default_config), "default config ok"); - assert.throws(()=>caosdb_map.check_config({"version": "wrong version",}), /The version of the configuration does not match the version of this implementation of the caosdb_map module. Should be '.*', was 'wrong version'./, "wrong version"); + assert.throws(() => caosdb_map.check_config({ + "version": "wrong version", + }), /The version of the configuration does not match the version of this implementation of the caosdb_map module. Should be '.*', was 'wrong version'./, "wrong version"); }); -QUnit.test("check dependencies", function(assert) { +QUnit.test("check dependencies", function (assert) { assert.ok(caosdb_map.check_dependencies, "available"); - assert.propEqual(caosdb_map.dependencies, ["log", {"L": ["latlngGraticule", "Proj"]}, "navbar", "caosdb_utils"]); + assert.propEqual(caosdb_map.dependencies, ["log", { + "L": ["latlngGraticule", "Proj"] + }, "navbar", "caosdb_utils"]); assert.ok(caosdb_map.check_dependencies(), "deps available"); }); -QUnit.test("create_toggle_map_button", function(assert) { +QUnit.test("create_toggle_map_button", function (assert) { assert.ok(caosdb_map.create_toggle_map_button, "available"); var button = caosdb_map.create_toggle_map_button(); assert.equal(button.tagName, "BUTTON", "is button"); - assert.ok($(button).hasClass("caosdb-f-toggle-map-button"),"has caosdb-f-toggle-map-button class"); + assert.ok($(button).hasClass("caosdb-f-toggle-map-button"), "has caosdb-f-toggle-map-button class"); assert.equal($(button).text(), "Map", "button says 'Map'"); // set other content: button = caosdb_map.create_toggle_map_button("Karte"); assert.equal(button.tagName, "BUTTON", "is button"); - assert.ok($(button).hasClass("caosdb-f-toggle-map-button"),"has caosdb-f-toggle-map-button class"); + assert.ok($(button).hasClass("caosdb-f-toggle-map-button"), "has caosdb-f-toggle-map-button class"); assert.equal($(button).text(), "Karte", "button says 'Karte'"); }); -QUnit.test("bind_toggle_map", function(assert) { +QUnit.test("bind_toggle_map", function (assert) { let button = $("<button/>")[0]; let done = assert.async(); assert.ok(caosdb_map.bind_toggle_map, "available"); - assert.throws(()=>caosdb_map.bind_toggle_map(button), /parameter 'toggle_cb'.* was undefined/, "no function throws"); - assert.throws(()=>caosdb_map.bind_toggle_map("test", ()=>{}), /parameter 'button'.* was string/, "string button throws"); + assert.throws(() => caosdb_map.bind_toggle_map(button), /parameter 'toggle_cb'.* was undefined/, "no function throws"); + assert.throws(() => caosdb_map.bind_toggle_map("test", () => {}), /parameter 'button'.* was string/, "string button throws"); assert.equal(caosdb_map.bind_toggle_map(button, done), button, "call returns button"); // button click calls 'done' @@ -105,12 +112,12 @@ QUnit.test("bind_toggle_map", function(assert) { }); -QUnit.test("create_map", function(assert) { +QUnit.test("create_map", function (assert) { assert.equal(typeof caosdb_map.create_map_view, "function", "function available"); }); -QUnit.test("create_map_panel", function(assert) { +QUnit.test("create_map_panel", function (assert) { assert.ok(caosdb_map.create_map_panel, "available"); let panel = caosdb_map.create_map_panel(); assert.equal(panel.tagName, "DIV", "is div"); @@ -118,9 +125,11 @@ QUnit.test("create_map_panel", function(assert) { assert.ok($(panel).hasClass("container"), "has class container"); }); -QUnit.test("create_map_view", function(assert) { - var view_config = $.extend(true, {}, caosdb_map._unconfigured_views[0], - {"select": true, "view_change": true}); +QUnit.test("create_map_view", function (assert) { + var view_config = $.extend(true, {}, caosdb_map._unconfigured_views[0], { + "select": true, + "view_change": true + }); var map_panel = $("<div/>"); var map = caosdb_map.create_map_view(map_panel[0], view_config); @@ -144,7 +153,7 @@ QUnit.test("create_map_view", function(assert) { map.remove(); // test with special crs: - view_config["crs"] = { + view_config["crs"] = { "code": "EPSG:3995", "proj4def": "+proj=stere +lat_0=90 +lat_ts=71 +lon_0=0 +k=1 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs", "options": { @@ -163,7 +172,7 @@ QUnit.test("create_map_view", function(assert) { }); -QUnit.test("get_map_entities", function(assert) { +QUnit.test("get_map_entities", function (assert) { var datamodel = this.datamodel; var container = $('<div/>').append(this.test_map_entity); var map_objects = caosdb_map.get_map_entities(container[0], datamodel); @@ -171,12 +180,12 @@ QUnit.test("get_map_entities", function(assert) { }); -QUnit.test("create_entitiy_markers", function(assert) { +QUnit.test("create_entity_markers", function (assert) { var datamodel = this.datamodel; var entities = $(this.test_map_entity).toArray(); // w/o popup - var markers = caosdb_map.create_entitiy_markers(entities, datamodel); + var markers = caosdb_map.create_entity_markers(entities, datamodel); assert.equal(markers.length, 1, "has one marker"); assert.ok(markers[0] instanceof L.Marker, "is marker"); var latlng = markers[0]._latlng; @@ -185,30 +194,32 @@ QUnit.test("create_entitiy_markers", function(assert) { assert.notOk(markers[0].getPopup(), "no popup"); // with popup - var markers = caosdb_map.create_entitiy_markers(entities, datamodel, ()=>"popup"); + var markers = caosdb_map.create_entity_markers(entities, datamodel, () => "popup"); assert.ok(markers[0].getPopup(), "has popup"); }); -QUnit.test("_add_current_page_entities", function(assert) { +QUnit.test("_add_current_page_entities", async function (assert) { var datamodel = this.datamodel; var layerGroup = L.layerGroup(); var container = $('<div class="caosdb-f-main-entities"/>').append(this.test_map_entity); $("body").append(container); assert.equal(layerGroup.getLayers().length, 0, "no layer"); - var cpe = caosdb_map._get_current_page_entities(datamodel, undefined, undefined, undefined, undefined); + var cpe = await caosdb_map._generic_get_current_page_entities(datamodel, undefined, undefined, undefined, undefined, undefined); assert.equal(cpe.length, 1, "has one entity"); container.remove(); }); -QUnit.test("make_layer_chooser_html", function(assert) { - var test_conf = { "id": "test_id", +QUnit.test("make_layer_chooser_html", function (assert) { + var test_conf = { + "id": "test_id", "name": "test name", "description": "test description", - "icon": { "html": "<span>ICON</span>", + "icon": { + "html": "<span>ICON</span>", }, }; @@ -217,19 +228,139 @@ QUnit.test("make_layer_chooser_html", function(assert) { assert.equal($(layer_chooser).attr("title"), "test description", "description set as title"); }); -QUnit.test("init_entity_layer", function(assert) { - var done = assert.async(); - var test_conf = { "id": "test_id", +QUnit.test("_init_single_entity_layer", function (assert) { + var test_conf = { + "id": "test_id", "name": "test name", "description": "test description", - "get_entities": async function() {done(); return []}, - "icon": { "html": "<span>ICON</span>", + "icon": { + "html": "<span>ICON</span>", }, } - var entityLayer= caosdb_map.init_entity_layer(test_conf); + var entityLayer = caosdb_map._init_single_entity_layer(test_conf); assert.equal(entityLayer.id, test_conf.id, "id"); assert.equal(entityLayer.active, true, "is active"); assert.ok(entityLayer.chooser_html instanceof HTMLElement, "chooser_html is HTMLElement"); - assert.equal(entityLayer.layer_group.getLayers().length, 0 , "empty layergroup"); + assert.equal(entityLayer.layer_group.getLayers().length, 0, "empty layergroup"); +}); + +QUnit.test("_get_with_POV ", function (assert) { + assert.equal(caosdb_map._get_with_POV( + []), "", "no POV"); + assert.equal(caosdb_map._get_with_POV( + ["lol"]), " WITH lol ", "single POV"); + assert.equal(caosdb_map._get_with_POV( + ["lol", "hi"]), " WITH lol WITH hi ", "with two POV"); +}); + + +QUnit.test("_get_select_with_path ", function (assert) { + assert.throws(() => caosdb_map._get_select_with_path(), /Supply the datamodel./, "missing datamodel"); + assert.throws(() => caosdb_map._get_select_with_path(this.datamodel, []), /Supply at least a RecordType./, "missing value"); + assert.equal(caosdb_map._get_select_with_path( + this.datamodel, + ["RealRT"]), "SELECT parent,latitude,longitude FROM ENTITY RealRT WITH latitude AND longitude ", "RT only"); + assert.equal(caosdb_map._get_select_with_path( + this.datamodel, + ["RealRT", "prop1"]), "SELECT parent,prop1.latitude,prop1.longitude FROM ENTITY RealRT WITH prop1 WITH latitude AND longitude ", "RT with one prop"); + assert.equal(caosdb_map._get_select_with_path( + this.datamodel, + ["RealRT", "prop1", "prop2"]), "SELECT parent,prop1.prop2.latitude,prop1.prop2.longitude FROM ENTITY RealRT WITH prop1 WITH prop2 WITH latitude AND longitude ", "RT with two props"); +}); + + +QUnit.test("_get_leaf_prop", async function (assert) { + const test_response = str2xml(` +<Response srid="851d063d-121b-4b67-98d4-6e5ad4d31e72" timestamp="1606214042274" baseuri="https://localhost:10443" count="8"> + <Query string="select Campaign.responsible.firstname from icecore" results="8"> + <ParseTree>(cq select (prop_sel (prop_subsel (selector_txt C a m p a i g n) . (prop_subsel (selector_txt r e s p o n s i b l e) . (prop_subsel (selector_txt f i r s t n a m e ))))) from (entity icecore) <EOF>)</ParseTree> + <Role/> + <Entity>icecore</Entity> + <Selection> + <Selector name="Campaign.responsible.firstname"/> + </Selection> + </Query> + <Record id="6525" name="Test_IceCore_1"> + <Permissions/> + <Property datatype="Campaign" id="6430" name="Campaign"> + <Record id="6516" name="Test-2020_Camp1"> + <Permissions/> + <Property datatype="REFERENCE" id="168" name="responsible"> + <Record id="6515" name="Test_Scientist"> + <Permissions/> + <Property datatype="DOUBLE" id="151" name="latitude" importance="FIX"> + 1.34 + <Permissions/> + </Property> + <Property datatype="DOUBLE" id="151" name="longitude" importance="FIX"> + 2 + <Permissions/> + </Property> + </Record> + <Permissions/> + </Property> + </Record> + <Permissions/> + </Property> + </Record> + <Record id="6526" name="Test_IceCore_2"> + <Permissions/> + <Property datatype="Campaign" id="6430" name="Campaign"> + <Record id="6516" name="Test-2020_Camp1"> + <Permissions/> + <Property datatype="REFERENCE" id="168" name="responsible"> + <Record id="6515" name="Test_Scientist"> + <Permissions/> + <Property datatype="DOUBLE" id="151" name="latitude" importance="FIX"> + 3 + <Permissions/> + </Property> + <Property datatype="DOUBLE" id="151" name="longitude" importance="FIX"> + 4.8345 + <Permissions/> + </Property> + </Record> + <Permissions/> + </Property> + </Record> + <Permissions/> + </Property> + </Record> +</Response>`); + var leaves = caosdb_map._get_leaf_prop(test_response, 2, this.datamodel) + + assert.equal(Object.keys(leaves).length, 2, "number of records"); + assert.notEqual(typeof leaves["6525"], "undefined", "has entity id"); + assert.deepEqual(leaves["6525"], ["1.34", "2"]); + assert.deepEqual(leaves["6526"], ["3", "4.8345"], "long/lat in second rec"); + + assert.equal( + caosdb_map._get_toplvl_rec_with_id(test_response, "6526")["id"], + "6526", + "number of records"); + + caosdb_map._set_subprops_at_top( + test_response, 2, this.datamodel, { + "6526": [1.234, 5.67] + }) + assert.equal($(test_response).find(`[name='longitude']`).length, + 4, + "number lng props"); + assert.equal($(test_response).find(`[name='latitude']`).length, + 4, + "number lat props"); + // after transforming, the long/lat props should be accessible + var html_ents = await transformation.transformEntities(test_response); + assert.equal( + getProperty(html_ents[0], "longitude"), + "2", + "longitude of first rec"); + +}); + +QUnit.test("_get_id_POV", function (assert) { + assert.equal(caosdb_map._get_id_POV([]), "WITH ", "no POV"); + assert.equal(caosdb_map._get_id_POV([5]), "WITH id=5", "one id"); + assert.equal(caosdb_map._get_id_POV([5, 6]), "WITH id=5 or id=6", "two ids"); }); diff --git a/test/core/js/modules/ext_xls_download.js.js b/test/core/js/modules/ext_xls_download.js.js index 4f9dc6c59b156f1f2265acb4b315887536667194..5426580436933b942cec6750b521747ab62f2d00 100644 --- a/test/core/js/modules/ext_xls_download.js.js +++ b/test/core/js/modules/ext_xls_download.js.js @@ -79,16 +79,14 @@ QUnit.module("ext_xls_download", { return str2xml('<response><script code="0" /><stdout>bla</stdout></response>'); } - caosdb_table_export._go_to_script_results = function(xls_link, filename) { - xls_link.setAttribute( - "href", - location.protocol + "//" +location.host + "/Shared/" + filename); + caosdb_table_export.go_to_script_results = function(filename) { assert.equal(filename, "bla", "filename correct"); done(); } var tsv_data = $('<a id="caosdb-f-query-select-data-tsv" />'); - $(document.body).append(tsv_data); + var modal = $('<div id="downloadModal"><div>'); + $(document.body).append([tsv_data, modal]); var xsl_link = $("<a/>"); @@ -96,12 +94,15 @@ QUnit.module("ext_xls_download", { await sleep(500); - assert.ok(xsl_link.attr("href").endsWith("Shared/bla"), xsl_link.attr("href") + " ends with Shared/bla"); - tsv_data.remove(); + modal.remove(); }); } +QUnit.test("_clean_cell", function(assert) { + assert.equal(caosdb_table_export._clean_cell("\n\t\n\t"), " ", "No valid content"); +}); + QUnit.test("_get_property_value", function(assert) { var f = caosdb_table_export._get_property_value; @@ -114,11 +115,17 @@ QUnit.test("_get_property_value", function(assert) { QUnit.test("_get_tsv_string", function(assert) { const table = this.test_case_1; const entities = $(table).find("tbody tr").toArray(); - assert.equal(entities.length, 2, "two example entities"); + assert.equal(entities.length, 3, "three example entities"); - var f = caosdb_table_export._get_tsv_string + var f = caosdb_table_export._create_tsv_string var tsv_string = f(entities, ["Bag", "Number"], true); - assert.equal(tsv_string, "data:text/csv;charset=utf-8,ID%09Bag%09Number%0A242%096366, 6406, 6407, 6408, 6409, 6410, 6411, 6412, 6413%090284%0A2112%09%091101", "tsv generated"); + var prefix = "data:text/csv;charset=utf-8," + assert.equal(tsv_string, + "ID\tVersion\tBag\tNumber\n242\tabc123\t6366, 6406, 6407, 6408, 6409, 6410, 6411, 6412, 6413\t02 8 4aaa a\n2112\tabc124\t\t1101\n2112\tabc125\t\t1102", "tsv generated"); + tsv_string = caosdb_table_export._encode_tsv_string(tsv_string); + assert.equal(tsv_string.slice(0,prefix.length), prefix); + assert.equal(decodeURIComponent(tsv_string.slice(prefix.length, tsv_string.length)), + "ID\tVersion\tBag\tNumber\n242\tabc123\t6366, 6406, 6407, 6408, 6409, 6410, 6411, 6412, 6413\t02 8 4aaa a\n2112\tabc124\t\t1101\n2112\tabc125\t\t1102", "tsv generated"); }); QUnit.test("_get_property_value", function (assert) { diff --git a/test/core/js/modules/form_elements.js.js b/test/core/js/modules/form_elements.js.js index 7f5930cd4253f408edf785045db10290697cd6c3..f8bf1ed1ac1495a8a3688aeb7b0cce387c8b69fb 100644 --- a/test/core/js/modules/form_elements.js.js +++ b/test/core/js/modules/form_elements.js.js @@ -541,3 +541,91 @@ QUnit.test("field_ready", function(assert) { }); }); +{ +const sleep = (ms) => { + return new Promise(res => setTimeout(res, ms)) +} + +QUnit.test("make_alert - cancel", async function(assert) { + var cancel_callback = assert.async() + var _alert = form_elements.make_alert({ + message: "message", + proceed_callback: () => {assert.ok(false, "this should not be called");}, + cancel_callback: cancel_callback, + }); + $("body").append(_alert); + + assert.equal($("body").find(".caosdb-f-form-elements-alert").length, 1, "alert is there"); + $(_alert).find("button.caosdb-f-btn-alert-cancel")[0].click(); + await sleep(500); + + assert.equal($("body").find(".caosdb-f-form-elements-alert").length, 0, "has been removed"); + + +}); + +QUnit.test("make_alert - proceed", async function(assert) { + var proceed_callback = assert.async(); + var _alert = form_elements.make_alert({ + message: "message", + proceed_callback: proceed_callback, + }); + assert.equal($(_alert).find("[type='checkbox']").length, 0, "no remember checkbox"); + + $("body").append(_alert); + assert.equal($("body").find(".caosdb-f-form-elements-alert").length, 1, "alert is there"); + $(_alert).find("button.caosdb-f-btn-alert-proceed")[0].click(); + await sleep(500); + + assert.equal($("body").find(".caosdb-f-form-elements-alert").length, 0, "has been removed"); + +}); + +QUnit.test("make_alert - remember", async function(assert) { + form_elements._set_alert_decision("unittests", ""); + + var proceed_callback = assert.async(3); + var _alert = form_elements.make_alert({ + message: "message", + proceed_callback: proceed_callback, + remember_my_decision_id: "unittests", + }); + assert.equal($(_alert).find("[type='checkbox']").length, 1, "has remember checkbox"); + + // append for the first time + $("body").append(_alert); + assert.equal($("body").find(".caosdb-f-form-elements-alert").length, 1, "alert is there"); + + // click proceed without "don't ask me again". + $(_alert).find("button.caosdb-f-btn-alert-proceed")[0].click(); + await sleep(500); + + form_elements._set_alert_decision("unittests", ""); + + assert.equal($("body").find(".caosdb-f-form-elements-alert").length, 0, "has been removed"); + + // append 2nd time + _alert = form_elements.make_alert({ + message: "message", + proceed_callback: proceed_callback, + remember_my_decision_id: "unittests", + }); + assert.equal($(_alert).find("[type='checkbox']").length, 1, "has remember checkbox"); + $("body").append(_alert); + assert.equal($("body").find(".caosdb-f-form-elements-alert").length, 1, "alert is there"); + $(_alert).find("[type='checkbox']").prop("checked", true); + + $(_alert).find("button.caosdb-f-btn-alert-proceed")[0].click(); + await sleep(500); + form_elements._set_alert_decision("unittests", "proceed"); + + // try 3rd time + _alert = form_elements.make_alert({ + message: "message", + proceed_callback: proceed_callback, + remember_my_decision_id: "unittests", + }); + assert.equal(typeof _alert, "undefined", "alert was not created, proceed callback was called third time"); +}); + +} diff --git a/test/core/js/modules/webcaosdb.js.js b/test/core/js/modules/webcaosdb.js.js index 239758161b8419a8a5e15799c67c909977f1fbf0..7b3f7abf404f261668c18690df337b73794dd8bd 100644 --- a/test/core/js/modules/webcaosdb.js.js +++ b/test/core/js/modules/webcaosdb.js.js @@ -324,7 +324,7 @@ QUnit.test("createUpdateForm", function(assert) { QUnit.test("createUpdateEntityHeading", function(assert) { let cueh = transaction.update.createUpdateEntityHeading; assert.ok(cueh, "function available"); - let eh = $('<div class="panel-heading"><div class="1strow"/><div class="2ndrow"/></div>')[0]; + let eh = $('<div class="panel-heading"><div class="1strow"></div><div class="2ndrow"></div></div>')[0]; assert.equal($(eh).children('.1strow').length, 1, "eh has 1st row"); assert.equal($(eh).children('.2ndrow').length, 1, "eh has 2nd row"); let uh = cueh(eh); @@ -957,10 +957,6 @@ QUnit.test("createCarouselNav", function(assert) { let original_get = connection.get; ref_property_elem.find('div').append(refLinks); - const sleep = function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); - } - QUnit.test("initProperty", async function(assert) { var done = assert.async(2); assert.ok(preview.initProperty, "function available"); @@ -1939,3 +1935,80 @@ QUnit.test("toolbox example", function(assert) { assert.equal($('.caosdb-f-navbar-toolbox[data-toolbox-name="Tools"]').length, 1, "one 'Tools' toolbox"); assert.equal($('.caosdb-f-navbar-toolbox[data-toolbox-name="Tools"] button').length, 3, "three 'Tools' buttons"); }); + +QUnit.module("webcaosdb.js - version_history", { + before: function(assert) { + connection._init(); + }, + after: function(assert) { + connection._init(); + }, +}); + +QUnit.test("available", function (assert) { + assert.equal(typeof version_history.init, "function"); + assert.equal(typeof version_history.get_history_tsv, "function"); + assert.equal(typeof version_history.init_export_history_buttons, "function"); + assert.equal(typeof version_history.init_load_history_buttons, "function"); + assert.equal(typeof version_history.retrieve_history, "function"); +}) + +QUnit.test("init_load_history_buttons and init_load_history_buttons", async function (assert) { + var xml_str = `<Response username="user1" realm="Realm1" srid="bc2f8f6b-71d6-49ca-890c-eebea3e38e18" timestamp="1606253365632" baseuri="https://localhost:10443" count="1"> + <UserInfo username="user1" realm="Realm1"> + <Roles> + <Role>role1</Role> + </Roles> + </UserInfo> + <Record id="8610" name="TestRecord1-6thVersion" description="This is the 6th version."> + <Permissions> + <Permission name="RETRIEVE:HISTORY" /> + </Permissions> + <Version id="efa5ac7126c722b3f43284e150d070d6deac0ba6"> + <Predecessor id="f09114b227d88f23d4e23645ae471d688b1e82f7" /> + <Successor id="5759d2bccec3662424db5bb005acea4456a299ef" /> + </Version> + <Parent id="8609" name="TestRT" /> + </Record> +</Response> +`; + var done = assert.async(2); + var xml = str2xml(xml_str); + version_history._get = async function (entity) { + assert.equal(entity, "Entity/8610@efa5ac7126c722b3f43284e150d070d6deac0ba6?H"); + done(); + $(xml).find("Version").attr("completeHistory", "true"); + return xml; + } + var html = await transformation.transformEntities(xml); + var load_button = $(html).find(".caosdb-f-entity-version-load-history-btn"); + $("body").append(html); + + assert.notOk(load_button.is(":visible"), "load_button hidden"); + load_button.click(); // nothing happens + + version_history.init_load_history_buttons(); + assert.ok(load_button.is(":visible"), "load_button is not hidden anymore"); + + // load_button triggers retrieval of history + load_button.click(); + await sleep(200); + + var gone_button = $(html).find(".caosdb-f-entity-version-load-history-btn"); + assert.equal(gone_button.length, 0, "button is gone"); + + export_button = $(html).find(".caosdb-f-entity-version-export-history-btn"); + assert.ok(export_button.is(":visible"), "export_button is visible"); + + version_history._download_tsv = function (tsv) { + assert.equal(tsv.indexOf("data:text/csv;charset=utf-8,Entity ID%09"), 0); + done(); + } + export_button.click(); + + $(html).remove(); +}); + +const sleep = function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} diff --git a/test/core/pics/saturn.tif b/test/core/pics/saturn.tif new file mode 100644 index 0000000000000000000000000000000000000000..f226ebab7fca3c9caa306f78d80eaa7b4068f994 Binary files /dev/null and b/test/core/pics/saturn.tif differ diff --git a/test/core/xml/table_export/test_case_select_table_1.xml b/test/core/xml/table_export/test_case_select_table_1.xml index 12f26180b89bdf3f2ba3280d4546783f54bfa8ea..be07fd42df8bc1275ab16802c0e82c19ece417ad 100644 --- a/test/core/xml/table_export/test_case_select_table_1.xml +++ b/test/core/xml/table_export/test_case_select_table_1.xml @@ -9,8 +9,9 @@ </Selection> </Query> <Record id="242"> + <Version id="abc123" head="true"/> <Property id="117" name="Number" datatype="TEXT" importance="FIX"> - 0284 + 02	8


4aaa	a </Property> <Property id="104" name="Bag" datatype="Bag" importance="FIX"> 6366 @@ -35,10 +36,19 @@ </Property> </Record> <Record id="2112"> + <Version id="abc124" head="true"/> <Property id="117" name="Number" datatype="TEXT" importance="FIX"> 1101 </Property> <Property id="104" name="Bag" datatype="LIST<Bag>" importance="FIX"> </Property> </Record> + <Record id="2112"> + <Version id="abc125" head="true"/> + <Property id="117" name="Number" datatype="TEXT" importance="FIX"> + 1102 + </Property> + <Property id="104" name="Bag" datatype="LIST<Bag>" importance="FIX"> + </Property> + </Record> </Response> diff --git a/test/docker/Dockerfile b/test/docker/Dockerfile index a19843fb3de11bbedf1c01f71619470bcc99f75c..026887097ac3b7dc13e6e429bf73c363e3adcbf3 100644 --- a/test/docker/Dockerfile +++ b/test/docker/Dockerfile @@ -1,7 +1,22 @@ -FROM debian:latest -RUN apt-get update && \ - apt-get install firefox-esr gettext-base pylint3 python3-pip \ - python3-httpbin git curl x11-apps xvfb unzip -y -RUN git clone -b dev https://gitlab.com/caosdb/caosdb-pylib.git && \ - cd caosdb-pylib && pip3 install . +FROM debian:10 +ADD node_gpg.asc /etc/apt/ +RUN apt-get update \ + && apt-get install -y gnupg ca-certificates\ + && apt-key add /etc/apt/node_gpg.asc \ + && echo "deb http://deb.debian.org/debian buster-backports main" >> /etc/apt/sources.list \ + && echo "deb https://deb.nodesource.com/node_14.x buster main" >> /etc/apt/sources.list \ + && apt-get update \ + && apt-get install -y \ + firefox-esr gettext-base python3-pip \ + python3-httpbin git curl x11-apps xvfb unzip \ + nodejs # Don't install `npm` (Debian), it conflicts with the `nodejs` (Node) package \ + && apt-get install -f +RUN pip3 install pylint pytest +RUN pip3 install caosdb +RUN pip3 install pandas xlrd==1.2.0 +RUN pip3 install git+https://gitlab.com/caosdb/caosdb-advanced-user-tools.git@dev +# For automatic documentation +#RUN npm install -g jsdoc +#RUN npm install -g jsdoc-sphinx +RUN pip3 install sphinx-js sphinx-autoapi recommonmark sphinx-rtd-theme diff --git a/test/docker/node_gpg.asc b/test/docker/node_gpg.asc new file mode 100644 index 0000000000000000000000000000000000000000..ed458a24e9132e0f22b6081d5e2b68926d00c725 --- /dev/null +++ b/test/docker/node_gpg.asc @@ -0,0 +1,54 @@ +Downloaded on 2020-12-18 + +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1 +Comment: GPGTools - https://gpgtools.org + +mQINBFObJLYBEADkFW8HMjsoYRJQ4nCYC/6Eh0yLWHWfCh+/9ZSIj4w/pOe2V6V+ +W6DHY3kK3a+2bxrax9EqKe7uxkSKf95gfns+I9+R+RJfRpb1qvljURr54y35IZgs +fMG22Np+TmM2RLgdFCZa18h0+RbH9i0b+ZrB9XPZmLb/h9ou7SowGqQ3wwOtT3Vy +qmif0A2GCcjFTqWW6TXaY8eZJ9BCEqW3k/0Cjw7K/mSy/utxYiUIvZNKgaG/P8U7 +89QyvxeRxAf93YFAVzMXhoKxu12IuH4VnSwAfb8gQyxKRyiGOUwk0YoBPpqRnMmD +Dl7SdmY3oQHEJzBelTMjTM8AjbB9mWoPBX5G8t4u47/FZ6PgdfmRg9hsKXhkLJc7 +C1btblOHNgDx19fzASWX+xOjZiKpP6MkEEzq1bilUFul6RDtxkTWsTa5TGixgCB/ +G2fK8I9JL/yQhDc6OGY9mjPOxMb5PgUlT8ox3v8wt25erWj9z30QoEBwfSg4tzLc +Jq6N/iepQemNfo6Is+TG+JzI6vhXjlsBm/Xmz0ZiFPPObAH/vGCY5I6886vXQ7ft +qWHYHT8jz/R4tigMGC+tvZ/kcmYBsLCCI5uSEP6JJRQQhHrCvOX0UaytItfsQfLm +EYRd2F72o1yGh3yvWWfDIBXRmaBuIGXGpajC0JyBGSOWb9UxMNZY/2LJEwARAQAB +tB9Ob2RlU291cmNlIDxncGdAbm9kZXNvdXJjZS5jb20+iQI4BBMBAgAiBQJTmyS2 +AhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRAWVaCraFdigHTmD/9OKhUy +jJ+h8gMRg6ri5EQxOExccSRU0i7UHktecSs0DVC4lZG9AOzBe+Q36cym5Z1di6JQ +kHl69q3zBdV3KTW+H1pdmnZlebYGz8paG9iQ/wS9gpnSeEyx0Enyi167Bzm0O4A1 +GK0prkLnz/yROHHEfHjsTgMvFwAnf9uaxwWgE1d1RitIWgJpAnp1DZ5O0uVlsPPm +XAhuBJ32mU8S5BezPTuJJICwBlLYECGb1Y65Cil4OALU7T7sbUqfLCuaRKxuPtcU +VnJ6/qiyPygvKZWhV6Od0Yxlyed1kftMJyYoL8kPHfeHJ+vIyt0s7cropfiwXoka +1iJB5nKyt/eqMnPQ9aRpqkm9ABS/r7AauMA/9RALudQRHBdWIzfIg0Mlqb52yyTI +IgQJHNGNX1T3z1XgZhI+Vi8SLFFSh8x9FeUZC6YJu0VXXj5iz+eZmk/nYjUt4Mtc +pVsVYIB7oIDIbImODm8ggsgrIzqxOzQVP1zsCGek5U6QFc9GYrQ+Wv3/fG8hfkDn +xXLww0OGaEQxfodm8cLFZ5b8JaG3+Yxfe7JkNclwvRimvlAjqIiW5OK0vvfHco+Y +gANhQrlMnTx//IdZssaxvYytSHpPZTYw+qPEjbBJOLpoLrz8ZafN1uekpAqQjffI +AOqW9SdIzq/kSHgl0bzWbPJPw86XzzftewjKNbkCDQRTmyS2ARAAxSSdQi+WpPQZ +fOflkx9sYJa0cWzLl2w++FQnZ1Pn5F09D/kPMNh4qOsyvXWlekaV/SseDZtVziHJ +Km6V8TBG3flmFlC3DWQfNNFwn5+pWSB8WHG4bTA5RyYEEYfpbekMtdoWW/Ro8Kmh +41nuxZDSuBJhDeFIp0ccnN2Lp1o6XfIeDYPegyEPSSZqrudfqLrSZhStDlJgXjea +JjW6UP6txPtYaaila9/Hn6vF87AQ5bR2dEWB/xRJzgNwRiax7KSU0xca6xAuf+TD +xCjZ5pp2JwdCjquXLTmUnbIZ9LGV54UZ/MeiG8yVu6pxbiGnXo4Ekbk6xgi1ewLi +vGmz4QRfVklV0dba3Zj0fRozfZ22qUHxCfDM7ad0eBXMFmHiN8hg3IUHTO+UdlX/ +aH3gADFAvSVDv0v8t6dGc6XE9Dr7mGEFnQMHO4zhM1HaS2Nh0TiL2tFLttLbfG5o +QlxCfXX9/nasj3K9qnlEg9G3+4T7lpdPmZRRe1O8cHCI5imVg6cLIiBLPO16e0fK +yHIgYswLdrJFfaHNYM/SWJxHpX795zn+iCwyvZSlLfH9mlegOeVmj9cyhN/VOmS3 +QRhlYXoA2z7WZTNoC6iAIlyIpMTcZr+ntaGVtFOLS6fwdBqDXjmSQu66mDKwU5Ek +fNlbyrpzZMyFCDWEYo4AIR/18aGZBYUAEQEAAYkCHwQYAQIACQUCU5sktgIbDAAK +CRAWVaCraFdigIPQEACcYh8rR19wMZZ/hgYv5so6Y1HcJNARuzmffQKozS/rxqec +0xM3wceL1AIMuGhlXFeGd0wRv/RVzeZjnTGwhN1DnCDy1I66hUTgehONsfVanuP1 +PZKoL38EAxsMzdYgkYH6T9a4wJH/IPt+uuFTFFy3o8TKMvKaJk98+Jsp2X/QuNxh +qpcIGaVbtQ1bn7m+k5Qe/fz+bFuUeXPivafLLlGc6KbdgMvSW9EVMO7yBy/2JE15 +ZJgl7lXKLQ31VQPAHT3an5IV2C/ie12eEqZWlnCiHV/wT+zhOkSpWdrheWfBT+ac +hR4jDH80AS3F8jo3byQATJb3RoCYUCVc3u1ouhNZa5yLgYZ/iZkpk5gKjxHPudFb +DdWjbGflN9k17VCf4Z9yAb9QMqHzHwIGXrb7ryFcuROMCLLVUp07PrTrRxnO9A/4 +xxECi0l/BzNxeU1gK88hEaNjIfviPR/h6Gq6KOcNKZ8rVFdwFpjbvwHMQBWhrqfu +G3KaePvbnObKHXpfIKoAM7X2qfO+IFnLGTPyhFTcrl6vZBTMZTfZiC1XDQLuGUnd +sckuXINIU3DFWzZGr0QrqkuE/jyr7FXeUJj9B7cLo+s/TXo+RaVfi3kOc9BoxIvy +/qiNGs/TKy2/Ujqp/affmIMoMXSozKmga81JSwkADO1JMgUy6dApXz9kP4EE3g== +=CLGF +-----END PGP PUBLIC KEY BLOCK----- diff --git a/test/server_side_scripting/ext_table_preview/data/bad.csv b/test/server_side_scripting/ext_table_preview/data/bad.csv new file mode 100644 index 0000000000000000000000000000000000000000..d29a9312a387186beb8bf4f77a8ec0e4b0ab80fa Binary files /dev/null and b/test/server_side_scripting/ext_table_preview/data/bad.csv differ diff --git a/test/server_side_scripting/ext_table_preview/data/bad.tsv b/test/server_side_scripting/ext_table_preview/data/bad.tsv new file mode 100644 index 0000000000000000000000000000000000000000..d29a9312a387186beb8bf4f77a8ec0e4b0ab80fa Binary files /dev/null and b/test/server_side_scripting/ext_table_preview/data/bad.tsv differ diff --git a/test/server_side_scripting/ext_table_preview/data/bad.xls b/test/server_side_scripting/ext_table_preview/data/bad.xls new file mode 100644 index 0000000000000000000000000000000000000000..1f31bf2754258e3d07f88fd1e6bdee4d7b11bee1 Binary files /dev/null and b/test/server_side_scripting/ext_table_preview/data/bad.xls differ diff --git a/test/server_side_scripting/ext_table_preview/data/bad.xlsx b/test/server_side_scripting/ext_table_preview/data/bad.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..1f31bf2754258e3d07f88fd1e6bdee4d7b11bee1 Binary files /dev/null and b/test/server_side_scripting/ext_table_preview/data/bad.xlsx differ diff --git a/test/server_side_scripting/ext_table_preview/data/server_error.csv b/test/server_side_scripting/ext_table_preview/data/server_error.csv new file mode 100644 index 0000000000000000000000000000000000000000..3e770df012f65d73ce4721a5f65d7e3f39959519 --- /dev/null +++ b/test/server_side_scripting/ext_table_preview/data/server_error.csv @@ -0,0 +1 @@ +Hi, this line contains a unicode backspace. This causes a server error, when pandas_table_preview.py's output is serialized into XML. \ No newline at end of file diff --git a/test/server_side_scripting/ext_table_preview/data/test.csv b/test/server_side_scripting/ext_table_preview/data/test.csv new file mode 100644 index 0000000000000000000000000000000000000000..7c9bfd1354393439f551021cfe340577433ce2aa --- /dev/null +++ b/test/server_side_scripting/ext_table_preview/data/test.csv @@ -0,0 +1,12 @@ +# test header +# two lines +A1,B1,C1,D1,E1,F1,G1,H1,I1,J1,K1,L1,M1,N1,O1,P1,Q1,R1,S1,T1 +A2,B2,C2,D2,E2,F2,G2,H2,I2,J2,K2,L2,M2,N2,O2,P2,Q2,R2,S2,T2 +A3,B3,csvfile,D3,E3,F3,G3,H3,I3,J3,K3,L3,M3,N3,O3,P3,Q3,R3,S3,T3 +A5,B5,C5,D5,E5,F5,G5,H5,I5,J5,K5,L5,M5,N5,O5,P5,Q5,R5,S5,T5 +A6,B6,C6,D6,E6,F6,G6,H6,I6,J6,K6,L6,M6,N6,O6,P6,Q6,R6,S6,T6 +A7,B7,csvfile,D7,E7,F7,G7,H7,I7,J7,K7,L7,M7,N7,O7,P7,Q7,R7,S7,T7 +A8,B8,C8,D8,E8,F8,G8,H8,I8,J8,K8,L8,M8,N8,O8,P8,Q8,R8,S8,T8 +A9,B9,C9,D9,E9,F9,G9,H9,I9,J9,K9,L9,M9,N9,O9,P9,Q9,R9,S9,T9 +A10,B10,C10,D10,E10,F10,G10,H10,I10,J10,K10,L10,M10,N10,O10,P10,Q10,R10,S10,T10 +A11,B11,C11,D11,E11,F11,G11,H11,I11,J11,K11,L11,M11,N11,O11,P11,Q11,R11,S11,T11 diff --git a/test/server_side_scripting/ext_table_preview/data/test.tsv b/test/server_side_scripting/ext_table_preview/data/test.tsv new file mode 100644 index 0000000000000000000000000000000000000000..863f692bf64e7dcabf74703587a93e37adf27e67 --- /dev/null +++ b/test/server_side_scripting/ext_table_preview/data/test.tsv @@ -0,0 +1,12 @@ +# test header +# two lines +A1 B1 C1 D1 E1 F1 G1 H1 I1 J1 K1 L1 M1 N1 O1 P1 Q1 R1 S1 T1 +A2 B2 C2 D2 E2 F2 G2 H2 I2 J2 K2 L2 M2 N2 O2 P2 Q2 R2 S2 T2 +A3 B3 csvfile D3 E3 F3 G3 H3 I3 J3 K3 L3 M3 N3 O3 P3 Q3 R3 S3 T3 +A5 B5 C5 D5 E5 F5 G5 H5 I5 J5 K5 L5 M5 N5 O5 P5 Q5 R5 S5 T5 +A6 B6 C6 D6 E6 F6 G6 H6 I6 J6 K6 L6 M6 N6 O6 P6 Q6 R6 S6 T6 +A7 B7 tsvfile D7 E7 F7 G7 H7 I7 J7 K7 L7 M7 N7 O7 P7 Q7 R7 S7 T7 +A8 B8 C8 D8 E8 F8 G8 H8 I8 J8 K8 L8 M8 N8 O8 P8 Q8 R8 S8 T8 +A9 B9 C9 D9 E9 F9 G9 H9 I9 J9 K9 L9 M9 N9 O9 P9 Q9 R9 S9 T9 +A10 B10 C10 D10 E10 F10 G10 H10 I10 J10 K10 L10 M10 N10 O10 P10 Q10 R10 S10 T10 +A11 B11 C11 D11 E11 F11 G11 H11 I11 J11 K11 L11 M11 N11 O11 P11 Q11 R11 S11 T11 diff --git a/test/server_side_scripting/ext_table_preview/data/test.xls b/test/server_side_scripting/ext_table_preview/data/test.xls new file mode 100644 index 0000000000000000000000000000000000000000..a355756b9ab72f9035246c5303800a2076d9bfc0 Binary files /dev/null and b/test/server_side_scripting/ext_table_preview/data/test.xls differ diff --git a/test/server_side_scripting/ext_table_preview/data/test.xlsx b/test/server_side_scripting/ext_table_preview/data/test.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..bc291f1aa86cd6d550320f07a7ce69cf813b8116 Binary files /dev/null and b/test/server_side_scripting/ext_table_preview/data/test.xlsx differ diff --git a/test/server_side_scripting/ext_table_preview/data/xss_attack.csv b/test/server_side_scripting/ext_table_preview/data/xss_attack.csv new file mode 100644 index 0000000000000000000000000000000000000000..e7d43505aef42c397f1859805bc87aab8b6da1a2 --- /dev/null +++ b/test/server_side_scripting/ext_table_preview/data/xss_attack.csv @@ -0,0 +1,8 @@ +# as it seems all these characters are escaped correctly. +"","%3C","<","<","<","<","<","<","<","<" +"<","<","<","<","<","<","<","<","<","<" +"<","<","<","<","<","<","<","<","<","<" +"<","<","<","<","<","<","<","<","<","<" +"<","<","<","<","<","<","<","<","<","<" +"<","<","<","<","<","<","<","<","<","<" +"<","<","<","<","<","<","\x3c","\x3C","\u003c","\u003C" diff --git a/test/server_side_scripting/ext_table_preview/pandas_table_preview.py b/test/server_side_scripting/ext_table_preview/pandas_table_preview.py new file mode 120000 index 0000000000000000000000000000000000000000..f24b3901ce8610fd02fc28468b747b7171834307 --- /dev/null +++ b/test/server_side_scripting/ext_table_preview/pandas_table_preview.py @@ -0,0 +1 @@ +../../../src/server_side_scripting/ext_table_preview/pandas_table_preview.py \ No newline at end of file diff --git a/test/server_side_scripting/ext_table_preview/requirements.txt b/test/server_side_scripting/ext_table_preview/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..4628529ba9dce50a08d574e21d3b4a71b50af2b1 --- /dev/null +++ b/test/server_side_scripting/ext_table_preview/requirements.txt @@ -0,0 +1,3 @@ +caosdb +caosadvancedtools +pandas diff --git a/test/server_side_scripting/ext_table_preview/test_pandas_table_preview.py b/test/server_side_scripting/ext_table_preview/test_pandas_table_preview.py new file mode 100644 index 0000000000000000000000000000000000000000..00d1c7f38746abe437abc76cd51b29600adcd049 --- /dev/null +++ b/test/server_side_scripting/ext_table_preview/test_pandas_table_preview.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +# encoding: utf-8 +# +# ** header v3.0 +# This file is a part of the CaosDB Project. +# +# Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> +# Copyright (C) 2020 Henrik tom Wörden <h.tomwoerden@indiscale.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# +# ** end header +# + +import os +import unittest + +import caosdb as db +from caosdb.common.models import _parse_single_xml_element +from lxml import etree +from pandas_table_preview import (MAXIMUMFILESIZE, create_table_preview, + ending_is_valid, read_file, size_is_ok) + + +class PreviewTest(unittest.TestCase): + def test_file_ending(self): + self.assertFalse(ending_is_valid("/this/is/no/xls.lol")) + self.assertFalse(ending_is_valid("xls.lol")) + self.assertFalse(ending_is_valid("ag.xls.lol")) + assert ending_is_valid("/this/is/a/lol.xls") + assert ending_is_valid("/this/is/a/lol.csv") + assert ending_is_valid("/this/is/a/lol.cSv") + assert ending_is_valid("/this/is/a/lol.CSV") + assert ending_is_valid("lol.CSV") + + def test_file_size(self): + entity_xml = ('<File id="1234" name="SomeFile" ' + 'path="/this/path.tsv" size="{size}"></File>') + small = _parse_single_xml_element( + etree.fromstring(entity_xml.format(size="20000"))) + + assert size_is_ok(small) + large = _parse_single_xml_element( + etree.fromstring(entity_xml.format( + size=str(int(MAXIMUMFILESIZE+1))))) + assert not size_is_ok(large) + + def test_output(self): + files = [os.path.join(os.path.dirname(__file__), "data", f) + for f in ["test.csv", "test.tsv", "test.xls", "test.xlsx"]] + + for fi in files: + table = read_file(fi, ftype="."+fi.split(".")[-1]) + searchkey = fi.split(".")[-1]+"file" + print(table) + assert (table == searchkey).any(axis=None) + + badfiles = [os.path.join(os.path.dirname(__file__), "data", f) + for f in ["bad.csv", "bad.tsv", "bad.xls", "bad.xlsx"]] + + for bfi in badfiles: + self.assertRaises(ValueError, read_file, + bfi, "."+bfi.split(".")[-1])