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


4aaa	a </Property> @@ -35,10 +36,19 @@ </Property> </Record> <Record id="2112"> + <Version id="abc124" head="true"/> <Property id="117" name="Number" datatype="TEXT" importance="FIX"> 1101 </Property> <Property id="104" name="Bag" datatype="LIST<Bag>" importance="FIX"> </Property> </Record> + <Record id="2112"> + <Version id="abc125" head="true"/> + <Property id="117" name="Number" datatype="TEXT" importance="FIX"> + 1102 + </Property> + <Property id="104" name="Bag" datatype="LIST<Bag>" importance="FIX"> + </Property> + </Record> </Response> diff --git a/test/docker/Dockerfile b/test/docker/Dockerfile index bccd94fe14f49a79d8a43b559b3b857ed1d7d07d..026887097ac3b7dc13e6e429bf73c363e3adcbf3 100644 --- a/test/docker/Dockerfile +++ b/test/docker/Dockerfile @@ -1,8 +1,22 @@ -FROM debian:latest -RUN apt-get update && \ - apt-get install firefox-esr gettext-base pylint3 python3-pip \ - python3-httpbin git curl x11-apps xvfb unzip -y python3-pytest +FROM debian:10 +ADD node_gpg.asc /etc/apt/ +RUN apt-get update \ + && apt-get install -y gnupg ca-certificates\ + && apt-key add /etc/apt/node_gpg.asc \ + && echo "deb http://deb.debian.org/debian buster-backports main" >> /etc/apt/sources.list \ + && echo "deb https://deb.nodesource.com/node_14.x buster main" >> /etc/apt/sources.list \ + && apt-get update \ + && apt-get install -y \ + firefox-esr gettext-base python3-pip \ + python3-httpbin git curl x11-apps xvfb unzip \ + nodejs # Don't install `npm` (Debian), it conflicts with the `nodejs` (Node) package \ + && apt-get install -f + +RUN pip3 install pylint pytest RUN pip3 install caosdb -RUN pip3 install pandas xlrd +RUN pip3 install pandas xlrd==1.2.0 RUN pip3 install git+https://gitlab.com/caosdb/caosdb-advanced-user-tools.git@dev - +# For automatic documentation +#RUN npm install -g jsdoc +#RUN npm install -g jsdoc-sphinx +RUN pip3 install sphinx-js sphinx-autoapi recommonmark sphinx-rtd-theme diff --git a/test/docker/node_gpg.asc b/test/docker/node_gpg.asc new file mode 100644 index 0000000000000000000000000000000000000000..ed458a24e9132e0f22b6081d5e2b68926d00c725 --- /dev/null +++ b/test/docker/node_gpg.asc @@ -0,0 +1,54 @@ +Downloaded on 2020-12-18 + +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1 +Comment: GPGTools - https://gpgtools.org + +mQINBFObJLYBEADkFW8HMjsoYRJQ4nCYC/6Eh0yLWHWfCh+/9ZSIj4w/pOe2V6V+ +W6DHY3kK3a+2bxrax9EqKe7uxkSKf95gfns+I9+R+RJfRpb1qvljURr54y35IZgs +fMG22Np+TmM2RLgdFCZa18h0+RbH9i0b+ZrB9XPZmLb/h9ou7SowGqQ3wwOtT3Vy +qmif0A2GCcjFTqWW6TXaY8eZJ9BCEqW3k/0Cjw7K/mSy/utxYiUIvZNKgaG/P8U7 +89QyvxeRxAf93YFAVzMXhoKxu12IuH4VnSwAfb8gQyxKRyiGOUwk0YoBPpqRnMmD +Dl7SdmY3oQHEJzBelTMjTM8AjbB9mWoPBX5G8t4u47/FZ6PgdfmRg9hsKXhkLJc7 +C1btblOHNgDx19fzASWX+xOjZiKpP6MkEEzq1bilUFul6RDtxkTWsTa5TGixgCB/ +G2fK8I9JL/yQhDc6OGY9mjPOxMb5PgUlT8ox3v8wt25erWj9z30QoEBwfSg4tzLc +Jq6N/iepQemNfo6Is+TG+JzI6vhXjlsBm/Xmz0ZiFPPObAH/vGCY5I6886vXQ7ft +qWHYHT8jz/R4tigMGC+tvZ/kcmYBsLCCI5uSEP6JJRQQhHrCvOX0UaytItfsQfLm +EYRd2F72o1yGh3yvWWfDIBXRmaBuIGXGpajC0JyBGSOWb9UxMNZY/2LJEwARAQAB +tB9Ob2RlU291cmNlIDxncGdAbm9kZXNvdXJjZS5jb20+iQI4BBMBAgAiBQJTmyS2 +AhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRAWVaCraFdigHTmD/9OKhUy +jJ+h8gMRg6ri5EQxOExccSRU0i7UHktecSs0DVC4lZG9AOzBe+Q36cym5Z1di6JQ +kHl69q3zBdV3KTW+H1pdmnZlebYGz8paG9iQ/wS9gpnSeEyx0Enyi167Bzm0O4A1 +GK0prkLnz/yROHHEfHjsTgMvFwAnf9uaxwWgE1d1RitIWgJpAnp1DZ5O0uVlsPPm +XAhuBJ32mU8S5BezPTuJJICwBlLYECGb1Y65Cil4OALU7T7sbUqfLCuaRKxuPtcU +VnJ6/qiyPygvKZWhV6Od0Yxlyed1kftMJyYoL8kPHfeHJ+vIyt0s7cropfiwXoka +1iJB5nKyt/eqMnPQ9aRpqkm9ABS/r7AauMA/9RALudQRHBdWIzfIg0Mlqb52yyTI +IgQJHNGNX1T3z1XgZhI+Vi8SLFFSh8x9FeUZC6YJu0VXXj5iz+eZmk/nYjUt4Mtc +pVsVYIB7oIDIbImODm8ggsgrIzqxOzQVP1zsCGek5U6QFc9GYrQ+Wv3/fG8hfkDn +xXLww0OGaEQxfodm8cLFZ5b8JaG3+Yxfe7JkNclwvRimvlAjqIiW5OK0vvfHco+Y +gANhQrlMnTx//IdZssaxvYytSHpPZTYw+qPEjbBJOLpoLrz8ZafN1uekpAqQjffI +AOqW9SdIzq/kSHgl0bzWbPJPw86XzzftewjKNbkCDQRTmyS2ARAAxSSdQi+WpPQZ +fOflkx9sYJa0cWzLl2w++FQnZ1Pn5F09D/kPMNh4qOsyvXWlekaV/SseDZtVziHJ +Km6V8TBG3flmFlC3DWQfNNFwn5+pWSB8WHG4bTA5RyYEEYfpbekMtdoWW/Ro8Kmh +41nuxZDSuBJhDeFIp0ccnN2Lp1o6XfIeDYPegyEPSSZqrudfqLrSZhStDlJgXjea +JjW6UP6txPtYaaila9/Hn6vF87AQ5bR2dEWB/xRJzgNwRiax7KSU0xca6xAuf+TD +xCjZ5pp2JwdCjquXLTmUnbIZ9LGV54UZ/MeiG8yVu6pxbiGnXo4Ekbk6xgi1ewLi +vGmz4QRfVklV0dba3Zj0fRozfZ22qUHxCfDM7ad0eBXMFmHiN8hg3IUHTO+UdlX/ +aH3gADFAvSVDv0v8t6dGc6XE9Dr7mGEFnQMHO4zhM1HaS2Nh0TiL2tFLttLbfG5o +QlxCfXX9/nasj3K9qnlEg9G3+4T7lpdPmZRRe1O8cHCI5imVg6cLIiBLPO16e0fK +yHIgYswLdrJFfaHNYM/SWJxHpX795zn+iCwyvZSlLfH9mlegOeVmj9cyhN/VOmS3 +QRhlYXoA2z7WZTNoC6iAIlyIpMTcZr+ntaGVtFOLS6fwdBqDXjmSQu66mDKwU5Ek +fNlbyrpzZMyFCDWEYo4AIR/18aGZBYUAEQEAAYkCHwQYAQIACQUCU5sktgIbDAAK +CRAWVaCraFdigIPQEACcYh8rR19wMZZ/hgYv5so6Y1HcJNARuzmffQKozS/rxqec +0xM3wceL1AIMuGhlXFeGd0wRv/RVzeZjnTGwhN1DnCDy1I66hUTgehONsfVanuP1 +PZKoL38EAxsMzdYgkYH6T9a4wJH/IPt+uuFTFFy3o8TKMvKaJk98+Jsp2X/QuNxh +qpcIGaVbtQ1bn7m+k5Qe/fz+bFuUeXPivafLLlGc6KbdgMvSW9EVMO7yBy/2JE15 +ZJgl7lXKLQ31VQPAHT3an5IV2C/ie12eEqZWlnCiHV/wT+zhOkSpWdrheWfBT+ac +hR4jDH80AS3F8jo3byQATJb3RoCYUCVc3u1ouhNZa5yLgYZ/iZkpk5gKjxHPudFb +DdWjbGflN9k17VCf4Z9yAb9QMqHzHwIGXrb7ryFcuROMCLLVUp07PrTrRxnO9A/4 +xxECi0l/BzNxeU1gK88hEaNjIfviPR/h6Gq6KOcNKZ8rVFdwFpjbvwHMQBWhrqfu +G3KaePvbnObKHXpfIKoAM7X2qfO+IFnLGTPyhFTcrl6vZBTMZTfZiC1XDQLuGUnd +sckuXINIU3DFWzZGr0QrqkuE/jyr7FXeUJj9B7cLo+s/TXo+RaVfi3kOc9BoxIvy +/qiNGs/TKy2/Ujqp/affmIMoMXSozKmga81JSwkADO1JMgUy6dApXz9kP4EE3g== +=CLGF +-----END PGP PUBLIC KEY BLOCK----- diff --git a/test/server_side_scripting/ext_table_preview/__pycache__/pandas_table_preview.cpython-37.pyc b/test/server_side_scripting/ext_table_preview/__pycache__/pandas_table_preview.cpython-37.pyc deleted file mode 100644 index 613d9dce64b94c3b4c66891f22cd02a6c337dff6..0000000000000000000000000000000000000000 Binary files a/test/server_side_scripting/ext_table_preview/__pycache__/pandas_table_preview.cpython-37.pyc and /dev/null differ diff --git a/test/server_side_scripting/ext_table_preview/__pycache__/test_pandas_table_preview.cpython-37-pytest-6.0.2.pyc b/test/server_side_scripting/ext_table_preview/__pycache__/test_pandas_table_preview.cpython-37-pytest-6.0.2.pyc deleted file mode 100644 index c3563a411e0c836d2613ab7189dc6833be735e00..0000000000000000000000000000000000000000 Binary files a/test/server_side_scripting/ext_table_preview/__pycache__/test_pandas_table_preview.cpython-37-pytest-6.0.2.pyc and /dev/null differ