diff --git a/.gitignore b/.gitignore index cc9336b6256771f79eb104a8b1325a55352657e1..ef0861267fc42504b306d158ecfd332a44b3f6ce 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +# -*- mode:conf; -*- + # dot files .* !/.git* @@ -8,6 +10,9 @@ # the build dir /public +/sss_bin +/node_modules/ +/build # screen logs screenlog.* diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 548f86457c3216f5eed7b75b4b81d7f048351248..6caa815dba6c984cc36720866cf863bdd11e1512 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,30 @@ 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: + - 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 cdc0bbfb39e53824ff52ea968b8926f4eeb413b3..f2c54f430fd5e7f3ccd11294c8fe0daf5c7a23f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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 +* After a SELECT statement now also all referenced files can be downloaded. +* Automated documentation builds: `make doc` + ### Changed (for changes in existing functionality) +- enabled and enhanced autocompletion + +* Login form is hidden behind another button. ### Deprecated (for soon-to-be removed features) @@ -16,6 +34,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- #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) ## [v0.2.1] - 2020-09-07 diff --git a/makefile b/Makefile similarity index 81% rename from makefile rename to Makefile index 50dd72b500724bbe40801292ab39c3c5cf7ce70f..92f69dfb6234826b752a1c3355e1e812e5b2df61 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; \ @@ -64,10 +67,12 @@ test: clean cp-src cp-ext cp-ext-test cp-conf $(addprefix $(PUBLIC_DIR)/, $(TEST merge_xsl: misc/merge_xsl.sh +EXCLUDE_EXPR = %~ %.backup +BUILDFILELIST = $(filter-out $(EXCLUDE_EXPR),$(wildcard build.properties.d/*)) build_properties: @set -a -e ; \ pushd build.properties.files ; \ - for f in ../build.properties.d/* ; do source "$$f" ; done ; \ + for f in ${BUILDFILELIST} ; do echo "processing ../$$f" && source "../$$f" ; done ; \ popd ; \ BUILD_NUMBER=$(BUILD_NUMBER) ; \ PROPS=$$(printenv | grep -e "^BUILD_") ; \ @@ -127,18 +132,31 @@ 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-3 +test-sss: install-sss + $(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"; \ @@ -146,26 +164,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 @@ -191,13 +209,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 $@ @@ -206,7 +224,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 $@ @@ -257,17 +275,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 @@ -278,11 +303,13 @@ unzip: for f in $(LIBS_ZIP); do unzip -q -o -d libs $$f; done -PYLINT = pylint3 -d all -e E,F +PYLINT ?= pylint3 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 c027fd7d9af49c33492122fc575254b8d6b00845..8a2cd006f4e6963b9eb591268d0826b802229ae8 100644 --- a/README_SETUP.md +++ b/README_SETUP.md @@ -21,12 +21,14 @@ * ** end header --> -# Folder Structure +# Getting Started with the Web Interface -* 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,13 +36,13 @@ * 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`. -This file defines default variables which will be replaced in the source files -during the build. +This file defines default variables which can be used in source files and +will be replaced with the defined values during the build. 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 @@ -49,18 +51,34 @@ files in there which override the default values from `00_default.properties`. 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`) +- sphinx-js +- recommonmark diff --git a/build.properties.d/00_default.properties b/build.properties.d/00_default.properties index 23b1e9ff929b030750f6cab0014f22cd0d6b7cf4..cb0a89ce6d5cf991de67b9063caaf4349e34b59d 100644 --- a/build.properties.d/00_default.properties +++ b/build.properties.d/00_default.properties @@ -45,6 +45,11 @@ BUILD_MODULE_EXT_PREVIEW=ENABLED BUILD_MODULE_EXT_RESOLVE_REFERENCES=ENABLED BUILD_MODULE_EXT_SSS_MARKDOWN=DISABLED BUILD_MODULE_EXT_TRIGGER_CRAWLER_FORM=DISABLED +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 @@ -63,7 +68,7 @@ BUILD_FAVICON=pics/caosdb_logo_42.png ############################################################################## # Link to the data policy statement document. -BUILD_FOOTER_DATA_POLICY_HREF=https://indiscale.com/?page_id=156 +BUILD_FOOTER_DATA_POLICY_HREF=https://missing-domain.com/missing-page # Custom footer elements can be placed here (will be placed inside a <div> # element). 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/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 2bbc16407ea883a0f6cd37306da3c3a8875fdf25..031c6da4bd8c57d3dda652b7aef62376e2a97b7d 100644 --- a/src/core/js/caosdb.js +++ b/src/core/js/caosdb.js @@ -337,6 +337,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. diff --git a/src/core/js/edit_mode.js b/src/core/js/edit_mode.js index a4773b247181c841a63ee0107fe75012fb4eed18..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. */ @@ -1203,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 @@ -1548,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(); } @@ -1795,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 1bcf73dea17889bda9858ed78a2f6848e1dc21ee..bd7ec6a9938ff80afbd7dafdf0a734cd443678bc 100644 --- a/src/core/js/ext_bottom_line.js +++ b/src/core/js/ext_bottom_line.js @@ -42,8 +42,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) { +var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntityPath, connection, UTIF, ext_table_preview) { /** * @property {string|function} create - a function with one parameter @@ -103,7 +105,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(); + }); } /** @@ -119,6 +205,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. * @@ -128,17 +216,26 @@ var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntit id: "_default_creators.pictures", is_applicable: (entity) => _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) => _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), }, { // fallback id: "_default_creators.fallback", is_applicable: (entity) => true, create: (entity) => fallback_preview, - }, + } ]; @@ -231,8 +328,10 @@ var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntit } } catch (err) { logger.error(err); - const err_msg = "An error occured while loading this preview"; - set_preview_container(entity, err_msg); + if (!err._is_bottom_line_error) { + err = new BottomLineError(err); + } + set_preview_container(entity, err.to_html()); } } @@ -460,8 +559,12 @@ var ext_bottom_line = function($, logger, is_in_view_port, load_config, getEntit _css_class_preview_container, _css_class_preview_container_button, _css_class_preview_container_resolvable, + BottomLineError: BottomLineError, + BottomLineWarning: BottomLineWarning, } -}($, log.getLogger("ext_bottom_line"), resolve_references.is_in_viewport_vertically, load_config, getEntityPath, connection); +}($, log.getLogger("ext_bottom_line"), + resolve_references.is_in_viewport_vertically, load_config, getEntityPath, + connection, UTIF, ext_table_preview); /** @@ -484,20 +587,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; } @@ -525,7 +631,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_references.js b/src/core/js/ext_references.js index d9829cb9959c1249a4381bd45be9429f7dbef855..7cd597e128e8c09da9134f42f542898fb84a4e53 100644 --- a/src/core/js/ext_references.js +++ b/src/core/js/ext_references.js @@ -450,9 +450,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); @@ -525,8 +527,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..ebeb2ac94ca7bd84cc41ffef7bf3d1abc1982875 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'); - const columns = table.find("th").toArray() - .map(e => e.textContent) + return table.find("th").toArray() + .map(e => caosdb_table_export._clean_cell(e.textContent)) .filter(e => e.length > 0); + } + + /** + * 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'); // 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\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)]; + 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..47c8e0ce0cf0df2599c6ea1f2afee941edfab4cc 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. * @@ -164,6 +163,113 @@ 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; + } + + /** + * @type {AlertConfig} + * @property {string} [title] - an optional title for the alert. + * @property {string} [severity="danger"] - a bootstrap class suffix. Other + * examples: warning, info + * @property {string} message - informs the user what they are about to do. + * @property {function} proceed_callback - the function which is called + * then the user hits the "Proceed" button. + * @property {function} [cancel_callback] - a callback which is called then + * the cancel button is clicked. By default, only the alert is being + * closed an nothing happens. + * @property {string} [proceed_text="Proceed"] - the text on the proceed button. + * @property {string} [cancel_text="Cancel"] - the text on the cancel button. + * @property {string} [remember_my_decision_id] - if this parameter is + * present, a checkbox is appended to the alert ("Don't ask me + * again."). If the checkbox is checked the next time the make_alert + * function is called with the same remember_my_decision_id is created, + * the alert won't show up and the proceed_callback is called without + * any user interaction. + * @property {string} [remember_my_decision_text="Don't ask me again."] - + * label text for the checkbox. + * @property {HTMLElement} [proceed_button] - an optional custom proceed + * button. + * @property {HTMLElement] [cancel_button] - an optional custom cancel + * button. + */ + + /** + * Make an alert, that is a dialog which can intercept a function call and + * asks the user to proceed or cancel. + * + * @param {AlertConfig} config + * @return {HTMLElement} + */ + 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; + } + } + + // 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); + } + + + // 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(); + } + }); + 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]; + } + /** * (Re-)set this module's functions to standard implementation. */ 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/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 efbf2e6b6e4db0ad7b14cb1c2483157bc79001f1..e414f9af4147714c5f6ff8a03b70309a02ec1446 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')"/> @@ -255,6 +275,11 @@ <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_trigger_crawler_form.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..ca1884aea16d59c7df92de304d3154698dc471bf 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> diff --git a/src/doc/Makefile b/src/doc/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..b3c35b4ddbc2849eb6b3bdc223095e275669ad39 --- /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) + +.PHONY: doc-help Makefile apidoc + +# 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 + PATH=$(NPM_PATH):$$PATH $(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) +# sphinx-build -M html . ../../build/doc + +# Not necessary in this repository, apidoc is doone with the sphinx-autoapi extension +# apidoc: +# @$(SPHINXAPIDOC) -o _apidoc --update --title="CaosDB Server" ../main/ 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..988df3c3a2340c6b6b6b7e34f808e0e0496f680e --- /dev/null +++ b/src/doc/conf.py @@ -0,0 +1,209 @@ +# -*- 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.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/'] 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..b3a26c96df4f13e22f4b01a6cb0a9f00a1d33981 --- /dev/null +++ b/src/doc/index.rst @@ -0,0 +1,34 @@ + +Welcome to the documentation of CaosDB's web UI! +================================================ + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + :hidden: + + Getting started <getting_started> + Concepts <concepts> + tutorials + API Index<genindex> + + +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>`. + +.. note:: + + TODO: Build the index (manually?) like here: https://github.com/mozilla/fathom/edit/master/docs/. + Note that :doc:`autoapi/index` still does not have any content. + + .. js:autofunction:: input2caosdbDate + + .. js:autofunction:: getEntityVersion + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`search` diff --git a/src/ext/js/fileupload.js b/src/ext/js/fileupload.js index fac11efadc3d1b6d364333d68b27904c37c3aa42..e21ccf7f6035a8170bd1a0f4c7f5868d56c83b37 100644 --- a/src/ext/js/fileupload.js +++ b/src/ext/js/fileupload.js @@ -93,7 +93,7 @@ var fileupload = new function() { formData.append("FileRepresentation", request); - // add the success and error handlers which put the + // add the success and error handlers xhr.addEventListener("load", success_handler); xhr.addEventListener("error", error_handler); }); 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 50d8cbef8003d6fb9ab383f94405bcfa07270774..ea7b63b9e37943f0bdd4e32499c6c1ea9310a618 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> @@ -64,11 +67,13 @@ <script src="js/proj4.js"></script> <script src="js/proj4leaflet.js"></script> <script src="js/ext_map.js"></script> + <script src="js/ext_table_preview.js"></script> <script src="js/ext_bottom_line.js"></script> <script src="js/ext_revisions.js"></script> - <script src="js/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> @@ -80,14 +85,16 @@ <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> <script src="js/modules/ext_map.js.js"></script> <script src="js/modules/ext_bottom_line.js.js"></script> <script src="js/modules/ext_revisions.js.js"></script> - <script src="js/modules/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/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 b/test/core/js/modules/ext_bottom_line.js.js similarity index 88% rename from test/core/js/modules/ext_bottom_line.js rename to test/core/js/modules/ext_bottom_line.js.js index 21c92167271f87554a9af881554d33db686d9194..610d95d1b00b3a8bf220a6d39e12ed71ecaa9090 100644 --- a/test/core/js/modules/ext_bottom_line.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}); }" } ] }; @@ -67,7 +67,7 @@ var ext_bottom_line_test_suite = function ($, ext_bottom_line, QUnit) { }); QUnit.test("_creators", function (assert) { - assert.equal(ext_bottom_line._creators.length, 7, "seven creators"); + assert.equal(ext_bottom_line._creators.length, 9, "nine creators, 5 default ones, 4 from these tests."); }); QUnit.test("add_preview_container", function(assert) { @@ -105,7 +105,7 @@ var ext_bottom_line_test_suite = function ($, ext_bottom_line, QUnit) { assert.equal(container.text(), "blablabla"); break; case "error": - assert.equal(container.text(), "An error occured while loading this preview"); + assert.equal(container.text(), "An error occured while loading this preview.Test Error"); break; case "load-forever": assert.equal(container.text(), "Please wait..."); @@ -121,4 +121,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_xls_download.js.js b/test/core/js/modules/ext_xls_download.js.js index 4f9dc6c59b156f1f2265acb4b315887536667194..997a89ec21d4a22f49746cbda1c46bb56268f80d 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; @@ -116,9 +117,15 @@ QUnit.test("_get_tsv_string", function(assert) { const entities = $(table).find("tbody tr").toArray(); assert.equal(entities.length, 2, "two 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\tBag\tNumber\n242\t6366, 6406, 6407, 6408, 6409, 6410, 6411, 6412, 6413\t02 8 4aaa a\n2112\t\t1101", "tsv generated"); + tsv_string = caosdb_table_export._encode_tsv_string(tsv_string); + assert.equal(tsv_string.slice(0,prefix.length), prefix); + assert.equal(decodeURIComponent(tsv_string.slice(prefix.length, tsv_string.length)), + "ID\tBag\tNumber\n242\t6366, 6406, 6407, 6408, 6409, 6410, 6411, 6412, 6413\t02 8 4aaa a\n2112\t\t1101", "tsv generated"); }); 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..ae0a856f106557be3712f303b06a99f6220ef827 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 @@ -10,7 +10,7 @@ </Query> <Record id="242"> <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 diff --git a/test/docker/Dockerfile b/test/docker/Dockerfile index a19843fb3de11bbedf1c01f71619470bcc99f75c..7340ee5a7bb94aacc13ff582f1d05221419a312b 100644 --- a/test/docker/Dockerfile +++ b/test/docker/Dockerfile @@ -1,7 +1,15 @@ -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 +RUN echo "deb http://deb.debian.org/debian buster-backports main" >> /etc/apt/sources.list \ + && apt-get update \ + && apt-get install -y \ + firefox-esr gettext-base pylint3 python3-pip \ + python3-httpbin git curl x11-apps xvfb unzip python3-pytest \ + && apt-get install -y -t buster-backports \ + npm +RUN pip3 install caosdb +RUN pip3 install pandas xlrd +RUN pip3 install git+https://gitlab.com/caosdb/caosdb-advanced-user-tools.git@dev +# For automatic documentation +RUN npm install -g jsdoc +RUN pip3 install sphinx-js sphinx-autoapi recommonmark sphinx-rtd-theme diff --git a/test/server_side_scripting/ext_table_preview/__pycache__/pandas_table_preview.cpython-37.pyc b/test/server_side_scripting/ext_table_preview/__pycache__/pandas_table_preview.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..613d9dce64b94c3b4c66891f22cd02a6c337dff6 Binary files /dev/null and b/test/server_side_scripting/ext_table_preview/__pycache__/pandas_table_preview.cpython-37.pyc differ diff --git a/test/server_side_scripting/ext_table_preview/__pycache__/test_pandas_table_preview.cpython-37-pytest-6.0.2.pyc b/test/server_side_scripting/ext_table_preview/__pycache__/test_pandas_table_preview.cpython-37-pytest-6.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c3563a411e0c836d2613ab7189dc6833be735e00 Binary files /dev/null and b/test/server_side_scripting/ext_table_preview/__pycache__/test_pandas_table_preview.cpython-37-pytest-6.0.2.pyc differ 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])